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>
|
<section>
|
||||||
<h2>The <code>bind</code> helper</h2>
|
<h2>The <code>bind</code> helper</h2>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<code>
|
||||||
|
<dt>bind</dt>
|
||||||
|
<dd>transform-function ⟶ update-function</dd>
|
||||||
|
</code>
|
||||||
|
</dl>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<dl>
|
<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>
|
<dt>Transform function</dt>
|
||||||
|
<code>
|
||||||
|
<dd>...data ⟶ new-element</dd>
|
||||||
|
</code>
|
||||||
<dd>
|
<dd>
|
||||||
A function that takes the initial or updated state and returns a
|
A function that takes the current state and returns a new HTML element.
|
||||||
new HTML element.
|
If the function returns a non-truthy value, the element won't be replaced.
|
||||||
If the function returns a non-truthy value, the element won't be
|
<div>
|
||||||
replaced.
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Imagine <code>counter</code> to be an object with a <code>count</code>
|
A simple self-contained incrementing counter button could be implemented like this:
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
<code-block>
|
<code-block>
|
||||||
// onIncrement doesn't return an initial state, so we have to wrap it:
|
let update = bind(count => html.button(`Count: ${count}`, {click: event => update(count+1)}))
|
||||||
let bindCount = bind(callback => counter.onIncrement(callback) || [counter.count])
|
document.body.append(update(1))
|
||||||
|
|
||||||
let counterMessage = count => text`Current count: ${html.b(count)}`
|
|
||||||
|
|
||||||
return bindCount(counterMessage)
|
|
||||||
</code-block>
|
</code-block>
|
||||||
|
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<code-block>
|
<code-block>
|
||||||
// onIncrement doesn't return an initial state, so we have to wrap it:
|
let update = bind(text)
|
||||||
let bindCount = bind(callback => counter.onIncrement(callback) || [counter.count])
|
counter.onIncrement(update)
|
||||||
|
return text`Current count: ${update(counter.count)}`
|
||||||
return text`Current count: ${bindCount(text)}`
|
|
||||||
</code-block>
|
</code-block>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -85,10 +85,30 @@ span[title] {
|
||||||
border-bottom: dotted currentcolor .16em;
|
border-bottom: dotted currentcolor .16em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-unset {
|
code-block:not(:defined) {
|
||||||
all: unset;
|
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 {
|
.columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
@ -96,17 +116,8 @@ span[title] {
|
||||||
}
|
}
|
||||||
.columns>* {
|
.columns>* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.columns > * {
|
|
||||||
flex: 100%;
|
flex: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width {
|
.all-unset { all: unset; }
|
||||||
grid-column: 1/-1;
|
.full-width { grid-column: 1/-1; }
|
||||||
}
|
|
||||||
|
|
||||||
code-block:not(:defined) {
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
30
skooma.js
30
skooma.js
|
@ -83,21 +83,19 @@ const nameSpacedProxy = (options={}) => new Proxy(Window, {
|
||||||
has: (target, prop) => true,
|
has: (target, prop) => true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const bind = register => transform => {
|
export const bind = transform => {
|
||||||
let element
|
let element
|
||||||
const addCurrent = current => Object.defineProperty(current, 'current', {get: () => element})
|
const inject = next => Object.defineProperty(next, 'current', {get: () => element})
|
||||||
element = transform(...register((...values) => {
|
const update = (...data) => {
|
||||||
try {
|
const next = transform(...data)
|
||||||
const next = transform(...values)
|
if (next) {
|
||||||
if (next) {
|
console.log(element)
|
||||||
element.replaceWith(addCurrent(next))
|
if (element) element.replaceWith(next)
|
||||||
element = next
|
element = inject(next)
|
||||||
}
|
return element
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
return addCurrent(element)
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handle = fn => event => { event.preventDefault(); return fn(event) }
|
export const handle = fn => event => { event.preventDefault(); return fn(event) }
|
||||||
|
@ -116,6 +114,6 @@ const textFromTemplate = (literals, items) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const text = (data="", ...items) =>
|
export const text = (data="", ...items) =>
|
||||||
typeof data == "string"
|
typeof data == "object" && "at" in data
|
||||||
? document.createTextNode(data)
|
? textFromTemplate(data, items)
|
||||||
: textFromTemplate(data, items)
|
: document.createTextNode(data)
|
||||||
|
|
Loading…
Reference in a new issue