Skip to content

herp-inc/snabbdom-jsx

Repository files navigation

@herp-inc/snabbdom-jsx npm

Yet another JSX pragma for Snabbdom

Features

Example

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

With npm

$ npm install @herp-inc/snabbdom-jsx

With yarn

$ 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,
  },
});

Add the following options to your tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@herp-inc/snabbdom-jsx"
  }
}

With Babel

Add @babel/plugin-transform-react-jsx to your devDependencies.

Add the following options to your Babel configuration:

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "importSource": "@herp-inc/snabbdom-jsx",
        "runtime": "automatic"
      }
    ]
  ]
}

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, role, and popoverTarget

The list, the role, and the popoverTarget attributes will be passed to the attributes module. Note that the attribute names will be lowercased.

<div role="button" />
// { attrs: { role: 'button' } }

<input list="options" />
// { attrs: { list: 'options' } }

<button popoverTarget="popover" />
// { attrs: { popovertarget: 'popover' } }

$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 $.

<div $attrs={{ class: 'foo' }} />
// { attrs: { class: 'foo' } }
<div $class={{ foo: true }} />
// { class: { foo: true } }
<div $dataset={{ foo: 'bar' }} />
// { dataset: { foo: 'bar' } }
<div
  $on={{
    click: (e) => {
      console.log(e);
    },
  }}
/>
// { on: { click: f } }
<div $props={{ className: 'foo' }} />
// { props: { className: 'foo' } }
<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 on 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/jsx-runtime' {
  namespace jsx {
    interface CustomModules {
      // Add your custom modules here
      custom: {
        foo: string;
      };
    }
  }
}

Components

A JSX component can be defined with a function with the signature of <Props>(props: Props) => Snabbdom.VNodeChildElement.

import type Snabbdom from '@herp-inc/snabbdom-jsx';

type Props = {
  children: Snabbdom.Node;
  name: string;
};

const Component: Snabbdom.Component<Props> = ({ children, name }) => (
  <div>
    Hello, {name}!<div>{children}</div>
  </div>
);

const vnode = <Component />;

Caveats

  • boolean, null, and undefined values are not be filtered out of the tree but rendered as comment nodes (for the sake of correct diffing)
  • snabbdom-pragma-style MODULE-PROPERTY notation is not supported.

Acknowledgements

The code base is based on these libraries: