Skip to content
Alexander Zaytsev edited this page Aug 29, 2024 · 26 revisions

jsx-dom-runtime

It's the Babel plugin to transforms JSX syntax to the DOM elements with minimal runtime dependency ~500 B. Support HTML, SVG, and MathML tags.

source code:

document.body.append(
  <main class="box">
    <h1 class="title">Hello, World!</h1>
  </main>
);

after compilation:

import { jsx as _jsx } from "jsx-dom-runtime";

document.body.append(
  _jsx("main", {
    class: "box",
    children: _jsx("h1", {
      class: "title",
      children: "Hello, World!"
    })
  })
);

You don't need to import any dependencies to your code. The babel will automatically import all necessary functions.

Install

npm i jsx-dom-runtime
# or
yarn add jsx-dom-runtime

How to use

Add preset to your .babelrc file.

.babelrc

{
  "presets": [
    "jsx-dom-runtime/babel-preset"
  ]
}

Syntax

Support the general JSX syntax.

Attributes

Write the attributes closer to HTML that to JavaScript

Use attribute class instead of the className DOM property as in React.

- <div className="box" />
+ <div class="box" />

Attribute for instead htmlFor DOM property

- <label htmlFor="cheese">Do you like cheese?</label>
+ <label for="cheese">Do you like cheese?</label>

Style

The style attribute supports the JavaScript object and a string value. Also, you can use CSS custom properties

<div style="background-color: #ffe7e8; border: 2px solid #e66465;" />
<div style="--color: red;" />
// or
<div style={{ backgroundColor: '#ffe7e8', border: '2px solid #e66465' }} />
<div style={{ '--color': 'red' }} />

SVG

Use unmodified SVG attributes instead of camelCase style as in React

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
-  <circle strokeWidth="2" strokeLinejoin="round" cx="24" cy="24" r="20" fill="none" />
+  <circle stroke-width="2" stroke-linejoin="round" cx="24" cy="24" r="20" fill="none" />
</svg>

Don't use namespaced attributes. The namespaced attributes are deprecated and no longer recommended.

Instead of xlink:href you should use href

<svg viewBox="0 0 160 40" xmlns="http://www.w3.org/2000/svg">
-  <a xlink:href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href">
+  <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href">
    <text x="10" y="25">MDN Web Docs</text>
  </a>
</svg>

Event handling

There are a few ways to add event handling to a DOM Element.

  1. Using the event handler properties that start with on* as onclick or ondblclick.
<button
  type="button"
  onclick={(event) => { }}
  ondblclick={(event) => { }}
>
  Click Me!
</button>;

Be attention! In this way, the event listener will be assigned directly to the Element object as a property.

// Equivalent on vanilla JavaScript
button.onclick = (event) => { };
button.ondblclick = (event) => { };
  1. Using the namespace syntax for the event listener that start with on:* as on:change or on:focus.
<input
  type="text"
  on:change={(event) => { }}
  on:focus={(event) => { }}
/>

After the compilation, it registers the event with addEventListener

// Equivalent on vanilla JavaScript
input.addEventListener('change', (event) => { });
input.addEventListener('focus', (event) => { });
  1. Using ref callback. The callback will be called with a target element when the element will be created.
<button
  type="button"
  ref={(node) => {
    // Use capture phase
    node.addEventListener('click', (event) => { }, true);
    // With event options
    node.addEventListener('dblclick', (event) => { }, { once: true });
  }}>
  Click Me!
</button>;

Function Components

Function components must start with a capital letter or they won’t work.

const App = ({ name }) => (
  <div>Hello {name}</div>
);

document.body.append(<App name="Bob" />);

Fragments

Use <>...</> syntax, to group multiple elements together. Under the hood, it use DocumentFragment interface.

document.body.append(
  <>
    <p>Hello</p>
    <p>World</p>
  </>
);

APIs

Creating Refs

Adding a reference to a DOM Element. When a ref is passed to an element in create, a reference to the node becomes accessible at the current attribute of the ref

import { useRef } from 'jsx-dom-runtime';

const ref = useRef();

const addItem = () => {
  // add an item to the list
  ref.current.append(<li>New Item</li>);
};

document.body.append(
  <>
    <button type="button" on:click={addItem}>
      Add Item
    </button>
    <ul ref={ref} />
  </>
);

Callback Refs

Another way to get the reference to an element can be done by passing a function callback. The callback will be called with the actual reference DOM element

const setRef = (node) => {
  node.addEventListener('focusin', () => {
    node.style.backgroundColor = 'pink';
  });

  node.addEventListener('focusout', () => {
    node.style.backgroundColor = '';
  });
};

document.body.append(
  <input type="text" ref={setRef} />
);

Text

Use the Text node in a DOM tree.

import { useText } from 'jsx-dom-runtime';

const [text, setText] = useText('The initial text');

const clickHandler = () => {
  setText('Clicked!');
};

document.body.append(
  <>
    <p>{text}</p>
    <button type="button" on:click={clickHandler}>
      Click me
    </button>
  </>
);

Template

Get template from a string.

import { Template } from 'jsx-dom-runtime';

document.body.append(
  <Template>
    {`<svg width="24" height="24" aria-hidden="true">
        <path d="M12 12V6h-1v6H5v1h6v6h1v-6h6v-1z"/>
      </svg>`}
  </Template>
);

properties

Add support of the DOM Element object properties. By default supported property value.

import { properties } from 'jsx-dom-runtime';

properties.add('textContent') // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
  .add('innerHTML') // https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
  .add('volume') // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
  .add('muted'); // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted

document.body.append(
  <>
    <div textContent="Hello, world!" />
    <div innerHTML="<p>Hello, world!</p>" />
    <audio
      src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3"
      controls
      volume={0.9}
      muted
    />
  </>
);

TypeScript types definition for the DOM Element properties

global.d.ts

declare global {
  namespace JSX {
    // add types for all JSX elements
    interface Attributes {
      textContent?: string;
      innerHTML?: string;
    }

    // add types only for Audio element
    interface HTMLAudioElementAttributes {
      volume?: number;
      muted?: boolean;
    }
  }
}

export {};

extensions

Add custom attributes in JSX.Element.

import { extensions } from 'jsx-dom-runtime';

extensions
  .set('x-class', (node, value) => {
    node.setAttribute('class', value.filter(Boolean).join(' '));
  })
  .set('x-dataset', (node, value) => {
    for (let key in value) {
      if (value[key] != null) {
        node.dataset[key] = value[key];
      }
    }
  })
  .set('x-autofocus', (node, value) => {
    setTimeout(() => node.focus(), value);
  });

document.body.append(
  <input
    x-class={['one', 'two']}
    x-dataset={{ testid: 'test', hook: 'text' }}
    x-autofocus={1000}
  />
);

Result

<input class="one two" data-testid="test" data-hook="text">

TypeScript types definition for custom attributes:

global.d.ts

declare global {
  namespace JSX {
    // add types for all JSX elements
    interface Attributes {
      'x-class'?: string[];
      'x-dataset'?: Record<string, string>;
    }

    // add types only for Input elements
    interface HTMLInputElementAttributes {
      'x-autofocus'?: number;
    }
  }
}

export {};

TypeScript Support

TypeScript can be used only for type checking. The library doesn't support compilation with TypeScript. Use @babel/preset-typescript

.babelrc

{
  "presets": [
    "@babel/preset-typescript",
    "jsx-dom-runtime/babel-preset"
  ]
}

To provide type checking add tsconfig.json file to your project:

tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "jsx-dom-runtime",
    "moduleResolution": "node",
    "noEmit": true,
    "lib": [
      "DOM"
    ]
  }
}

Example:

src/index.tsx

import { useText } from 'jsx-dom-runtime';

interface Props {
  label: string;
}

const App: JSX.FC<Props> = ({ label }) => {
  let i = 0;

  const [textNode, setCount] = useText(i);

  const clickHandler: JSX.EventListener = () => {
    setCount(++i);
  };

  return (
    <div class="card">
      <h1 class="label">{label}</h1>
      <button type="button" on:click={clickHandler}>
        Click me! {textNode}
      </button>
    </div>
  );
};

document.body.append(<App label="Hello!" />);

License

MIT