Elevator Pitch
Skooma combines easy and powerful HTML generation with optional reactivity.
All in plain JavaScript.
return html.p(
"Text Node",
html.br(),
html.b("HTML Node"),
{style:{color:"salmon"}},
)
Skooma is small enough that you can use it even if you only need it in a single function, easy enough to figure out what's going on even if you've never used it, powerful enough that you won't need another framework and flexible enough to let you use one regardless.
If that sounds good, it's time to back up those claims!
Continue reading to get an overview of skooma's features.
Or jump down to the explainer for the hows and whys.
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"
Calling any method on this object will generate an HTML node. Passing in arguments is how you add children, set attributes and even do more advanced things.
return html.div()
Using the component
helper from the sckooma/observable.js
module
makes it easier to see results on your page:
Just pass it a function that returns some DOM nodes, and it'll register a custom element for you.
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>
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. This works for strings, HTML elements, functions and even observable state containers.
return html.span(
"Skooma ",
html.u(
"is ",
html.b("cool")
)
)
Adding attributes is just as easy: Just pass an object into the function. If the value is an array, it'll get joined with spaces.
return html.span({
id: "warning",
class: ["danger", "bold"],
})
Event listeners work just like attribute, except the value is a function:
return html.button("Click Me!", {
click: event => {
alert("Button clicked :3")
}
})
Some "attributes" are handled in special ways to make things easier:
style
sets the inline styles of the node.
camelCase
names will be converted to kebab-case
and values
will be converted to strings.
return html.div(
{ style: {
color: "salmon"
}},
html.span("Big Salmon",
{ style: {
fontSize: "1.4em"
}}
)
)
dataset
converts its key-value pairs into data-
attributes.
return html.span(
{ dataset: {
from: "Fire",
to: "Heat",
snakeCase: "converted"
} },
"Heat from Fire"
)
shadowRoot
will attach a shadow DOM to the newly created node.
return html.div(
{ shadowRoot: [
html.p("Shadow DOM text"),
"Arrays work too :)"
]},
"Light DOM text"
)
It's just JavaScript
Skooma is entirely implemented as a light-weight JavaScript library.
What this means is that there's no new syntax for things you already know how to do.
Functions like filter
or map
can be applied directly to your data,
nodes can be assigned to variables right as you create them,
common structures can be extracted into function and even passed around as arguments,
and the syntax is just the boring old javascript that most tools already understand.
return html.ul(
["u", "b", "i"].map(
type => html.li(html[type](
`<${type}> element`
))
)
)
State changes in Skooma
import {ObservableObject} from 'skooma/observable.js'
Skooma offers a series of state management classes it calls "observables". Their purpose is to store one or more values and emit events when they are changed.
The render.js
library handles observables by setting DOM nodes up to
get updated when the observable emits a change event. Neihter module depends on the other
and they use a common protocol to work together.
const counter = new State({value: 0})
counter.valueChanged = newValue =>
console.log(`Value: ${newValue}`)
return html.buttonGroup(
html.button(
"Click Me! ",
html.span(counter),
{
click() {
counter.value += 1
}
}
),
html.button(
"Clear", {
click() {
counter.value = 0
}
}
)
)
Obligatory Todo list
This simple ToDo component uses nothing more than Skooma and the stylesheets already on this page to save some boilerplate.
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()
}
})
)
let todo, input
return todo = html.flexColumn(
{gap: 1},
input = html.input({
type: "text",
placeholder:
"Do some stuff",
}),
html.button("Add Task",
{
click() {
todo.append(task(
input.value ||
input.placeholder
))
}
})
)
Skooma Explained
A bit of boring history
The skooma/render.js
module traces its basic idea back to a Lua module.
The basic mechanism is the exact same, as the languages are very similar;
with the noteable difference that skooma.lua outputs text and uses a very
lightweight DOM-like data-structure as an intermediary representation.
Porting the library code to JavaScript was an obvious choice, given how similar both languages are in how they handle functions and metaprogramming. The concept worked well to generate HTML to send to the client, so it would work equally well to generate DOM nodes directly in the browser. And the obvious advantages over a server-side implementation is that things like event listeners could be attached directly.
One design goal while further expanding the capabilities of skooma has been to preserve this original use-case.
The library is small enough that pulling it into a project just to generate a few nested DOM structures
here and there isn't an unjustifiable waste of application size.
This is also why the render
module does not depend on the observable
module, and has as little code as it can get away with to enable interoperability.
The later addition of the Obervable
module, and the ability of skooma to detect state objects and
handle them specially to enable reactivity is what truly enables the library to be used on its own to build
simple and even moderately complex interactive components.
And with most of this extra complexity existing in a separate file,
the size impact on the core skooma library is very minimal, when
reactivity is not wanted, or an alternative mechanism for reactivity
is preferred.
Implementation
Skooma is based on a convenient wrapper around document.createElement
made even more convenient by a Proxy
to generate & populate DOM nodes.
Rationale & design principles
The design of skooma is based on the following assumption about web applications:
Most web application code can be modeled with simple abstractions. The few cases that can't tend to be too complex for any general-purpose abstraction.
Leading to a framework that focuses on the former group, while giving developers the tools to handle the tricky bits without having to hack themselves out of the framework.
One consideration that sets skooma apart from other alternatives, is the emphasis on being both easy to adopt, integrate, and even abandon.
While skooma provides some level of abstraction over native browser APIs, everything done via skooma is still easy to mentally map onto the underlying APIs. This reduces the mental load of translating what the code says, and what is happening to the website, creating a common mental model with vanilla javascript, as well as other non-magic libraries and micro-frameworks.
This, in practice, means that adding some code that uses skooma to an existing project, integrating it with other technologies, and even removing it in parts or in whole from a project that has outgrown it, or from performance-critical parts of an application that need even the last drop of performance squeezed out of the browser are all very simple to achieve.
Even taking out certain aspects like reactivity, while still using skooma to generate DOM nodes that get manipulated manually in response to complex state-changes is as easy as replacing the state objects with primitive values and writing the custom code to take their place.
Where other frameworks suck in their stomach and do their best to fit every use-case you could have, skooma is happy to just step aside and make space for more specialised tools.