UNPKG

@pfern/elements

Version:

A minimalist, pure functional declarative UI toolkit.

256 lines (171 loc) 5.86 kB
# Elements.js A minimalist declarative UI toolkit designed around purity, immutability, and HTML semantics. ## Features * Zero-dependency functional UI engine * Stateless components defined as pure functions * Fully declarative, deeply composable view trees * HTML element functions with JSDoc and TypeScript-friendly signatures * No hooks, no classes, no virtual DOM heuristics --- ## Why Elements.js? Modern frameworks introduced declarative UI—but buried it beneath lifecycle hooks, mutable state, and complex diffing algorithms. **Elements.js goes further:** * Pure functions represent both logic and view * The DOM *is* your state model * Re-rendering is *recursion*, not reconciliation > Can UI be defined as a tree of pure function calls—nothing more? Yes. Elements.js proves it. --- ## Philosophy ### Declarative from top to bottom * No internal component state * No lifecycle methods or effects * Every component is a function To update a view: just **call the function again** with new arguments. The DOM subtree is replaced in place. ### State lives in the DOM There is no observer graph, no `useState`, and no memory of previous renders. The DOM node *is the history*. Input state is passed as an argument. ### Minimal abstraction * No keys, refs, proxies, or context systems * No transpilation step * No reactive graph to debug Elements.js embraces the full truth of each function call as the only valid state. --- ## Example: Counter ```js import { div, pre, button, component, render } from './elements.js'; const counter = component((count = 0) => div( pre(count), button({ onclick: () => counter(count + 1) }, 'Increment') ) ) render(counter(), document.body); ``` * Each click returns a new call to `counter(count + 1)` * The old DOM node is replaced with the new one * No virtual DOM, no diffing --- ## Form Example: Todos App ```js import { button, div, component, form, input, li, span, ul } from './elements.js'; export const todos = component((items = []) => { const add = ({ todo: { value } }) => value && todos([...items, { value, done: false }]) const remove = item => todos(items.filter(i => i !== item)) const toggle = item => todos(items.map(i => i === item ? { ...i, done: !item.done } : i)) return div({ class: 'todos' }, form({ onsubmit: add }, input({ name: 'todo', placeholder: 'What needs doing?' }), button({ type: 'submit' }, 'Add')), ul(...items.map(item => li({ style: { 'text-decoration': item.done ? 'line-through' : 'none' } }, span({ onclick: () => toggle(item) }, item.value), button({ onclick: () => remove(item) }, '✕') )) ) ) }) ``` This is a complete MVC-style app: * Stateless * Immutable * Pure --- ## Root Rendering Shortcut If you use `html`, `head`, or `body` as the top-level tag, `render()` will automatically mount into the corresponding document element—no need to pass a container. ```js import { body, h1, h2, head, header, html, link, main, meta, render, section, title } from './elements.js' import { todos } from './components/todos.js' render( html( head( title('Elements.js'), meta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }), link({ rel: 'stylesheet', href: 'css/style.css' }) ), body( header(h1('Elements.js Demo')), main( section( h2('Todos'), todos() ) ) ) ) ) ``` --- ## Declarative Events All event listeners in Elements.js are pure functions. You can return a vnode from a listener to declaratively update the component tree—no mutation or imperative logic required. ### General Behavior * Any event handler (e.g. `onclick`, `onsubmit`, `oninput`) may return a new vnode to trigger a subtree replacement. * If the handler returns `undefined`, the event is treated as passive (no update occurs). * Returned vnodes are passed to `component()` to re-render declaratively. ### Form Events For `onsubmit`, `oninput`, and `onchange`, Elements.js provides a special signature: ```js (event.target.elements, event) ``` That is, your handler receives: 1. `elements`: the HTML form’s named inputs 2. `event`: the original DOM event object Elements.js will automatically call `event.preventDefault()` *only if* your handler returns a vnode. ```js form({ onsubmit: ({ todo: { value } }, e) => value && todos([...items, { value, done: false }]) }) ``` If the handler returns nothing, `preventDefault()` is skipped and the form submits natively. --- ## API ### `component(fn)` Wrap a recursive pure function that returns a vnode. ### `render(vnode[, container])` Render a vnode into the DOM. If `vnode[0]` is `html`, `head`, or `body`, no `container` is required. ### DOM Elements Every HTML and SVG tag is available as a function: ```js div({ id: 'box' }, 'hello') svg({ width: 100 }, circle({ r: 10 })) ``` ### TypeScript & JSDoc Each tag function (e.g. `div`, `button`, `svg`) includes a `@typedef` and MDN-sourced description to: * Provide editor hints * Encourage accessibility and semantic markup * Enable intelligent autocomplete --- ## Status * Production-ready core * 🧪 Fully tested (data-in/data-out behavior) * Under 2kB min+gzip * Node and browser compatible --- ## Installation ```bash npm install @pfern/elements ``` Or clone the repo and use as an ES module: ```js import { render, div, component, ... } from './elements.js'; ``` --- ## Summary Elements.js is a thought experiment turned practical: > Can UI be nothing but functions? Turns out, yes. * No diffing * No state hooks * No lifecycle * No reconciliation heuristics Just pure declarative HTML—rewritten in JavaScript. --- **Lightweight. Immutable. Composable.** Give it a try. You might never go back.