npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@exodra/dom

v0.2.0

Published

High-performance DOM renderer for Exodra - mount, hydrate, and update with WeakMap-based tracking

Readme

@exodra/dom

DOM rendering and hydration for Exodra applications.

Installation

npm install @exodra/dom @exodra/core @exodra/reactivity

Overview

@exodra/dom renders Exodra schemas into the browser DOM:

  • Mounting — render a schema into a container
  • Hydration — attach to server-rendered HTML
  • Event handling — handlers declared in the handlers bucket
  • Reactive attributes — driven by bindables in the bindables bucket
  • Lifecycledispose() tears the tree down and detaches it from the DOM

This package consumes raw Exodra schemas produced by h() (from @exodra/core) or compiled from JSX by @exodra/babel-plugin-jsx. Exodra has a strict bucketed props model — attributes, reactive values, lists, and event handlers each live in their own bucket. There are no flat React-style props.

The raw h() buckets are plural: static, bindables, bindableLists, handlers, bindableHandlers. (In JSX they are written singular — static, bindable, bindableList, handlers, bindableHandlers — and the babel plugin maps them to the plural core buckets.)

API

import { mount, hydrate } from '@exodra/dom';
import type { TExoDomMountResult } from '@exodra/dom';

// mount(schema, container) — append a schema's element into `container`
mount(schema, container): TExoDomMountResult

// hydrate(schema, element) — adopt existing server-rendered DOM
hydrate(schema, element): TExoDomMountResult

TExoDomMountResult is:

type TExoDomMountResult = {
  node: ExoNodeDom;        // the live node tree
  element: Element | Text; // the mounted root (the container for a Fragment root)
  dispose(): void;         // unmount + detach from the DOM
};

There is no standalone unmount(container), and mount does not return a function. To unmount, keep the result and call result.dispose().

Mounting

import { h, text } from '@exodra/core';
import { mount } from '@exodra/dom';

const app = h('div', {
  static: {
    class: 'app',
    children: [
      h('h1', { static: { children: text('Hello Exodra') } }),
      h('button', {
        static: { children: text('Click me') },
        handlers: { onClick: () => alert('Clicked!') },
      }),
    ],
  },
});

const { dispose } = mount(app, document.getElementById('root')!);

// Later, unmount and detach from the DOM:
dispose();

Note: the 3rd argument to h(type, attrs, cacheKey) is a clone-cache key, not children. Children always live inside a bucket — static.children (static), bindables.children (a single reactive child), or bindableLists.children (a reactive list). If specified in more than one bucket, the last wins.

Hydration

For SSR apps, hydrate adopts the existing server-rendered element instead of recreating it:

import { hydrate } from '@exodra/dom';

const { dispose } = hydrate(app, document.getElementById('root')!);

Event Handling

Event handlers go in the handlers bucket. A flat onClick would silently land in static as a dead attribute, so Exodra keeps them separate:

h('button', {
  static: { children: text('Interactive') },
  handlers: {
    onClick: e => console.log('Clicked!', e),
    onMouseOver: () => console.log('Hover!'),
  },
});

// Prevent default in a form submit:
h('form', {
  handlers: {
    onSubmit: e => {
      e.preventDefault();
      // handle form
    },
  },
});

For a reactive handler (a bindable whose value is the current listener), use the bindableHandlers bucket.

Reactive Attributes

Reactive values are bindables/derived from @exodra/reactivity, passed directly into the bindables bucket — they are bindable objects, not thunks. Read with .getValue(), write with .setValue(...). There is no .value and no .get()/.set().

import { bindable, derive } from '@exodra/reactivity';

const isActive = bindable(false);
const cls = derive(isActive, active => (active ? 'active' : 'inactive'));

h('div', {
  bindables: { class: cls },
  handlers: { onClick: () => isActive.setValue(!isActive.getValue()) },
});

Working with Forms

Bind an input's value to a writable bindable, and update it from the event:

import { bindable } from '@exodra/reactivity';

const name = bindable('');
const email = bindable('');

h('form', {
  static: {
    children: [
      h('input', {
        static: { type: 'text' },
        bindables: { value: name },
        handlers: {
          onInput: e => name.setValue((e.target as HTMLInputElement).value),
        },
      }),
      h('input', {
        static: { type: 'email' },
        bindables: { value: email },
        handlers: {
          onInput: e => email.setValue((e.target as HTMLInputElement).value),
        },
      }),
    ],
  },
});

The @exodra/forms package provides bindText, bindNumber, bindChecked, and bindSelect helpers that produce the { bindables, handlers } pair for you.

Conditional Rendering

Use a derived bindable as a single reactive child via bindables.children:

import { bindable, derive } from '@exodra/reactivity';

const showDetails = bindable(false);
const details = derive(showDetails, show =>
  show ? h('div', { static: { children: text('Details…') } }) : text('')
);

h('div', {
  static: {
    children: [
      h('button', {
        static: { children: text('Toggle') },
        handlers: {
          onClick: () => showDetails.setValue(!showDetails.getValue()),
        },
      }),
    ],
  },
  bindables: { children: details },
});

List Rendering

Reactive lists come from list() and go into the bindableLists bucket:

import { list } from '@exodra/reactivity';

const items = list<string>(['Item 1', 'Item 2']);

const ul = h('ul', {
  bindableLists: {
    children: list([
      h('li', { static: { children: text('Item 1') } }),
      h('li', { static: { children: text('Item 2') } }),
    ]),
  },
});

// Mutate with list ops — push / insert / remove / move / set / reset:
items.push('Item 3');

list is op-based: it exposes snapshot(), subscribeOps(), push, insert, remove, move, set, reset. It has no map/filter/forEach/length.

Lifecycle Hooks

Static lifecycle hooks onExoMount / onExoUnmount live in the static bucket and receive the node ({ element }):

h('div', {
  static: {
    onExoMount: node => console.log('mounted', node.element),
    onExoUnmount: node => console.log('unmounted', node.element),
  },
});

API Reference

| Export | Description | | --- | --- | | mount(schema, container) | Mount a schema; returns { node, element, dispose } | | hydrate(schema, element) | Hydrate existing DOM; returns { node, element, dispose } | | ExoNodeDom | The DOM node implementation | | TExoDomMountResult (type) | Return type of mount/hydrate |

License

MIT


📖 Full documentation: exodra.org