Skip to content

Commit

Permalink
tsxTag for tsx-dom and docs for it
Browse files Browse the repository at this point in the history
  • Loading branch information
Lusito committed Jun 7, 2024
1 parent 8e26069 commit 34d8104
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

Expand Down
24 changes: 24 additions & 0 deletions packages/tsx-dom-ssr/docs/custom-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,27 @@ const child = (
</my-custom-select>,
);
```

## 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
<p is="word-count"></p>
```

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 = <word-count tsxTag="p" />;
```

By doing this, you'll get the correct props while writing the JSX syntax, but the generated HTML will still be:

```html
<p is="word-count"></p>
```
5 changes: 5 additions & 0 deletions packages/tsx-dom-ssr/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
24 changes: 24 additions & 0 deletions packages/tsx-dom/docs/custom-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,27 @@ document.body.appendChild(
</my-custom-select>,
);
```

## 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
<p is="word-count"></p>
```

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 = <word-count tsxTag="p" />;
```

By doing this, you'll get the correct props while writing the JSX syntax, but the generated HTML will still be:

```html
<p is="word-count"></p>
```
7 changes: 4 additions & 3 deletions packages/tsx-dom/src/createElement.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions packages/tsx-dom/src/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/tsx-dom/src/setAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
6 changes: 6 additions & 0 deletions packages/tsx-dom/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type ComponentAttributes = {

export interface HTMLComponentProps<T extends Element> 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<T>;
}

Expand Down
12 changes: 12 additions & 0 deletions packages/tsx-dom/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ export function createDomElement(tag: string, attrs: ComponentAttributes | null)

return document.createElement(tag, options);
}

export function applyTsxTag<T extends null | ComponentAttributes>(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 };
}

0 comments on commit 34d8104

Please sign in to comment.