Yet another JSX pragma for Snabbdom
Features
- Straightforward and intuitive syntax:
<input type="text" />
rather than<input props={{ type: 'text' }}>
- Attributes on intrinsic elements are typechecked (only for HTML elements for now)
-
className
andid
will be the part of thesel
- Type-safe custom modules via module augmentation
- Support for React 17 style new JSX transform
Example
import { jsx } from '@herp-inc/snabbdom-jsx';
const vnode = (
<div id="container" className="two classes" onclick={someFn}>
<span $style={{ fontWeight: 'bold' }}>This is bold</span> and this is just normal text
<a href="/foo">I'll take you places!</a>
</div>
);
Installation
Note that the following packages are peer dependencies of this library, which need to be installed separately.
Package | Version |
---|---|
csstype |
3 |
snabbdom |
3 |
npm
With$ npm install @herp-inc/snabbdom-jsx
yarn
With$ yarn add @herp-inc/snabbdom-jsx
Usage
Note that fragments are still experimental. Make sure you are using Snabbdom v3.2+ and opt it in to enable the feature.
const patch = init(modules, undefined, {
experimental: {
fragments: true,
},
});
TypeScript
WithClassic runtime
Add the following options to your tsconfig.json
:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "jsx",
"jsxFragmentFactory": "Fragment"
}
}
Then import the jsx
function in your .tsx
file.
import { jsx } from '@herp-inc/snabbdom-jsx';
const vnode = <div>Hello, JSX!</div>;
When you want to use a JSX fragment, you also have to import the Fragment
function.
import { Fragment, jsx } from '@herp-inc/snabbdom-jsx';
const vnode = (
<>
Hello, <strong>JSX</strong>!
</>
);
Automatic runtime
Make sure you are using TypeScript v4.1+ and add the following options to your tsconfig.json
:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@herp-inc/snabbdom-jsx"
}
}
Then the jsx
and the jsxs
functions will automatically be imported.
Babel
WithAdd @babel/plugin-transform-react-jsx
to your devDependencies
.
Classic runtime
Make sure are using Babel v7.9.0+ and add the following options to your Babel configuration:
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "jsx",
"pragmaFrag": "Fragment",
"runtime": "classic"
}
]
]
}
Then import the jsx
function in your file.
import { jsx } from '@herp-inc/snabbdom-jsx';
const vnode = <div>Hello, JSX!</div>;
When you want to use a JSX fragment, you also have to import the Fragment
function.
import { Fragment, jsx } from '@herp-inc/snabbdom-jsx';
const vnode = (
<>
Hello, <strong>JSX</strong>!
</>
);
Automatic runtime
Add the following options to your Babel configuration:
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"importSource": "@herp-inc/snabbdom-jsx",
"runtime": "automatic"
}
]
]
}
Then the jsx
and the jsxs
functions will automatically be imported.
Attributes mapping
By default, an attribute will be passed to the props module.
<input type="text" />
// { props: { type: 'text' } }
However, certain attributes will be treated differently.
className
and id
className
and id
attributes will be concatenated to the sel
with .
and #
respectively, and won't be passed to any modules. For example, the expression <div id="foo" className="bar baz" />
will yield a virtual node with { sel: 'div#foo.bar.baz' }
aria-*
An attribute starting with aria-
will be passed to the attributes module.
<button aria-label="Send" />
// { attrs: { 'aria-label': 'Send' } }
data-*
An attribute starting with data-
will be passed to the dataset module. Note that the data-
prefix will be removed and dashes will be converted to camel case.
<div data-foo-bar="baz" />
// { dataset: { fooBar: 'baz' } }
is
The is
attribute can be used when you want to instantiate your customized built-in elements.
<div is="custom-element" />
// { is: 'custom-element' }
on*
An attribute starting with on
will passed to the event listeners module.
<div
onclick={(e) => {
console.log(e);
}}
/>
// { on: { click: f } }
list
and role
The list
and the role
attributes will be passed to the attributes module.
<div role="button" />
// { attrs: { role: 'button' } }
<input list="options" />
// { attrs: { list: 'options' } }
$hook
The $hook
attribute is treated as hooks.
<div
$hook={{
insert(vnode) {
console.log(vnode);
},
}}
/>
// { hook: { insert: f } }
For the sake of backward compatibility, hook
(without the dollar sign) also behaves the same. However it is deprecated and will be removed in the future.
$key
The $key
attribute is treated as a key.
<div $key="foo" />
// { key: 'foo' }
For the sake of backward compatibility, key
(without the dollar sign) also behaves the same. However it is deprecated and will be removed in the future.
SVG elements
Attributes of <svg>
and its descendant elements are passed to the attributes module.
Built-in modules
In Snabbdom, different functionalities are delegated to separate modules. Values can be passed to them via attributes starting with $
.
$attrs
(the attributes module)
<div $attrs={{ class: 'foo' }} />
// { attrs: { class: 'foo' } }
$class
(the class module)
<div $class={{ foo: true }} />
// { class: { foo: true } }
$dataset
(the dataset module)
<div $dataset={{ foo: 'bar' }} />
// { dataset: { foo: 'bar' } }
$on
(the event listeners module)
<div
$on={{
click: (e) => {
console.log(e);
},
}}
/>
// { on: { click: f } }
$props
(the props module)
<div $props={{ className: 'foo' }} />
// { props: { className: 'foo' } }
$style
(the style module)
<div $style={{ opacity: '0', delayed: { opacity: '1' }, remove: { opacity: '0' } }} />
// { style: { opacity: '0', delayed: { opacity: '1' }, remove: { opacity: '0' } } }
Aliases
For the sake of backward compatibility, the following aliases are also defined. However they are deprecated and will be removed in the future.
Attribute | Alias(es) |
---|---|
$attrs |
attrs |
$class |
class |
$dataset |
data , dataset
|
$on |
on |
$props |
props |
$style |
style |
Custom modules
Just like built-in modules, you can pass an arbitrary value to your custom modules via an attribute starting with $
. For example, the expression <div $custom={{ foo: 'bar' }} />
will yield { custom: { foo: 'bar' } }
.
Note for TypeScript users
Unlike built-in modules, we have no assumptions about what kind of values should be passed to custom modules. You have to augment jsx.CustomModules
interface so that it will typecheck.
declare module '@herp-inc/snabbdom-jsx' {
namespace jsx {
interface CustomModules {
// Add your custom modules here
custom: {
foo: string;
};
}
}
}
Components
A component can be defined with a function with the signature of <Props>(props: Props, children: Snabbdom.Node[]) => Snabbdom.Node
.
import Snabbdom, { jsx } from '@herp-inc/snabbdom-jsx';
type Props = {
name: string;
};
const Component: Snabbdom.Component<Props> = ({ name }, children) => (
<div>
Hello, {name}!<div>{children}</div>
</div>
);
const vnode = <Component />;
Caveats
-
boolean
,null
, andundefined
values are not be filtered out of the tree and rendered as comment nodes (for the sake of correct diffing) -
snabbdom-pragma
-styleMODULE-PROPERTY
notation is not supported.
Acknowledgements
The code base is based on these libraries: