Rework skooma.bind function

This commit is contained in:
Talia 2022-02-06 12:56:54 +01:00
parent 8697c168dc
commit 7c6e231d09
Signed by: darkwiiplayer
GPG key ID: 7808674088232B3E
3 changed files with 83 additions and 57 deletions

View file

@ -195,49 +195,66 @@
<section>
<h2>The <code>bind</code> helper</h2>
<dl>
<code>
<dt>bind</dt>
<dd>transform-function &xrarr; 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 &xrarr; 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 &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>
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 =&gt; counter.onIncrement(callback) || [counter.count])
let counterMessage = count =&gt; text`Current count: ${html.b(count)}`
return bindCount(counterMessage)
let update = bind(count =&gt; html.button(`Count: ${count}`, {click: event =&gt; 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 =&gt; 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>

View file

@ -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; }

View file

@ -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)