@llui/dom
@llui/dom
Runtime for the LLui web framework -- The Elm Architecture with compile-time bitmask optimization.
No virtual DOM. view() runs once at mount, building real DOM nodes with reactive bindings that update surgically when state changes.
Install
pnpm add @llui/dom
Quick Start
import { component, mountApp, div, button } from '@llui/dom'
type State = { count: number }
type Msg = { type: 'inc' } | { type: 'dec' }
const Counter = component<State, Msg, never>({
name: 'Counter',
init: () => [{ count: 0 }, []],
update: (state, msg) => {
switch (msg.type) {
case 'inc':
return [{ ...state, count: state.count + 1 }, []]
case 'dec':
return [{ ...state, count: state.count - 1 }, []]
}
},
view: ({ send, text }) => [
button({ onClick: () => send({ type: 'dec' }) }, [text('-')]),
text((s) => String(s.count)),
button({ onClick: () => send({ type: 'inc' }) }, [text('+')]),
],
})
mountApp(document.getElementById('app')!, Counter)
View<S, M> -- the helper bundle
view receives a single View<S, M> bag. Destructure what you need -- send plus any state-bound helpers. TypeScript infers S from the component definition, so no per-call generics:
view: ({ send, text, show, each, branch, memo }) => [
text(s => s.label), // s is State -- inferred
...show({ when: s => s.visible, render: () => [...] }),
...each({ items: s => s.items, key: i => i.id, render: ({ item }) => [...] }),
]
Element helpers (div, button, span, etc.) stay as imports -- they're stateless and don't need the S binding.
API
Core
| Export | Purpose |
|---|---|
component(def) |
Create a component definition |
mountApp(el, def) |
Mount a component to a DOM element |
hydrateApp(el, def) |
Hydrate server-rendered HTML |
flush() |
Synchronously flush all pending updates |
createView(send) |
Create a full View bundle (for tests/dynamic use) |
View Primitives
| Primitive | Purpose |
|---|---|
text(accessor) |
Reactive text node |
show({ when, render }) |
Conditional rendering |
branch({ on, cases }) |
Multi-case switching |
each({ items, key, render }) |
Keyed list rendering |
portal({ target, render }) |
Render into a different DOM location |
child({ def, key, props }) |
Full component boundary (Level 2 composition) |
memo(accessor) |
Memoized derived value |
selector(field) |
O(1) one-of-N selection binding |
onMount(callback) |
Lifecycle hook (runs once after mount) |
errorBoundary(opts) |
Catch render errors |
foreign({ create, update }) |
Integrate non-LLui libraries |
slice(h, selector) |
View over a sub-slice of state |
Composition
| Export | Purpose |
|---|---|
mergeHandlers(...handlers) |
Combine multiple update handlers |
sliceHandler({ get, set, narrow, sub }) |
Route messages to a state slice |
Context
| Export | Purpose |
|---|---|
createContext(defaultValue) |
Create a context |
provide(ctx, accessor, children) |
Provide value to subtree |
useContext(ctx) |
Read context value |
Element Helpers
50+ typed element constructors: div, span, button, input, a, h1-h6, table, tr, td, ul, li, img, form, label, select, textarea, canvas, video, nav, header, footer, section, article, p, pre, code, and more.
SSR
| Export | Purpose |
|---|---|
renderToString(def) |
Render component to HTML string |
initSsrDom() |
Initialize jsdom for SSR (from @llui/dom/ssr) |
Sub-path Exports
import { installDevTools } from '@llui/dom/devtools' // dev-only, tree-shaken
import { initSsrDom } from '@llui/dom/ssr' // server-only
import { replaceComponent } from '@llui/dom/hmr' // HMR support
Performance
Competitive with Solid and Svelte on js-framework-benchmark. 5.8 KB gzipped.
License
MIT