js/page/skooma.html

370 lines
11 KiB
HTML

<link rel="stylesheet" href="style.css">
<script type="module" src="codeblock.js"></script>
<script type="module" src="filesize.js"></script>
<script type="module" src="scrollToTop.js"></script>
<script type="module" src="toc.js"></script>
<scroll-to-top>
</scroll-to-top>
<a class="back" href="..">Module Index</a>
<h1>Skooma.js</h1>
<code-block>import {html} from 'skooma.js'</code-block>
<section>
<h2>Table of Contents</h2>
<table-of-contents>
</table-of-contents>
</section>
<section>
<h2 id="introduction">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>
Skooma is only a <file-size file="../skooma.js">small</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.
<br>
This means you're writing plain JavaScript code that needs no additional
transpilation steps and runs directly in the browser.
</p>
</section>
<section>
<h2 id="showcase">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 =&gt; console.log(event)
})
</code-block>
<code-block>
let button document.createElement("button")
button.innerText = "Click Me!"
button.addEventListener(
"click", event =&gt; console.log(event)
)
return button
</code-block>
<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>
<p>Adding a <strong>shadow-root</strong> to the new element can be done with the magic <code>shadowRoot</code> property.</p>
<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>
<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>
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>
<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>
<p>Custom built-ins can be created with the <code>is</code> attribute.</p>
<p></p>
<code-block>
return html.span({is: "my-span"})
// Also sets the `is` attribute, useful for selectors
// like span[is="my-span"]
</code-block>
<code-block>
return document.createElement("span", {is: "my-span"})
// No actual `is` attribute. GL styling these.
</code-block>
</div>
</section>
<section>
<h2 id="svg">The <code>svg</code> helper</h2>
<p>
This works exactly the same as the <code>html</code> helper,
except that it creates elements with the appropriate namespace
and does <em>not</em> convert camelCase to kebab-case.
</p>
</section>
<section>
<h2 id="text">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 id="fragment">The <code>fragment</code> helper</h2>
<p>
This helper simply collects its arguments into a document fragment.
One may think of it as <code>Array.from</code>, except it collects HTML elements into a fragment instead.
</p>
<code-block>
const list = ["b", "i", "u"].map(name =&gt; html.[name](name))
return text`Some ${fragment(...list)} other text`
</code-block>
</section>
<section>
<h2 id="bind">The <code>bind</code> helper</h2>
<dl>
<code>
<dt>bind</dt>
<dd>transform-function &xrarr; update-function</dd>
</code>
</dl>
<p>
<dl>
<dt>Transform function</dt>
<code>
<dd>...data &xrarr; new-element</dd>
</code>
<dd>
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>
</dd>
<dt>Update function</dt>
<code>
<dd>...data &xrarr; new-element</dd>
</code>
<dd>
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.
</dd>
</dl>
</p>
<p>
A simple self-contained incrementing counter button could be implemented like this:
</p>
<code-block>
let update = bind(count =&gt; html.button(`Count: ${count}`, {click: event =&gt; update(count+1)}))
document.body.append(update(1))
</code-block>
<p>
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:
</p>
<code-block>
let update = bind(text)
counter.onIncrement(update)
return text`Current count: ${update(counter.count)}`
</code-block>
<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>
</section>
<section>
<h2 id="handle">The <code>handle</code> helper</h2>
<p>
This helper function takes an event handler and wraps it in a new
function that calls <code>preventDefault</code> on the event before
passing it to the original function.
</p>
<code-block>
html.form(html.button({
click: handle(event =&gt; console.log("I'm not submitting anything"))
}))
</code-block>
</section>
<section>
<h2 id="empty">The <code>empty</code> constant</h2>
<p>
This symbol will be completely ignored when it appears as a children in any skooma generator.
</p>
<code-block>
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
</code-block>
</section>