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:
- No build steps required
- Plays nice with web components
- Just vanilla JavaScript
- Code mirrors output in structure
- Single Responsibility: Generating HTML
- No closing tags
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
- Not a DOM node
- Has a
value
property - 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
))
}
})
)