Add State library and deprecate speaker & listener
This commit is contained in:
parent
89d4084462
commit
287aa98955
6 changed files with 171 additions and 31 deletions
34
index.html
34
index.html
|
@ -66,33 +66,19 @@
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Listener</h2>
|
||||
<h2>State</h2>
|
||||
<p>
|
||||
Like a normal object, except you can register callbacks for property changes.
|
||||
<a class="fancy" href="page/listener.html">Read More</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<h3 class="all-unset"><b>Code Sample</b>:</h3>
|
||||
<code-block>
|
||||
import listener from 'listener.js'
|
||||
|
||||
const user = listener({name: "John Doe"})
|
||||
user.listen('name', value => console.warn(`User name has changed to ${value}`))
|
||||
</code-block>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Speaker</h2>
|
||||
<p>
|
||||
Publish and subscribe to messages, relayed via micro-tasks.
|
||||
<a class="fancy" href="page/speaker.html">Read More</a>
|
||||
Utility class to monitor state changes on an object using a Proxy and events.
|
||||
Changes are batched and processed in a microtask.
|
||||
<a class="fancy" href="page/state.html">Read More</a>
|
||||
|
||||
<code-block>
|
||||
const speaker = new Speaker()
|
||||
speaker.listen((...args) => console.log(...args))
|
||||
speaker.speak("First", "second", "third")
|
||||
const state = new State()
|
||||
speaker.meepChanged = value => console.log(`meep ${value}`)
|
||||
state.proxy.meep = "Heat from fire"
|
||||
state.proxy.meep = "Fire from heat"
|
||||
state.proxy.meep = "moop"
|
||||
// outputs: "meep moop"
|
||||
</code-block>
|
||||
|
||||
No, this has nothing to do with playing audio.
|
||||
|
|
|
@ -9,6 +9,7 @@ Example:
|
|||
*/
|
||||
|
||||
const registry = new WeakMap()
|
||||
|
||||
const listener = (target={}) => {
|
||||
const callbacks = new Map()
|
||||
const methods = Object.create(null)
|
||||
|
@ -16,7 +17,7 @@ const listener = (target={}) => {
|
|||
const callback = once
|
||||
? (...args) => { this.forget(name, callback); return fn(...args) }
|
||||
: fn
|
||||
let set = callbacks.get(name) ?? new Set()
|
||||
const set = callbacks.get(name) ?? new Set()
|
||||
callbacks.set(name, set)
|
||||
set.add(callback)
|
||||
return this
|
||||
|
@ -29,13 +30,13 @@ const listener = (target={}) => {
|
|||
return callbacks.delete(name)
|
||||
}
|
||||
}
|
||||
let proxy = new Proxy(target, {
|
||||
const proxy = new Proxy(target, {
|
||||
set: (target, prop, value) => {
|
||||
if (callbacks.has(null)) callbacks.get(null).forEach(callback => callback(value, target[prop], prop))
|
||||
if (callbacks.has(prop)) callbacks.get(prop).forEach(callback => callback(value, target[prop], prop))
|
||||
return Reflect.set(target, prop, value)
|
||||
},
|
||||
get: (target, prop, value) => prop in methods
|
||||
get: (target, prop, _value) => prop in methods
|
||||
? methods[prop]
|
||||
: target[prop]
|
||||
})
|
||||
|
|
91
page/state.html
Normal file
91
page/state.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<script type="module" src="codeblock.js"></script>
|
||||
<script type="module" src="scrollToTop.js"></script>
|
||||
|
||||
<scroll-to-top>
|
||||
</scroll-to-top>
|
||||
|
||||
<a class="back" href="..">Module Index</a>
|
||||
|
||||
<h1 class="js module">listener.js</h1>
|
||||
|
||||
<code-block>import {State} from 'state.js'</code-block>
|
||||
|
||||
<section>
|
||||
<h2>Description</h2>
|
||||
<p>
|
||||
State objects emit an event whenever a property gets written to on their associated Proxy.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By default, state changes are only queued up and processed in a microtask, allowing for batch-processing.<br>
|
||||
This means that for repeated changes to the same property, the user can decide to process all the changes in one go or even discard everything but the last value of each property.<br>
|
||||
That way one can easily avoid some types of unnecessary redraws, network requests, etc.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To make code more readable, the constructor defaults to adding an event listener that checks for a method called [prop]<code>Changed</code> in the state object, and calls it with the new value. This behaviour can be disabled by setting the <code>methods</code> option to false.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The state object also has a getter and setter pair for the `state` attribute, which simply accesses this same attribute on the proxy object.
|
||||
This is simply a convenience feature to save some typing on single-variable states.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<dl>
|
||||
<dt><code>defer</code></dt>
|
||||
<dd>Set to false to disable defered change processing. This will emit a new event every time something changes, even if it's about to be changed again in the next line.</dd>
|
||||
<dt><code>methods</code></dt>
|
||||
<dd>Set to false to disable the default event listener that attempts to call a [prop]Changed method on the state object.</dd>
|
||||
</dl>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Examples</h2>
|
||||
|
||||
<p>
|
||||
A simple counter state that prints a new count to the console every time it gets updated.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
const counter = new State({state: 0}, {defer: false})
|
||||
|
||||
counter.stateChanged = (count) => console.log(`new count: ${count}`)
|
||||
|
||||
counter.state += 1
|
||||
counter.state += 1
|
||||
counter.state += 1
|
||||
</code-block>
|
||||
|
||||
<p>
|
||||
This example uses an event listener instead to get notified of all property changes.
|
||||
Collecting changes into a map is an easy way of de-duplicating their values and keeping only the last one.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
const state = new State({}, {methods: false})
|
||||
|
||||
state.addEventListener("change", event => {
|
||||
console.log(`There were ${event.changes.length} changes.`)
|
||||
new Map(event.changes).forEach((value, prop) => {
|
||||
console.log(`Final vaue of ${prop}: ${value}`)
|
||||
})
|
||||
})
|
||||
|
||||
state.proxy.foo = "foo 1"
|
||||
state.proxy.foo = "foo 2"
|
||||
state.proxy.bar = "bar 1"
|
||||
state.proxy.foo = "foo 3"
|
||||
state.proxy.bar = "bar 2"
|
||||
|
||||
// There were 5 changes.
|
||||
// Final vaue of foo: foo 3
|
||||
// Final vaue of bar: bar 2
|
||||
</code-block>
|
||||
</section>
|
|
@ -24,13 +24,22 @@ support inheriting from other classes for extending builtin elements.
|
|||
Generate CSS from JS objects. Yes, you can generate CSS from JSON now. Or from
|
||||
YAML. Or from whatever you want, really.
|
||||
|
||||
## State
|
||||
|
||||
Combines both Listener and Speaker into one convenient class that batches and
|
||||
defers changes by default.
|
||||
|
||||
## Listener
|
||||
|
||||
(Deprecated; use `State` instead)
|
||||
|
||||
A proxy object that fires a callback when certain (or all) properties are
|
||||
changed.
|
||||
|
||||
## Speaker
|
||||
|
||||
(Deprecated; use `State` instead)
|
||||
|
||||
Simple messaging helper that uses microtasks by default.
|
||||
|
||||
## Debounce
|
||||
|
|
|
@ -15,8 +15,8 @@ export class Speaker {
|
|||
speak(...args) {
|
||||
if (!this._scheduled.length) {
|
||||
queueMicrotask(() => {
|
||||
for (let args of this._scheduled) {
|
||||
for (let callback of this._callbacks) {
|
||||
for (const args of this._scheduled) {
|
||||
for (const callback of this._callbacks) {
|
||||
callback(...args)
|
||||
}
|
||||
this._retain = args
|
||||
|
@ -28,7 +28,7 @@ export class Speaker {
|
|||
}
|
||||
|
||||
forget(callback) {
|
||||
this._callbacks.delete(callbacks)
|
||||
this._callbacks.delete(callback)
|
||||
}
|
||||
|
||||
silence() {
|
||||
|
@ -38,7 +38,7 @@ export class Speaker {
|
|||
|
||||
export class ImmediateSpeaker extends Speaker {
|
||||
speak(...args) {
|
||||
for (let callback of this._callbacks) {
|
||||
for (const callback of this._callbacks) {
|
||||
callback(...args)
|
||||
}
|
||||
this._retain = args
|
||||
|
|
53
state.js
Normal file
53
state.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
export class ChangeEvent extends Event {
|
||||
constructor(...changes) {
|
||||
super('change')
|
||||
this.changes = changes
|
||||
}
|
||||
}
|
||||
|
||||
export class State extends EventTarget {
|
||||
#target
|
||||
#options
|
||||
#queue
|
||||
constructor(target={}, options={}) {
|
||||
super()
|
||||
this.#options = options
|
||||
this.#target = target
|
||||
this.proxy = new Proxy(target, {
|
||||
set: (_target, prop, value) => { this.set(prop, value); return true },
|
||||
get: (target, prop) => target[prop],
|
||||
})
|
||||
|
||||
this.addEventListener
|
||||
|
||||
if (options.methods ?? true) {
|
||||
this.addEventListener("change", ({changes}) => {
|
||||
new Map(changes).forEach((value, prop) => {
|
||||
if (`${prop}Changed` in this) this[`${prop}Changed`](value)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
set state(value) { this.proxy.state = value }
|
||||
get state() { return this.proxy.state }
|
||||
|
||||
set(prop, value) {
|
||||
if (this.#options.defer ?? true) {
|
||||
if (!this.#queue) {
|
||||
this.#queue = []
|
||||
queueMicrotask(() => {
|
||||
this.dispatchEvent(new ChangeEvent(...this.#queue))
|
||||
this.#queue = undefined
|
||||
})
|
||||
}
|
||||
this.#queue.push([prop, value])
|
||||
this.#target[prop] = value
|
||||
} else {
|
||||
this.#target[prop] = value
|
||||
this.dispatchEvent(new ChangeEvent([prop, value]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default State
|
Loading…
Reference in a new issue