Skooma.js

import {html} from 'skooma.js'

Introduction & Scope

Skooma.js is a library for generating DOM nodes within JavaScript.

What are the benefits of Skooma?

Skooma is only a small ES6 module that uses meta-programming to turn JavaScript into a DSL that generates HTML and XML subtrees.
This means you're writing plain JavaScript code that needs no additional transpilation steps and runs directly in the browser.

Showcase

Here's a few examples of how things are done in Skooma.js and how it compares to vanilla JavaScript.

Skooma.js

Vanilla JavaScript

Generating a single empty HTML element. The html namespace creates generator functions dynamically.

Using the browser API, this is a bit more verbose, but still looks similar.

return html.h1() return document.createElement("h1")

String arguments to the generator function will be inserted as text nodes.

Without Skooma.js this would already require using a variable since createElement cannot insert text content into a new node.

return html.h1("Hello, World!") let h1 = document.createElement("h1") h1.innerText = "Hello, World!" return h1

DOM Nodes can also be passed into the generator function to add them as child-elements.

This would normally require two separate variables, one for each element.

return html.div(html.b("Hello!")) let div = document.createElement("div") let b = document.createElement("b") b.innerText = "Hello!" div.append(b) return div

When passing an object, its key/value pairs will be added as attributes to the new element.

Once again, in plain JS this requires a variable.

return html.div({attribute: "value"}) let div = document.createElement("div") div.setAttribute("attribute", "value") return div

When an object value is a function, it will instead be added as an event handler. The corresponding key will be used as the event name.

You guessed it: variable.

return html.button("Click Me!", { click: event => console.log(event) }) let button document.createElement("button") button.innerText = "Click Me!" button.addEventListener( "click", event => console.log(event) ) return button

The magic dataset attribute can be used to set values in the object's data-set

return html.div({ dataset: { name: "user" } }) let div = document.createElement("div") div.dataset.name = "user" return div

Adding a shadow-root to the new element can be done with the magic shadowRoot property.

return html.div({ shadowRoot: html.p("Shadow-DOM text content") }, "Light-DOM text content") let div = document.createElement("div") let p = document.createElement("p") p.innerText = "Shadow-DOM text content" div.attachShadow({mode: "open"}).append(p) div.innerText = "Light-DOM text content" return div

Object can be styled inline via the magic style property.

Meanwhile in Vanilla JS styling properties have to be added one by one

return html.div("Hello, World!" { class: 'button', style: { color, // some constant border: '1px solid currentcolor } }) let div = document.createElement("div") div.innerHTML = "Hello, World!" div.style.color: color // some constant div.style.border: '1px solid currentcolor' return div

Function arguments will be called on the new element.
This can be used to easily add custom initialisation logic to elements.

return html.p("Hello", console.log, ", world!") const element = document.createElement("p") element.innerText = "Hello" console.log(element) element.innerText += ", world!" return element

Custom elements with hyphenated names can be created easily

return html.myComponent() return document.createElement("my-component")

Custom built-ins can be created with the is attribute.

return html.span({is: "my-span"}) // Also sets the `is` attribute, useful for selectors // like span[is="my-span"] return document.createElement("span", {is: "my-span"}) // No actual `is` attribute. GL styling these.

The svg helper

This works exactly the same as the html helper, except that it creates elements with the appropriate namespace and does not convert camelCase to kebab-case.

The text helper

The text helper provides a convenient wrapper around the document.createTextNode function

In its simplest form, it's only a shorthand for its vanilla counterpart

return text("Hello, World!") return document.createTextNode("Hello, World!")

However, you don't need to pass an argument to it.

return text() return document.createTextNode("")

It also acts as a tag function for template literals, returning a document fragment containing a list of text nodes.

return text`Hello, ${name}!` let fragment = new DocumentFragment() fragment.append("Hello, ") fragment.append(name) fragment.append("!") return fragment

You can even interpolate actual DOM nodes in the string

return text`Hello, ${html.b(name)}!` let fragment = new DocumentFragment() fragment.append("Hello, ") let bold = document.createElement("b") bold.innerHTML = name fragment.append(bold) fragment.append("!") return fragment

The bind helper

bind
transform-function ⟶ update-function

Transform function
...data ⟶ new-element
A function that takes the current state and returns a new HTML element. If the function returns a non-truthy value, the element won't be replaced.
Note: the function must return a single element. Therefore one cannot use tagged template literals with text as this would return a document fragment which cannot be replaced.
Update function
...data ⟶ new-element
A function that passes its arguments to the transform function and returns its results while also taking care of replacing the old element with the new one and injecting the current attribute into it.

A simple self-contained incrementing counter button could be implemented like this:

let update = bind(count => html.button(`Count: ${count}`, {click: event => update(count+1)})) document.body.append(update(1))

The initial call of update sets the initial count of the button, and the attached event handler updates the button every time it is clicked, thereby replacing it with a new one.

For this next example, imagine a counter object that works like this:

The following code could be used to display the current count in the application:

let update = bind(text) counter.onIncrement(update) return text`Current count: ${update(counter.count)}`

When an element gets replaced with a newer version of itself, any variable containing the old element will become "stale". For this reason, the function injects a current property into every element it creates that will always point to the newest version of the element.

The empty constant

This symbol will be completely ignored when it appears as a children in any skooma generator.

const name = undefined html.div("name: ", name ?? "") // This will generate an (unnecessary) empty text note html.div("name: ", name ?? null) // This will print a warning to the console (same with undefined) html.div("name: ", name ?? empty) // This will only generate the first text node