2022-01-01 12:05:43 +00:00
< link rel = "stylesheet" href = "style.css" >
2022-01-04 12:02:48 +00:00
< script type = "module" src = "codeblock.js" > < / script >
2022-01-04 13:21:38 +00:00
< script type = "module" src = "filesize.js" > < / script >
2022-01-04 12:02:48 +00:00
2022-01-01 12:05:43 +00:00
< h1 > Skooma.js< / h1 >
2022-01-04 12:02:48 +00:00
< code-block > import {html} from 'skooma.js'< / code-block >
2022-01-01 12:05:43 +00:00
< section >
< h2 > Introduction & Scope< / h2 >
< p >
Skooma.js is a library for generating DOM nodes within JavaScript.
< / p >
< h3 > What are the benefits of Skooma?< / h3 >
< p >
2022-01-04 13:21:38 +00:00
Skooma is only a small < file-size file = "../skooma.js" > < / file-size > ES6
module that uses meta-programming to turn JavaScript into a
< span title = "Domain-Specific Language" > DSL< / span > that generates HTML
and XML subtrees.
2022-01-01 12:05:43 +00:00
< br >
This means you're writing plain JavaScript code that needs no additional
transpilation steps and runs directly in the browser.
< / p >
< / section >
2022-01-04 12:02:48 +00:00
< section >
< h2 > Showcase< / h2 >
< p > Here's a few examples of how things are done in Skooma.js and how it compares to vanilla JavaScript.< / p >
< div class = "columns" >
< h3 > Skooma.js< / h3 >
< h3 > Vanilla JavaScript< / h3 >
< p > Generating a single empty HTML element. The < code > html< / code > namespace creates generator functions dynamically.< / p >
< p > Using the browser API, this is a bit more verbose, but still looks similar.< / p >
< code-block >
return html.h1()
< / code-block >
< code-block >
return document.createElement("h1")
< / code-block >
< p > String arguments to the generator function will be inserted as < strong > text nodes< / strong > .< / p >
< p > Without Skooma.js this would already require using a variable since < code > createElement< / code > cannot insert text content into a new node.< / p >
< code-block >
return html.h1("Hello, World!")
< / code-block >
< code-block >
let h1 = document.createElement("h1")
h1.innerText = "Hello, World!"
return h1
< / code-block >
< p > DOM Nodes can also be passed into the generator function to add them as < strong > child-elements.< / strong > < / p >
< p > This would normally require two separate variables, one for each element.< / p >
< code-block >
return html.div(html.b("Hello!"))
< / code-block >
< code-block >
let div = document.createElement("div")
let b = document.createElement("b")
b.innerText = "Hello!"
div.append(b)
return div
< / code-block >
< p > When passing an object, its key/value pairs will be added as < strong > attributes< / strong > to the new element.< / p >
< p > Once again, in plain JS this requires a variable.< / p >
< code-block >
return html.div({attribute: "value"})
< / code-block >
< code-block >
let div = document.createElement("div")
div.setAttribute("attribute", "value")
return div
< / code-block >
< p > When an object value is a function, it will instead be added as an < strong > event handler< / strong > . The corresponding key will be used as the event name.< / p >
< p > You guessed it: variable.< / p >
< code-block >
return html.button("Click Me!", {
click: event => console.log(event)
})
< / code-block >
< code-block >
let button document.createElement("button")
button.innerText = "Click Me!"
button.addEventListener(
"click", event => console.log(event)
)
return button
< / code-block >
2022-04-05 22:40:15 +00:00
< p > The magic < code > dataset< / code > attribute can be used to set values in the object's data-set< / p >
< p > < / p >
< code-block >
return html.div({ dataset: { name: "user" } })
< / code-block >
< code-block >
let div = document.createElement("div")
div.dataset.name = "user"
return div
< / code-block >
2022-01-13 21:02:20 +00:00
< p > Adding a < strong > shadow-root< / strong > to the new element can be done with the magic < code > shadowRoot< / code > property.< / p >
2022-01-04 12:02:48 +00:00
< p > < / p >
< code-block >
return html.div({
shadowRoot: html.p("Shadow-DOM text content")
}, "Light-DOM text content")
< / code-block >
< code-block >
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
< / code-block >
2022-01-13 21:02:20 +00:00
< p > Object can be < strong > styled< / strong > inline via the magic < code > style< / code > property.< / p >
< p > Meanwhile in Vanilla JS styling properties have to be added one by one< / p >
< code-block >
return html.div("Hello, World!" {
class: 'button', style: {
color, // some constant
border: '1px solid currentcolor
}
})
< / code-block >
< code-block >
let div = document.createElement("div")
div.innerHTML = "Hello, World!"
div.style.color: color // some constant
div.style.border: '1px solid currentcolor'
return div
< / code-block >
< p > Custom elements with hyphenated names can be created easily< / p >
< p > < / p >
< code-block >
return html.myComponent()
< / code-block >
< code-block >
return document.createElement("my-component")
< / code-block >
2022-04-05 22:36:21 +00:00
< p >
Function arguments will be called on the new element.< br >
This can be used to easily add custom initialisation logic to elements.
< / p >
< p > < / p >
< code-block >
return html.p("Hello", console.log, ", world!")
< / code-block >
< code-block >
const element = document.createElement("p")
element.innerText = "Hello"
console.log(element)
element.innerText += ", world!"
return element
< / code-block >
2022-01-13 21:02:20 +00:00
< / div >
< / section >
< section >
< h2 > The < code > text< / code > helper< / h2 >
< div class = "columns" >
< p > The < code > text< / code > helper provides a convenient wrapper around the
< code > document.createTextNode< / code > function< / p >
< p > In its simplest form, it's only a shorthand for its vanilla counterpart< / p >
< code-block >
return text("Hello, World!")
< / code-block >
< code-block >
return document.createTextNode("Hello, World!")
< / code-block >
< p > However, you don't need to pass an argument to it.< / p >
< p > < / p >
< code-block >
return text()
< / code-block >
< code-block >
return document.createTextNode("")
< / code-block >
< p > It also acts as a tag function for template literals, returning a
document fragment containing a list of text nodes.< / p >
< p > < / p >
< code-block >
return text`Hello, ${name}!`
< / code-block >
< code-block >
let fragment = new DocumentFragment()
fragment.append("Hello, ")
fragment.append(name)
fragment.append("!")
return fragment
< / code-block >
< p > You can even interpolate actual DOM nodes in the string< / p >
< p > < / p >
< code-block >
return text`Hello, ${html.b(name)}!`
< / code-block >
< code-block >
let fragment = new DocumentFragment()
fragment.append("Hello, ")
let bold = document.createElement("b")
bold.innerHTML = name
fragment.append(bold)
fragment.append("!")
return fragment
< / code-block >
< / div >
< / section >
< section >
< h2 > The < code > bind< / code > helper< / h2 >
2022-02-06 11:56:54 +00:00
< dl >
< code >
< dt > bind< / dt >
< dd > transform-function ⟶ update-function< / dd >
< / code >
< / dl >
2022-01-13 21:02:20 +00:00
< p >
< dl >
2022-02-06 11:56:54 +00:00
< dt > Transform function< / dt >
< code >
< dd > ...data ⟶ new-element< / dd >
< / code >
2022-01-13 21:02:20 +00:00
< dd >
2022-02-06 11:56:54 +00:00
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.
< div >
< strong > Note:< / strong > the function must return a single < em > element< / em > .
Therefore one cannot use tagged template literals with < code > text< / code >
as this would return a document fragment which cannot be replaced.
< / div >
2022-01-13 21:02:20 +00:00
< / dd >
2022-02-06 11:56:54 +00:00
< dt > Update function< / dt >
< code >
< dd > ...data ⟶ new-element< / dd >
< / code >
2022-01-13 21:02:20 +00:00
< dd >
2022-02-06 11:56:54 +00:00
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 < code > current< / code >
attribute into it.
2022-01-13 21:02:20 +00:00
< / dd >
< / dl >
< / p >
< p >
2022-02-06 11:56:54 +00:00
A simple self-contained incrementing counter button could be implemented like this:
2022-01-13 21:02:20 +00:00
< / p >
2022-01-14 10:57:21 +00:00
< code-block >
2022-02-06 11:56:54 +00:00
let update = bind(count => html.button(`Count: ${count}`, {click: event => update(count+1)}))
document.body.append(update(1))
2022-01-14 10:57:21 +00:00
< / code-block >
< p >
2022-02-06 11:56:54 +00:00
The initial call of < code > update< / code > 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.
< / p >
< p >
For this next example, imagine a < code > counter< / code > object that works like this:
< ul >
< li > < code > counter.count< / code > returns the current count
< li > < code > counter.onUpdate< / code > lets the user register a callback that will be called with the new count whenever the counter updates
< li > The counter will be updated periodically by some other part of the application
< / ul >
The following code could be used to display the current count in the application:
2022-01-14 10:57:21 +00:00
< / p >
< code-block >
2022-02-06 11:56:54 +00:00
let update = bind(text)
counter.onIncrement(update)
return text`Current count: ${update(counter.count)}`
2022-01-14 10:57:21 +00:00
< / code-block >
2022-01-13 21:02:20 +00:00
< p >
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 < code > current< / code > property into every element it
creates that will always point to the newest version of the element.
< / p >
2022-01-04 12:02:48 +00:00
< / section >