Rework skooma.bind function
This commit is contained in:
parent
8697c168dc
commit
7c6e231d09
3 changed files with 83 additions and 57 deletions
|
@ -195,49 +195,66 @@
|
|||
<section>
|
||||
<h2>The <code>bind</code> helper</h2>
|
||||
|
||||
<dl>
|
||||
<code>
|
||||
<dt>bind</dt>
|
||||
<dd>transform-function ⟶ update-function</dd>
|
||||
</code>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
<dl>
|
||||
<dt>Callback registration function</dt>
|
||||
<dd>
|
||||
A function that takes a callback as its single argument and returns
|
||||
an initial state as an array of elements. The inital state will be
|
||||
used to generate the bound element for the first time. The callback
|
||||
function should be called whenever an update in the UI is desired
|
||||
and the new state should be passed as its argument.
|
||||
</dd>
|
||||
<dt>Transform function</dt>
|
||||
<code>
|
||||
<dd>...data ⟶ new-element</dd>
|
||||
</code>
|
||||
<dd>
|
||||
A function that takes the initial or updated state and returns a
|
||||
new HTML element.
|
||||
If the function returns a non-truthy value, the element won't be
|
||||
replaced.
|
||||
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>
|
||||
Imagine <code>counter</code> to be an object with a <code>count</code>
|
||||
attribute representing the current count and <code>onIncrement</code> to
|
||||
be a function to register a callback to be called whenever the counter
|
||||
gets updated.
|
||||
A simple self-contained incrementing counter button could be implemented like this:
|
||||
</p>
|
||||
<code-block>
|
||||
// onIncrement doesn't return an initial state, so we have to wrap it:
|
||||
let bindCount = bind(callback => counter.onIncrement(callback) || [counter.count])
|
||||
|
||||
let counterMessage = count => text`Current count: ${html.b(count)}`
|
||||
|
||||
return bindCount(counterMessage)
|
||||
let update = bind(count => html.button(`Count: ${count}`, {click: event => update(count+1)}))
|
||||
document.body.append(update(1))
|
||||
</code-block>
|
||||
|
||||
<p>
|
||||
This can also be broken down to text nodes for more atomic updates.
|
||||
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>
|
||||
// onIncrement doesn't return an initial state, so we have to wrap it:
|
||||
let bindCount = bind(callback => counter.onIncrement(callback) || [counter.count])
|
||||
|
||||
return text`Current count: ${bindCount(text)}`
|
||||
let update = bind(text)
|
||||
counter.onIncrement(update)
|
||||
return text`Current count: ${update(counter.count)}`
|
||||
</code-block>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -85,10 +85,30 @@ span[title] {
|
|||
border-bottom: dotted currentcolor .16em;
|
||||
}
|
||||
|
||||
.all-unset {
|
||||
all: unset;
|
||||
code-block:not(:defined) {
|
||||
font-family: monospace;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
dl, dt, dd { all: unset; }
|
||||
dt, dd { display: block; }
|
||||
dl {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
dt {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
dl>*+dt { margin-top: .8em; }
|
||||
dl>dt+* { margin-top: .6em; }
|
||||
dl>*+* { margin-top: .4em; }
|
||||
|
||||
code { font-size: 1.1em; }
|
||||
|
||||
dl>code dt, dl>code dd { all: unset;}
|
||||
dl>code dt::after { content: ' : ';}
|
||||
|
||||
.columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
@ -96,17 +116,8 @@ span[title] {
|
|||
}
|
||||
.columns>* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.columns > * {
|
||||
flex: 100%;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
code-block:not(:defined) {
|
||||
font-family: monospace;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.all-unset { all: unset; }
|
||||
.full-width { grid-column: 1/-1; }
|
||||
|
|
30
skooma.js
30
skooma.js
|
@ -83,21 +83,19 @@ const nameSpacedProxy = (options={}) => new Proxy(Window, {
|
|||
has: (target, prop) => true,
|
||||
})
|
||||
|
||||
export const bind = register => transform => {
|
||||
export const bind = transform => {
|
||||
let element
|
||||
const addCurrent = current => Object.defineProperty(current, 'current', {get: () => element})
|
||||
element = transform(...register((...values) => {
|
||||
try {
|
||||
const next = transform(...values)
|
||||
if (next) {
|
||||
element.replaceWith(addCurrent(next))
|
||||
element = next
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const inject = next => Object.defineProperty(next, 'current', {get: () => element})
|
||||
const update = (...data) => {
|
||||
const next = transform(...data)
|
||||
if (next) {
|
||||
console.log(element)
|
||||
if (element) element.replaceWith(next)
|
||||
element = inject(next)
|
||||
return element
|
||||
}
|
||||
}))
|
||||
return addCurrent(element)
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
export const handle = fn => event => { event.preventDefault(); return fn(event) }
|
||||
|
@ -116,6 +114,6 @@ const textFromTemplate = (literals, items) => {
|
|||
}
|
||||
|
||||
export const text = (data="", ...items) =>
|
||||
typeof data == "string"
|
||||
? document.createTextNode(data)
|
||||
: textFromTemplate(data, items)
|
||||
typeof data == "object" && "at" in data
|
||||
? textFromTemplate(data, items)
|
||||
: document.createTextNode(data)
|
||||
|
|
Loading…
Reference in a new issue