diff --git a/README.md b/README.md index c1659cb..efa5dc2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ## Demo Projects - tsx-dom-demo\ - A simple To-Do list showing one way you might use [tsx-dom](https://lusito.github.io/tsx-dom/tsx-dom/index.html). + A simple [To-Do list](https://lusito.github.io/tsx-dom/demo/) showing one way you might use [tsx-dom](https://lusito.github.io/tsx-dom/tsx-dom/index.html). - tsx-dom-ssr-demo\ A more complex demo showcasing [tsx-dom-ssr](https://lusito.github.io/tsx-dom/tsx-dom-ssr/index.html) with the [Rick and Morty API](https://rickandmortyapi.com/) diff --git a/packages/tsx-dom-ssr/docs/custom-elements.md b/packages/tsx-dom-ssr/docs/custom-elements.md index 09e349f..c9e793b 100644 --- a/packages/tsx-dom-ssr/docs/custom-elements.md +++ b/packages/tsx-dom-ssr/docs/custom-elements.md @@ -76,3 +76,27 @@ const child = ( , ); ``` + +## The `is` Attribute vs `tsxTag` + +Let's say you have a custom element `word-count`, but it should fall back to a p tag when the custom-element has not been registered (yet). +Normally, you would do it like this: + +```html +

+``` + +This tells the browser it's a `p` tag, but execute the code for `word-count` on it, once it's defined. +This won't work properly with tsx-dom, as you will get no intellisense for the respective props of that custom element. + +In order to solve this issue, we have the `tsxTag` attribute, which does the exact inverse: + +```tsx +const child = ; +``` + +By doing this, you'll get the correct props while writing the JSX syntax, but the generated HTML will still be: + +```html +

+``` diff --git a/packages/tsx-dom-ssr/src/types.ts b/packages/tsx-dom-ssr/src/types.ts index 5bbf40f..d4210a9 100644 --- a/packages/tsx-dom-ssr/src/types.ts +++ b/packages/tsx-dom-ssr/src/types.ts @@ -11,6 +11,11 @@ export interface BaseProps { export interface HTMLComponentProps extends BaseProps { dangerouslySetInnerHTML?: string; + /** + * This is essentially a reverse "is" attribute. + * If you specify it, the generated tag will be tsxTag and it will receive an "is" attribute with the tag you specified in your JSX. + * This is needed because we can't make the is-property associate with the correct component props. + */ tsxTag?: keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap; } diff --git a/packages/tsx-dom/docs/custom-elements.md b/packages/tsx-dom/docs/custom-elements.md index a9b73ee..fc51ed3 100644 --- a/packages/tsx-dom/docs/custom-elements.md +++ b/packages/tsx-dom/docs/custom-elements.md @@ -123,3 +123,27 @@ document.body.appendChild( , ); ``` + +## The `is` Attribute vs `tsxTag` + +Let's say you have a custom element `word-count`, but it should fall back to a p tag when the custom-element has not been registered (yet). +Normally, you would do it like this: + +```html +

+``` + +This tells the browser it's a `p` tag, but execute the code for `word-count` on it, once it's defined. +This won't work properly with tsx-dom, as you will get no intellisense for the respective props of that custom element. + +In order to solve this issue, we have the `tsxTag` attribute, which does the exact inverse: + +```tsx +const element = ; +``` + +By doing this, you'll get the correct props while writing the JSX syntax, but the generated HTML will still be: + +```html +

+``` diff --git a/packages/tsx-dom/src/createElement.ts b/packages/tsx-dom/src/createElement.ts index 1418f51..ab89936 100644 --- a/packages/tsx-dom/src/createElement.ts +++ b/packages/tsx-dom/src/createElement.ts @@ -1,6 +1,6 @@ import { setAttributes } from "./setAttributes"; import type { ComponentAttributes, ComponentChild, FC, RefType } from "./types"; -import { applyChildren, createDomElement } from "./utils"; +import { applyChildren, applyTsxTag, createDomElement } from "./utils"; export function createElement( tag: string | FC, @@ -9,9 +9,10 @@ export function createElement( ): JSX.Element { if (typeof tag === "function") return tag({ ...attrs, children }); - const element = createDomElement(tag, attrs); + const { finalTag, finalAttrs } = applyTsxTag(tag, attrs); + const element = createDomElement(finalTag, finalAttrs); - if (attrs) setAttributes(element, attrs); + if (finalAttrs) setAttributes(element, finalAttrs); applyChildren(element, children); return element; diff --git a/packages/tsx-dom/src/jsx-runtime.ts b/packages/tsx-dom/src/jsx-runtime.ts index 838a096..f34a346 100644 --- a/packages/tsx-dom/src/jsx-runtime.ts +++ b/packages/tsx-dom/src/jsx-runtime.ts @@ -1,14 +1,15 @@ import { setAttributes } from "./setAttributes"; import type { BaseProps, FC } from "./types"; -import { applyChildren, createDomElement } from "./utils"; +import { applyChildren, createDomElement, applyTsxTag } from "./utils"; export function jsx(tag: string | FC, props: BaseProps): JSX.Element { if (typeof tag === "function") return tag(props); const { children, ...attrs } = props; - const element = createDomElement(tag, attrs); + const { finalTag, finalAttrs } = applyTsxTag(tag, attrs); + const element = createDomElement(finalTag, finalAttrs); - if (attrs) setAttributes(element, attrs); + setAttributes(element, finalAttrs); applyChildren(element, [children]); return element; diff --git a/packages/tsx-dom/src/setAttributes.ts b/packages/tsx-dom/src/setAttributes.ts index 712ff34..6f40b39 100644 --- a/packages/tsx-dom/src/setAttributes.ts +++ b/packages/tsx-dom/src/setAttributes.ts @@ -15,7 +15,7 @@ const eventAttributeName = /^on\p{Lu}/u; export function setAttributes(element: JSX.Element, attrs: ComponentAttributes) { for (const name of Object.keys(attrs)) { // Ignore some debug props that might be added by bundlers - if (name === "__source" || name === "__self") continue; + if (name === "__source" || name === "__self" || name === "tsxTag") continue; const value = attrs[name]; if (name === "class") { diff --git a/packages/tsx-dom/src/types.ts b/packages/tsx-dom/src/types.ts index ed4710e..5b79961 100644 --- a/packages/tsx-dom/src/types.ts +++ b/packages/tsx-dom/src/types.ts @@ -12,6 +12,12 @@ export type ComponentAttributes = { export interface HTMLComponentProps extends BaseProps { dangerouslySetInnerHTML?: string; + /** + * This is essentially a reverse "is" attribute. + * If you specify it, the generated tag will be tsxTag and it will receive an "is" attribute with the tag you specified in your JSX. + * This is needed because we can't make the is-property associate with the correct component props. + */ + tsxTag?: keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap; ref?: RefType; } diff --git a/packages/tsx-dom/src/utils.ts b/packages/tsx-dom/src/utils.ts index f8bc6e0..132f663 100644 --- a/packages/tsx-dom/src/utils.ts +++ b/packages/tsx-dom/src/utils.ts @@ -23,3 +23,15 @@ export function createDomElement(tag: string, attrs: ComponentAttributes | null) return document.createElement(tag, options); } + +export function applyTsxTag(tag: string, attrs: T) { + let finalTag = tag; + let finalAttrs = attrs; + if (finalAttrs && "tsxTag" in finalAttrs) { + finalTag = finalAttrs.tsxTag as string; + if (!finalAttrs.is && tag.includes("-")) { + finalAttrs = { ...finalAttrs, is: tag }; + } + } + return { finalTag, finalAttrs }; +}