Compare commits
No commits in common. "4136eb65af6898f6f8a283213df451a463ac429f" and "6088b976d8dd783140d889b7210217bf54662dd0" have entirely different histories.
4136eb65af
...
6088b976d8
5 changed files with 147 additions and 267 deletions
|
@ -1,93 +0,0 @@
|
||||||
## Skooma.js
|
|
||||||
|
|
||||||
### HTML Proxy
|
|
||||||
|
|
||||||
The proxy object that does the actual HTML generation.
|
|
||||||
|
|
||||||
```js
|
|
||||||
document.body.append(html.div(
|
|
||||||
html.span("Hello, World!")
|
|
||||||
))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handle helper
|
|
||||||
|
|
||||||
Wraps a funcion of the signature `event, ... -> value` so that
|
|
||||||
`event.preventDefault` gets called before running the function.
|
|
||||||
|
|
||||||
```js
|
|
||||||
button.addEventListener("click",
|
|
||||||
handle(() => console.log("click"))
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fragment helper
|
|
||||||
|
|
||||||
Wraps a list of elements in a new document fragment.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const spans = fragment(
|
|
||||||
html.span("First span"),
|
|
||||||
html.span("Second span")
|
|
||||||
)
|
|
||||||
document.body.append(spans.cloneNode())
|
|
||||||
```
|
|
||||||
|
|
||||||
### Text helper
|
|
||||||
|
|
||||||
When called as a normal function, returns a new text node with the given
|
|
||||||
content. Unlike `document.createTextNode`, it does not fail for non-string
|
|
||||||
values.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const node = text("Hello, World!")
|
|
||||||
```
|
|
||||||
|
|
||||||
When used as a tagged template, returns a document fragment containing text
|
|
||||||
nodes and interpolated values. DOM nodes can be interpolated into the document
|
|
||||||
fragment.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const description = text`Interpolate ${html.b("bold")} text`
|
|
||||||
```
|
|
||||||
|
|
||||||
For consistency, even tagged templates with no interpolated variables will
|
|
||||||
always return a document fragment.
|
|
||||||
|
|
||||||
## State.js
|
|
||||||
|
|
||||||
### AbortRegistry
|
|
||||||
|
|
||||||
`FinalizationRegistry` that takes an `AbortController` and aborts it whenever
|
|
||||||
the registered value gets collected.
|
|
||||||
|
|
||||||
### ChangeEvent
|
|
||||||
|
|
||||||
The event class emitted when a change is detected on a skooma state.
|
|
||||||
Provides the `final` getter.
|
|
||||||
|
|
||||||
### MapStorage
|
|
||||||
|
|
||||||
A utility class that simulates the `Storage` API but is backed by a map. Can be
|
|
||||||
used as fallback in environments where persistent storages aren't available.
|
|
||||||
|
|
||||||
### SimpleState
|
|
||||||
|
|
||||||
Base state class that all other states inherit from, used primarily for class
|
|
||||||
checking, as the `State` class introduces behaviours that may be undesireable
|
|
||||||
when inheriting.
|
|
||||||
|
|
||||||
### State
|
|
||||||
|
|
||||||
The main state class that does all the magic.
|
|
||||||
|
|
||||||
### ForwardState
|
|
||||||
|
|
||||||
Proxy to a named property on another State to be used with APIs that only accept
|
|
||||||
single-value states.
|
|
||||||
|
|
||||||
### StoredState
|
|
||||||
|
|
||||||
State class that is backed by a Storage instead of an internal proxy.
|
|
||||||
|
|
||||||
## domLense.js
|
|
174
doc/render.md
174
doc/render.md
|
@ -1,174 +0,0 @@
|
||||||
# Rendering DOM nodes using `render.js`
|
|
||||||
|
|
||||||
```js
|
|
||||||
import {html} from "skooma/render.js"
|
|
||||||
```
|
|
||||||
|
|
||||||
A functional-friendly helper library for procedural DOM generation and templating, with support for reactive state objects.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.button(
|
|
||||||
"Clicki Me!",
|
|
||||||
{
|
|
||||||
class: "primary",
|
|
||||||
click({target}) {
|
|
||||||
console.log("User clicked", target)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
button => { console.log("Created", button) }
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
* elements as functions `content -> element`
|
|
||||||
* content as arguments or in arrays
|
|
||||||
* attributes in object arguments
|
|
||||||
* style, dataset, shadow root, etc. as magic attributes
|
|
||||||
* events as function attributes
|
|
||||||
* initialisers as `void -> void` functions
|
|
||||||
|
|
||||||
## Interface / Examples
|
|
||||||
|
|
||||||
### Basic DOM generation
|
|
||||||
|
|
||||||
Accessing the `html` proxy with any string key returns a new node generator function.
|
|
||||||
When called this function will generate a DOM node (HTML Tag).
|
|
||||||
The name of the function becomes the tag name of the node.
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.div()
|
|
||||||
```
|
|
||||||
|
|
||||||
Content and attributes can be set via the function arguments:
|
|
||||||
Strings and DOM nodes are inserted as children, while other objects (except for some special cases) have their key-value pairs turned into attribute-value pairs on the
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.div("Big Text", {style: "font-size: 1.4em"})
|
|
||||||
```
|
|
||||||
|
|
||||||
Arrays are iterated and their values treated as arguments.
|
|
||||||
This works both for inserting children and setting attributes.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const content = [" ps: hi", {class: "title"}]
|
|
||||||
html.h1({id: "main-heading"}, "Heading", content)
|
|
||||||
```
|
|
||||||
|
|
||||||
Function arguments are treated differently depending on their length:]
|
|
||||||
Functions with **no** named parameters are called, and their return value is then evaluated just like an argument to the generator.
|
|
||||||
|
|
||||||
All other functions are (immediately) called with the newly created node as their first and only argument.
|
|
||||||
These can be used to initialise the new node in a point-free style.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const hello = () => html.bold("Hello, World!")
|
|
||||||
const init = node => console.log("Initialising", node)
|
|
||||||
html.div(hello, init)
|
|
||||||
```
|
|
||||||
|
|
||||||
Nested tags can be generated with nested function calls.
|
|
||||||
When properly formatted, this means simpler templates will have the same structure as if written in HTML (sans those pesky closing tags).
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.div(
|
|
||||||
html.p(
|
|
||||||
html.b("Bold Text")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Attribute Processing
|
|
||||||
|
|
||||||
For convenience, arrays assigned as attributes will be joined with spaces:
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.a({class: ["button", "important"]})
|
|
||||||
```
|
|
||||||
|
|
||||||
Assigning a function as an attribute will instead attach it as an event listener:
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.button("Click me!", {click: event => {
|
|
||||||
alert("You clicked the button.")
|
|
||||||
}})
|
|
||||||
```
|
|
||||||
|
|
||||||
The special `style` property can be set to an object and its key/value pairs will be inserted as CSS properties on the element's `style` object.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const style = { color: "salmon" }
|
|
||||||
html.span("Salmon", { style })
|
|
||||||
```
|
|
||||||
|
|
||||||
The special property `shadowRoot` will attach a shadow-DOM to the element if none is present and append its content to the shadow root.
|
|
||||||
Arrays are iterated over and their elements appended individually.
|
|
||||||
|
|
||||||
```js
|
|
||||||
html.div({
|
|
||||||
shadowRoot = ["Hello, ", html.b("World"), "!"]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The `dataset` property will add its key/value pairs to the new node's `dataset`,
|
|
||||||
as a more convenient alternative to setting individual `data-` attributes.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const dataset = { foo: "bar" }
|
|
||||||
const div = html.div({dataset})
|
|
||||||
console.log(dataset.foo === div.dataset.foo)
|
|
||||||
console.log(div.getAttribute("data-foo") === "bar")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reactivity
|
|
||||||
|
|
||||||
Skooma supports reactivity through a simple protocol:
|
|
||||||
|
|
||||||
Observable objects identify themselves with the `observable` attribute,
|
|
||||||
which must return a truthy value.
|
|
||||||
|
|
||||||
Observables are expected to expose a `value` attribute that is both readable and writeable,
|
|
||||||
and to emit a "change" event whenever its vale has changed.
|
|
||||||
|
|
||||||
Observables can be passed to skooma's node functions as both
|
|
||||||
attribute values (values in an object) or
|
|
||||||
child elements (direct arguments or in an array).
|
|
||||||
|
|
||||||
#### Reactive Children
|
|
||||||
|
|
||||||
Passing an observable as a child element will attempt to insert its current
|
|
||||||
value into the new node as if it was passed in directly, but will also hook into
|
|
||||||
the observable to replace the value when the state changes.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const state = new Observable.value(0)
|
|
||||||
|
|
||||||
const button = html.button(state, {
|
|
||||||
click(event) { state.value++ }
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that to keep the replacement logic simple, it is not currently possible to
|
|
||||||
insert use document fragments, as these could insert several top-level children
|
|
||||||
into a component that would then all have to be replaced. When an observable
|
|
||||||
contains or changes to a document fragment, skooma will raise an error.
|
|
||||||
|
|
||||||
Before replacing an element, a `"replace"` event is emitted from the old
|
|
||||||
element. This event bubbles and is cancelable, and can thus be used both to
|
|
||||||
completely prevent the replacement according to custom logic, to alter or
|
|
||||||
initialise the new element before it is inserted, or even to modify the old
|
|
||||||
object instead of replacing it.
|
|
||||||
|
|
||||||
#### Reactive Attributes
|
|
||||||
|
|
||||||
Passing an observable as an object value will, likewise, treat its value as the
|
|
||||||
attribute value, and update it whenever the state's value changes.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const state = new Observable.value(0)
|
|
||||||
|
|
||||||
const input_1 = html.input({ type: "number", value: state })
|
|
||||||
const input_2 = html.input({ type: "number", value: state })
|
|
||||||
```
|
|
||||||
|
|
||||||
TODO: events as for reactive children
|
|
147
readme.md
147
readme.md
|
@ -1 +1,148 @@
|
||||||
# Skooma
|
# Skooma
|
||||||
|
|
||||||
|
A functional-friendly helper library for procedural DOM generation and
|
||||||
|
templating.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {html} from "skooma.js"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
```js
|
||||||
|
const text = new State({value: "Skooma is cool"})
|
||||||
|
setTimeout(() => {text.value = "Skooma is awesome!"}, 1e5)
|
||||||
|
|
||||||
|
document.body.append(html.div(
|
||||||
|
html.h1("Hello, World!"),
|
||||||
|
html.p(text, {class: "amazing"}),
|
||||||
|
html.button("Show Proof", {click: event => { alert("It's true!") }})
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interface / Examples
|
||||||
|
|
||||||
|
### Basic DOM generation
|
||||||
|
|
||||||
|
Accessing the `html` proxy with any string key returns a new node generator
|
||||||
|
function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
html.div("Hello, World!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Attributes can be set by passing objects to the generator:
|
||||||
|
|
||||||
|
```js
|
||||||
|
html.div("Big Text", {style: "font-size: 1.4em"})
|
||||||
|
```
|
||||||
|
|
||||||
|
Complex structures can easily achieved by nesting generator functions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
html.div(
|
||||||
|
html.p(
|
||||||
|
html.b("Bold Text")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For convenience, arrays assigned as attributes will be joined with spaces:
|
||||||
|
|
||||||
|
```js
|
||||||
|
html.a({class: ["button", "important"]})
|
||||||
|
```
|
||||||
|
|
||||||
|
Assigning a function as an attribute will instead attach it as an event
|
||||||
|
listener:
|
||||||
|
|
||||||
|
```js
|
||||||
|
html.button("Click me!", {click: event => {
|
||||||
|
alert("You clicked the button.")
|
||||||
|
}})
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- TODO: Document special keys -->
|
||||||
|
|
||||||
|
Generators can be called with many arguments. Arrays get iterated recursively as
|
||||||
|
if they were part of a flat argument list.
|
||||||
|
|
||||||
|
### Generating Text Nodes
|
||||||
|
|
||||||
|
```js
|
||||||
|
text("Hello, World")
|
||||||
|
// Wraps document.createTextNode
|
||||||
|
text()
|
||||||
|
// Defaults to empty string instead of erroring
|
||||||
|
text(null)
|
||||||
|
// Non-string arguments still error
|
||||||
|
|
||||||
|
text`Hello, World!`
|
||||||
|
// returns a new document fragment containing the text node "Hello, World!"
|
||||||
|
text`Hello, ${user}!`
|
||||||
|
// returns a document fragment containing 3 nodes:
|
||||||
|
// "Hello, ", the interpolated value of `user` and "!"
|
||||||
|
text`Hello, ${html.b(user)}!`
|
||||||
|
// Text node for Hello, the <b> tag with the user's name, and a text node for !
|
||||||
|
```
|
||||||
|
|
||||||
|
## handle
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {handle} from 'skooma.js'
|
||||||
|
```
|
||||||
|
|
||||||
|
Since it is common for event handlers to call `preventDefault()`, skooma
|
||||||
|
provides a helper function called `handle` with the following definition:
|
||||||
|
|
||||||
|
```js
|
||||||
|
fn => event => { event.preventDefault(); return fn(event) }
|
||||||
|
```
|
||||||
|
|
||||||
|
## A few more examples:
|
||||||
|
|
||||||
|
Create a Button that deletes itself:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.body.append(
|
||||||
|
html.button("Delete Me", {click: event => event.target.remove()})
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Turn a two-dimensional array into an HTML table:
|
||||||
|
```js
|
||||||
|
const table = rows =>
|
||||||
|
html.table(html.tbody(rows.map(
|
||||||
|
row => html.tr(row.map(
|
||||||
|
cell => html.rd(cell, {dataset: {
|
||||||
|
content: cell.toLowerCase(),
|
||||||
|
}})
|
||||||
|
))
|
||||||
|
)))
|
||||||
|
```
|
||||||
|
|
||||||
|
A list that you can add items to
|
||||||
|
```js
|
||||||
|
let list, input = ""
|
||||||
|
document.body.append(html.div([
|
||||||
|
list=html.ul(),
|
||||||
|
html.input({type: 'text', input: e => input = e.target.value}),
|
||||||
|
html.button({click: event => list.append(html.li(input))}, "Add"),
|
||||||
|
]))
|
||||||
|
```
|
||||||
|
|
||||||
|
A list that you can also delete items from
|
||||||
|
```js
|
||||||
|
const listItem = content => html.li(
|
||||||
|
html.span(content), " ", html.a("[remove]", {
|
||||||
|
click: event => event.target.closest("li").remove(),
|
||||||
|
style: { cursor: 'pointer', color: 'red' },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
let list, input = ""
|
||||||
|
document.body.append(html.div([
|
||||||
|
list=html.ul(),
|
||||||
|
html.input({type: 'text', input: e => input = e.target.value}),
|
||||||
|
html.button({click: event => list.append(listItem(input))}, "Add"),
|
||||||
|
]))
|
||||||
|
```
|
||||||
|
|
Loading…
Reference in a new issue