Elevator Pitch
With nyooom Nyooom you can elegantly express nested DOM structures in plain JavaScript.
At its core, it is a library to generate HTML elements.
Together with its utility modules like Observable
, it turns into a powerful front end micro framework.
return html.p(
"Text Node",
html.br(),
html.b("HTML Node"),
{style:{color:"salmon"}},
)
Nyooom aims to be small enough that you can use it even if you only need it in a single function, easy enough that you can figure out what's going on even if you've never used it before and powerful enough that you won't need another framework, without preventing you from using one if you prefer.
If that sounds good, it's time to back up those claims!
Continue reading to get an overview of how nyooom works and what it can do.
Or jump down to the explainer to get a more detailed explanation of what nyooom is good at and why.
Getting Started
Trying out nyooom is super easy!
Just import the html
export into your script and start generating DOM nodes.
import {html} from "https://cdn.jsdelivr.net/npm/nyooom@1.3.1/nyooom.min.js"
Indexing this object with any value will return a node factory function.
The type of node is decided by the property name: calling html.div()
will create a <div>
node.
return html.div()
If you quickly want to render your HTML to a website to see the result,
the component helper found in sckooma/observable.js
makes this a lot easier.
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/nyooom@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.
String arguments get inserted as text nodes, and DOM nodes are simply appended to the child list.
This way, your JS code can start looking almost like a proper templating language,
albeit with some extra quotes and braces here and there.
return html.span(
"Nyooom ",
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"],
})
If the value is a function, it'll be added as an event listener instead.
return html.button("Click Me!", {
click: event => {
alert("Button clicked :3")
}
})
Setting inline styles is just as easy:
return html.div(
{ style: {
color: "salmon"
}},
html.span("Big Salmon",
{ style: {
fontSize: "1.4em"
}}
)
)
camelCase
names will be converted to kebab-case
and values
will be converted to strings.
And the same goes for the dataset
property.
return html.span(
{ dataset: {
from: "Fire",
to: "Heat",
snakeCase: "converted"
} },
"Heat from Fire"
)
And setting shadowRoot
to a DOM node or an array of such will attach a shadow DOM to the newly created node.
return html.div(
{ shadowRoot:
html.p("Shadow DOM text")
},
"Light DOM text"
)
It's all just JavaScript
When generating HTML with nyooom, you never stop writing vanilla JavaScript that runs directly in your browser.
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 Nyooom
import {ObservableObject} from 'nyooom/observable.js'
nyooom.js
Nyooom considers object with a truthy observable
property to be Observables.
These objects are assumed to have a value
property representing their current
value, and to emit a "change"
event when their value changes.
Passing an observable to a generator function where an attribute would be expected will bind the attribute to the observable. The attribute value will be set to the initial value of the observable, and future changes of the observable will update the value of the attribute.
Passing an observable where a child element would be expected
will insert a reactive child element by inserting the current value
of the observable and replacing it whenever the value gets updated.
For primitives, this means they will first be converted to strings and then into a text node.
Special attributes are also supported. For example, setting an attribute to an observable with a function value, the function will be registered as an event listener. Changing the value of the observable to a different function will unregister the current listener and register the new one instead.
observable.js
This module exports several classes that store state and emit events whenever the state changes.
The most generic one of these would be ObservableObject
,
which in its values
property exposes a Proxy to a plain JavaScript object.
Changing any value on this proxy will emit an event on the associated ObservableObject
.
By default, changes on all Observables are enqueued and an event is dispatched in a microtask.
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 Nyooom 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
))
}
})
)
Nyooom Explained
TODO: Write