A minimalist, pure functional declarative UI toolkit.
npm install @pfern/elementsElements.js is a tiny, functional UI toolkit for building DOM trees with plain
functions. Components are just functions; updates are just calling the function
again with new arguments.
``bash`
npm install @pfern/elements
`js
import { button, component, div, output } from '@pfern/elements'
export const counter = component((count = 0) =>
div(
output(count),
button(
{ onclick: () => counter(count + 1) },
'Increment')))
`
`js
import { button, component, div,
form, input, li, span, ul } from '@pfern/elements'
export const todos = component(
(items = [{ value: 'Add my first todo', done: true }]) => {
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) }, '✕'))))))})
`
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())))))
`
* Any event handler (e.g. onclick, onsubmit, oninput) may return a newundefined
vnode to trigger a subtree replacement.
* If the handler returns , the event is treated as passive (no update
occurs).
* Returned vnodes are applied at the closest component boundary.
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 inputsevent
2. : 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.
Wrap a recursive pure function that returns a vnode.
Render a vnode into the DOM. If vnode[0] is html, head, or body, nocontainer is required.
Every HTML and SVG tag is available as a function:
`js`
div({ id: 'box' }, 'hello')
svg({ width: 100 }, circle({ r: 10 }))
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
Elements are data-in, data-out only, so mocking and headless browsers like
jsdom are unnecessary out of the box. See the tests in this
repository for some examples.
- Elements.js is intended to be small and easy to reason about.
- For a starter app template, use @pfern/create-elements:npx @pfern/create-elements my-app
- https://github.com/pfernandez/create-elements
- examples/`.
- More examples live in