Skip to content

gbdrummer/jet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jet

A web-native, build-free reactive UI library.

  • No build step: write modern ESM and run it directly in the browser.
  • Vanilla JS: no compiler macros, no JSX transform.
  • Fine-grained DOM updates (no VDOM diff).
  • Signals + derived signals for reactive state.
  • Inline derivations (signal.derive(...)) and inline list mappings (arrayObserver.map(...)).
  • HTML tagged template literals via html.
  • Deterministic teardown for rendered entities (listeners cleaned up on destroy).

Import aliases can be handled with HTML import maps:

<script type="importmap">
{
  "imports": {
    "jet": "./src/index.js"
  }
}
</script>

Status / expectations

NOTE: This repository is best viewed as a research and reference implementation of Jet. It documents a number of architectural ideas and experiments that informed the project, but it is not a complete framework release and certain areas remain intentionally underdeveloped or in flux. These docs and code should be considered experimental and subject to change.

  • Jet is suitable for prototypes and internal tools today.
  • Expect breaking changes while the public API settles.

No components, no component lifecycle

Jet intentionally does not revolve around a “component” abstraction, and therefore does not provide a conventional component lifecycle API (mount, update, unmount, hooks, etc.).

Instead:

  • You build UI from renderable specs (html templates, element(...) specs, signal bindings, array mappings).
  • Rendering creates internal entities that own DOM ranges and subscriptions.
  • Teardown is driven by entity destruction (destroy) rather than component unmount hooks.

Practically, this means you generally don’t write code that depends on a component instance existing. You write:

  • plain functions that produce specs
  • signals/observers for state
  • event handlers (config.on) for DOM events and (where applicable) entity lifecycle events

Install / import

Jet is ESM-first.

import {
  render,
  element,
  html,
  fallback,
  include,
  tokens,
  observe,
  observeArray,
  observeObject,
  createFilter,
  compute
} from 'jet'

Quick example

import { render, element, html, observe, observeArray } from 'jet'

const count = observe(0)
const doubled = count.derive(n => n * 2)

const items = observeArray(['a', 'b'])

const incrementButton = element(
  'button',
  {
    on: {
      click: () => {
        count.value = count.value + 1
      }
    }
  },
  html`Count: ${count}`
)

const app = html`
  <main>
    ${incrementButton}

    <p>Doubled: ${doubled}</p>

    <ul>
      ${items.map((item) => html`<li>${item}</li>`)}
    </ul>
  </main>
`

render(document.body, {}, app)

Rendering primitives

html(strings, ...values)

html is a tagged template function that produces a template spec Jet knows how to render.

In templates, interpolations can be:

  • primitives (string, number, boolean, null, undefined)
  • other templates (html)
  • element specs from element(...)
  • signals / derived signals from observe(...) / .derive(...)
  • array mapping specs from observeArray(...).map(...)
  • promises (rendered via a fallback controller)

render(rootElement, config, content)

Renders into an existing DOM element (for example document.body).

  • rootElement is an actual DOM element (used as the root host).
  • content can be anything Jet can render (template, primitive, signal, mapping, etc.).

element(tag, config?, content?)

Creates an element spec.

config supports common buckets:

  • attrs: attributes
  • props: properties
  • style: style properties
  • on: event listeners / lifecycle events

For hyphen-separated attribute names, you can use nested object syntax. For example, data-theme="dark" can be expressed as:

import { element, html } from 'jet'

const panel = element('div', {
  attrs: {
    data: {
      theme: 'dark'
    }
  }
}, html`Hello`)

tokens(strings, ...values)

tokens is a tagged template helper for building space-separated token lists (commonly class strings). It:

  • supports nested token lists
  • supports arrays
  • supports signals and derived signals inside the template (it unwraps them and subscribes)

It’s designed to be used in attrs, props, or style through Jet’s IDL managers.

import { element, html, tokens, observe } from 'jet'

const primary = observe(true)
const variant = primary.derive(v => v ? 'btn-primary' : 'btn-secondary')

const button = element('button', {
  attrs: {
    class: tokens`btn ${variant}`
  }
}, html`Click me`)

fallback(fallbackContent, promise, config?)

Creates a “fallback controller” spec.

  • Jet renders fallbackContent immediately.
  • Once the promise resolves, Jet replaces that region with the resolved content.

include(path, config?)

Loads an external resource and returns a promise that resolves to content.

  • .js is loaded via dynamic import().
  • everything else is loaded via fetch() and rendered as an html template.
  • results are cached by path.

Fine-grained reactivity

observe(initialValue, config?)

Creates a signal.

  • Read the current value: signal.value
  • Write a new value: signal.value = next
  • Listen for changes: signal.on('change', handler)

Derived signals: signal.derive(transform)

Derivations are inline and local.

const count = observe(1)
const label = count.derive(n => `Count = ${n}`)

Garbage-collected derived signals

Jet tracks derived signals using WeakRef + FinalizationRegistry so that derived signals can be garbage-collected if you drop all strong references to them.

In other words: creating derived signals does not inherently require you to manually unregister them.

More precisely:

  • Signals are normal JS objects: if nothing references them anymore, the runtime can collect them.
  • Derived signals are registered against their parent using weak references, so the parent signal does not keep derived signals alive.
  • If a derived signal is still referenced (or still subscribed-to by some live rendering), it will not be collected.

Reactive collections

observeArray(initialArray, config?)

observeArray provides a proxy-like array value that captures mutations and emits change operations.

  • Mutating methods like push, splice, unshift, etc. are wrapped.
  • Changes are queued and flushed on a microtask.

Inline mappings: arrayObserver.map(transform, config?)

observeArray(...).map(...) returns an array mapping spec. When interpolated into a template, Jet renders a region that updates in response to array operations.

Internally, Jet coalesces array DOM updates and schedules flushes with requestAnimationFrame for smoothness under rapid updates.

Additional useful bits:

  • clone({ sync: true }) can create a follower array observer that applies operations from the source.
  • Filtering hooks exist and can drive a filter.change event during mapping.

observeObject(initialObject, config?)

observeObject wraps a plain object in a proxy that emits operations for:

  • setting properties
  • deleting properties
  • replacing the entire object

It also includes ergonomic helpers:

  • obj.set(key, value)
  • obj.assign(partial)
  • obj.update(fn)
  • obj.replace(next)

Editor ergonomics (VS Code)

Jet templates are authored as:

html`<div class="panel">${/* ... */''}</div>`

For syntax highlighting and basic IntelliSense inside html\...``, install a lit-html compatible extension.

  • Recommended: the VS Code extension “lit-plugin”
  • Alternative: an extension that provides lit-html highlighting

Most of these extensions recognize the tag name html by default, or allow configuration to treat html tagged templates as HTML.

Notes / gotchas

  • Don’t call render() inside a template. Jet treats render() as a root-only operation.
  • Signals and rendered entities are event targets; teardown is designed to be deterministic (listeners removed after destroy).

License

MIT

About

A web-native, build-free reactive UI library.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors