<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> <scroll-to-top> </scroll-to-top> <h1>Skooma.js</h1> <code-block>import {html} from 'skooma.js'</code-block> <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> 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. <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>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> <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>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>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> <dl> <code> <dt>bind</dt> <dd>transform-function ⟶ update-function</dd> </code> </dl> <p> <dl> <dt>Transform function</dt> <code> <dd>...data ⟶ 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 ⟶ 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 => html.button(`Count: ${count}`, {click: event => 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>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 => console.log("I'm not submitting anything")) })) </code-block> </section> <section> <h2>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>