Skooma

import { html } from "skooma.js"

A new way of building HTML, components and applications in vanilla JavaScript

Get Started

Elevator Pitch

Skooma lets you elegantly express nested DOM structures in plain JavaScript.
It makes your code more declarative without re-inventing language features like loops and conditions.

return html.p( "Text Node", html.br(), html.b("HTML Node") )

Skooma aims to be what document.createElement should have been. It creates a node, sets classes and attributes, inserts content and attaches event listeners.

Some key characteristics:

Getting Started

Trying out skooma is super easy!
Just import the html export into your script and start generating DOM nodes.

import {html} from "https://cdn.jsdelivr.net/npm/skooma@1.3.1/skooma.min.js"

To create elements, skooma exposes the html proxy. Accessing this proxy with any value will return a node factory function.
In its most basic form, these functions will simply generate a new DOM node of the respective type.

return html.div()

If you quickly want to render your HTML to a website to see the result, the component helper found in sckooma/state.js makes this a lot easier.
This wrapper registers a new custom element to insert the results of a function in your document.

import {component} from "https://cdn.jsdelivr.net/npm/skooma@1.3.1/state.min.js"
const myComponent = () => html.div("Rendered Component")
component(myComponent) //Registers it as <my-component>

This wrapper also provides some state-management features that will be described in a separate section.

Of course you can also just call document.body.append(html.span("My Text")) in your script.

Basic DOM generation

Content can be added to a node by simply passing it as arguments to the function. String arguments get inserted as text nodes, and DOM nodes are simply appended to the child list.
This results in an easy way to compose nested DOM structures.

return html.span( "Testing ", html.u("stuff") )

Attributes are added by passing an object to the function, where each key-value pair gets turned into an attribute on the generated node.

return html.span({ id: "warning", class: ["danger", "bold"], // Arrays get joined with spaces })

When trying to assign a function as the value of an attribute, skooma will instead register that function as an event handler with the attribute name being used the event name instead.

return html.button( "Click Me!", { click: event => { alert("Button clicked :3") } } )

Leveraging JS features

When generating HTML with skooma, you never stop writing JavaScript. This means that all JavaScript features are available anywhere inside your code. Functions like filter or map can be applied directly to your data, nodes can be assigned to variables right as you create them, and the syntax is always 100% pure javascript as most browsers and dev tools understand it.

return html.ul( ["u", "b", "i"].map( type => html.li(html[type]( `<${type}> element` )) ) )

A more complex example that assembles an array of objects into a table:

const table = (...rows) => { const keys = new Set() for (const row of rows) { Object.keys(row).forEach( key => keys.add(key) ) } const header = Array.from(keys) return html.table( {class: "full-width"}, html.thead( html.tr(header.map( key => html.th(key) )) ), html.tbody ( rows.map(row => html.tr( header.map( key => html.td( row[key] ?? empty ) ) )) ) ) } return table( {name: "Alice", hobby: "fishing"}, {name: "Bob", hobby: "cooking", age: 22} )

Managing state in Skooma

import {State} from 'skooma/state.js'

Reactivity and state management in skooma are split between the core module and the state module.

Generator Functions

The core skooma.js module understands a state object to be anything that is

  1. Not a DOM node
  2. Has a value property
  3. Has an addEventListener method

When such an object is passed into a generator function as either a child element or an attribute, skooma will use the value of the state object and register a "change" event that replaces the attribute or child element with the current value.

One special case worth mentioning is when an attribute is assigned a state object with a function as its value. In these cases, the function gets registered as an event handler just like if it was assigned directly, but updating the state will remove the event handler and register a new event handler with the current value.

The state module

This module primarily exposts the State class, which extends the EventTarget class.

Every state object exposes a proxy object via the proxy property. This proxy can be used like any normal object, but setting any property on it will dispatch a "change" event on the corresponding state object. The proxy object itself also has a getter and setter for the value property, which gets forwarded directly to proxy.value for easier access.

This means that any State object satisfies the API of a state object from the previous section, meaning they can be used to build reactive components.

const counter = new State({value: 0}) counter.valueChanged = newValue => console.log(`Value: ${newValue}`) return html.flexColumn( {gap: 1}, html.button( "Click Me! ", html.span(counter), { click: () => { counter.value += 1 }} ), html.button( "Clear", { click: () => { counter.value = 0 }} ) )

The basic State object is backed by a plain JS object, so their attributes are unique and do not persist page reload.
By contrast, the StoredState class, which extends the core State class, is backed by a Storage object like window.localStorage or window.sessionStorage, meaning that they persist page reloads.
Additionally, they detect changes both from the current as well as from other browser tabs/windows, so any updates of the state get propagated automatically to all states backed by the same storage.

A simple Todo list

A simple, but not completely bare-bones todo list application, using nothing more than Skooma and the CSS already present on this page to save some code.

let todo, input const task = value => html.flexRow ( {class: ["todo"], gap: 1}, value, html.span("[x]", { style: { color: "var(--primary-6)", cursor: "pointer" }, click: event => { event .target .closest(".todo") .remove() } }) ) return todo = html.flexColumn( {gap: 1}, input=html.input({ type: "text", placeholder: "Do some stuff", }), html.button("Add Task", { click: event => { todo.append(task( input.value || input.placeholder )) } }) )