Compare commits
10 commits
af137c4b24
...
0ffd1fabf6
Author | SHA1 | Date | |
---|---|---|---|
0ffd1fabf6 | |||
e6e158b2f4 | |||
0103592665 | |||
233a96d0e0 | |||
bef3fc5c31 | |||
42fa5e8d4d | |||
e99df6b58f | |||
1b1351fffa | |||
dcbadde066 | |||
a1178b916a |
9 changed files with 155 additions and 23 deletions
|
@ -46,7 +46,7 @@ export default Class => {
|
||||||
if (typeof prop.value == "function") {
|
if (typeof prop.value == "function") {
|
||||||
Class.queues = new WeakMap()
|
Class.queues = new WeakMap()
|
||||||
Class.prototype[name.slice(1)] = function(...args) {
|
Class.prototype[name.slice(1)] = function(...args) {
|
||||||
const queue = Class.queues.has(this) ? queues.get(this) : []
|
const queue = Class.queues.has(this) ? Class.queues.get(this) : []
|
||||||
if (!queue.length) queueMicrotask(() => {
|
if (!queue.length) queueMicrotask(() => {
|
||||||
this[name](...queue)
|
this[name](...queue)
|
||||||
queue.length = 0
|
queue.length = 0
|
||||||
|
|
18
index.html
18
index.html
|
@ -15,6 +15,8 @@
|
||||||
<p style="text-align: center;">
|
<p style="text-align: center;">
|
||||||
A collection of <em>JavaScript modules</em> to make <em>front-end</em> easier.
|
A collection of <em>JavaScript modules</em> to make <em>front-end</em> easier.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
@ -81,6 +83,22 @@
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</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>
|
||||||
|
|
||||||
|
<code-block>
|
||||||
|
const speaker = new Speaker()
|
||||||
|
speaker.listen((...args) => console.log(...args))
|
||||||
|
speaker.speak("First", "second", "third")
|
||||||
|
</code-block>
|
||||||
|
|
||||||
|
No, this has nothing to do with playing audio.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Debounce</h2>
|
<h2>Debounce</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -4,11 +4,11 @@ properties and react accordingly.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
const l = listener()
|
const l = listener()
|
||||||
l.listen("contract", contract => speaker.handle(contract))
|
l.listen("contract", contract => speaker.speak(contract))
|
||||||
l.contract = new Contract()
|
l.contract = Sithis.getNewContract()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const registry = new Map()
|
const registry = new WeakMap()
|
||||||
const listener = (target={}) => {
|
const listener = (target={}) => {
|
||||||
const callbacks = new Map()
|
const callbacks = new Map()
|
||||||
const methods = Object.create(null)
|
const methods = Object.create(null)
|
||||||
|
|
|
@ -270,6 +270,12 @@
|
||||||
<section>
|
<section>
|
||||||
<h2 id="bind">The <code>bind</code> helper</h2>
|
<h2 id="bind">The <code>bind</code> helper</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bind is a low-magic abstraction for simple full re-render micro-components.
|
||||||
|
It takes a function that renders input data into a DOM subtree and returns an update function.
|
||||||
|
Every call of the update function will trigger a full re-render of the entire subree and replace the old one within the DOM.
|
||||||
|
</p>
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<code>
|
<code>
|
||||||
<dt>bind</dt>
|
<dt>bind</dt>
|
||||||
|
@ -281,7 +287,7 @@
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Transform function</dt>
|
<dt>Transform function</dt>
|
||||||
<code>
|
<code>
|
||||||
<dd>...data ⟶ new-element</dd>
|
<dd>...data ⟶ element</dd>
|
||||||
</code>
|
</code>
|
||||||
<dd>
|
<dd>
|
||||||
A function that takes the current state and returns a new HTML element.
|
A function that takes the current state and returns a new HTML element.
|
||||||
|
@ -294,7 +300,7 @@
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Update function</dt>
|
<dt>Update function</dt>
|
||||||
<code>
|
<code>
|
||||||
<dd>...data ⟶ new-element</dd>
|
<dd>...data ⟶ element</dd>
|
||||||
</code>
|
</code>
|
||||||
<dd>
|
<dd>
|
||||||
A function that passes its arguments to the transform function and
|
A function that passes its arguments to the transform function and
|
||||||
|
|
88
page/speaker.html
Normal file
88
page/speaker.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
<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">speaker.js</h1>
|
||||||
|
|
||||||
|
<code-block>import {Speaker} from 'speaker.js'</code-block>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Description</h2>
|
||||||
|
<p>
|
||||||
|
A publish/subscribe helper class that uses micro-tasks to relay messages to many subscribers.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Example</h2>
|
||||||
|
|
||||||
|
<code-block>
|
||||||
|
const speaker = new Speaker()
|
||||||
|
speaker.listen(message => { document.title = message })
|
||||||
|
speaker.listen(message => { console.log(`Message received: ${message}`) })
|
||||||
|
|
||||||
|
speaker.speak("New page title")
|
||||||
|
console.log("This line runs before any of the callbacks")
|
||||||
|
</code-block>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Note that the callbacks don't run immediately.
|
||||||
|
They are scheduled to a micro-task to be called later on.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Methods</h2>
|
||||||
|
<dl>
|
||||||
|
<dt><code>new Speaker(...initial) : Speaker</code></dt>
|
||||||
|
<dd>
|
||||||
|
Constructs a new speaker.
|
||||||
|
All arguments passed to the constructor will be retained and returned
|
||||||
|
by any call to <code>listen</code> before the first time <code>speak</code>
|
||||||
|
is called.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt><code>Speaker.listen(callback : (...args) => undefined) : [...args]</code></dt>
|
||||||
|
<dd>
|
||||||
|
Registers a callback to be called on every new message.
|
||||||
|
Returns an array containing the retained arguments of the last message.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt><code>Speaker.speak(...args)</code></dt>
|
||||||
|
<dd>
|
||||||
|
Relays a message of zero or more arguments to all registered callbacks.
|
||||||
|
As mentioned above, callbacks will not be called immediately, but scheduled in a new micro-task.
|
||||||
|
The arguments are retained until <code>speak</code> is called again.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt><code>Speaker.forget(callback)</code></dt>
|
||||||
|
<dd>
|
||||||
|
Removes a single callback function from its list of callbacks.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt><code>Speaker.silence()</code></dt>
|
||||||
|
<dd>
|
||||||
|
Clears the list of callbacks completely.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Immediate Speaker</h2>
|
||||||
|
<p>
|
||||||
|
Additionally, the module exposts the <code>ImmediateSpeaker</code> class,
|
||||||
|
which does the exact same as a normal speaker,
|
||||||
|
but executes its callbacks immediately instead of scheduling a micro-task.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The API is the <em>exact same</em> as a normal <code>Speaker</code>.
|
||||||
|
</p>
|
||||||
|
</section>
|
12
pqueue.js
12
pqueue.js
|
@ -1,3 +1,14 @@
|
||||||
|
// Promise Queue
|
||||||
|
// Wraps promises to make sure they resolve in the order they were wrapped.
|
||||||
|
//
|
||||||
|
// Usage example:
|
||||||
|
// - Some asynchronous process starts fetching pages from a server in order
|
||||||
|
// - New requests may start before previous ones are finished
|
||||||
|
// - With some bad luck, page M may load before page N < M
|
||||||
|
// This means you can't just append pages as they arrive.
|
||||||
|
// PQueue will make sure promise M never resolves before promise N.
|
||||||
|
|
||||||
|
// Wrap a promise to make its state queryable
|
||||||
const queryable = promise => {
|
const queryable = promise => {
|
||||||
let result = promise.then(result => {
|
let result = promise.then(result => {
|
||||||
q.result = result
|
q.result = result
|
||||||
|
@ -9,6 +20,7 @@ const queryable = promise => {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Integer => Promise => Promise
|
||||||
export default (parallel = 1) => {
|
export default (parallel = 1) => {
|
||||||
const running = []
|
const running = []
|
||||||
const waiting = []
|
const waiting = []
|
||||||
|
|
24
readme.md
24
readme.md
|
@ -8,10 +8,11 @@ copy-pasting around more often than necessary.
|
||||||
|
|
||||||
So what does it all do?
|
So what does it all do?
|
||||||
|
|
||||||
## Better
|
## Skooma
|
||||||
|
|
||||||
an "improved" version of the builtin HTMLElement that's hopefully a lot easier
|
Generate HTML and SVG DOM nodes more easily and do stuff with them. Feels like
|
||||||
to build actual things with. It's really just another utility layer.
|
an in-between of using a templating language and writing lisp code. Overall very
|
||||||
|
recommendable.
|
||||||
|
|
||||||
## Element
|
## Element
|
||||||
|
|
||||||
|
@ -32,11 +33,9 @@ changed.
|
||||||
|
|
||||||
Simple messaging helper that uses microtasks by default.
|
Simple messaging helper that uses microtasks by default.
|
||||||
|
|
||||||
## Skooma
|
## Debounce
|
||||||
|
|
||||||
Generate HTML and SVG DOM nodes more easily and do stuff with them. Feels like
|
Debouncing wrapper for functions to avoid repeated execution of expensive code.
|
||||||
an in-between of using a templating language and writing lisp code. Overall very
|
|
||||||
recommendable.
|
|
||||||
|
|
||||||
## Template
|
## Template
|
||||||
|
|
||||||
|
@ -47,4 +46,13 @@ like 5 lines or so.
|
||||||
|
|
||||||
Currently a sngle class `ObjectStorage` implementing the API of the Storage
|
Currently a sngle class `ObjectStorage` implementing the API of the Storage
|
||||||
class using a plain JS Map as backend. This is mostly meant as a page-local
|
class using a plain JS Map as backend. This is mostly meant as a page-local
|
||||||
fallback to LocalStorage and SessionStorage
|
fallback to LocalStorage and SessionStorage.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
Allows you to apply code to HTML elements by looking for a `use` attribute and
|
||||||
|
running it as code on the element.
|
||||||
|
|
||||||
|
## Pqueue
|
||||||
|
|
||||||
|
Ensures in-order promise resolution and optionally limits parallel execution.
|
||||||
|
|
17
skooma.js
17
skooma.js
|
@ -15,7 +15,7 @@ const keyToPropName = key => key.replace(/^[A-Z]/, a => "-"+a).replace(/[A-Z]/g,
|
||||||
export const empty = Symbol("Explicit empty argument for Skooma")
|
export const empty = Symbol("Explicit empty argument for Skooma")
|
||||||
|
|
||||||
const insertStyles = (rule, styles) => {
|
const insertStyles = (rule, styles) => {
|
||||||
for (let [key, value] of Object.entries(styles))
|
for (const [key, value] of Object.entries(styles))
|
||||||
if (typeof value == "undefined")
|
if (typeof value == "undefined")
|
||||||
rule.removeProperty(keyToPropName(key))
|
rule.removeProperty(keyToPropName(key))
|
||||||
else
|
else
|
||||||
|
@ -46,11 +46,11 @@ const getCustom = args => String(
|
||||||
|
|
||||||
const parseArgs = (element, before, ...args) => {
|
const parseArgs = (element, before, ...args) => {
|
||||||
if (element.content) element = element.content
|
if (element.content) element = element.content
|
||||||
for (let arg of args) if (arg !== empty)
|
for (const arg of args) if (arg !== empty)
|
||||||
if (typeof arg == "string" || typeof arg == "number")
|
if (typeof arg == "string" || typeof arg == "number")
|
||||||
element.insertBefore(document.createTextNode(arg), before)
|
element.insertBefore(document.createTextNode(arg), before)
|
||||||
else if (arg === undefined || arg == null)
|
else if (arg === undefined || arg == null)
|
||||||
console.warn(`Argument is ${typeof arg}`, element)
|
console.warn(`An argument of type ${typeof arg} has been ignored`, element)
|
||||||
else if (typeof arg == "function")
|
else if (typeof arg == "function")
|
||||||
arg(element)
|
arg(element)
|
||||||
else if ("nodeName" in arg)
|
else if ("nodeName" in arg)
|
||||||
|
@ -58,11 +58,11 @@ const parseArgs = (element, before, ...args) => {
|
||||||
else if ("length" in arg)
|
else if ("length" in arg)
|
||||||
parseArgs(element, before, ...arg)
|
parseArgs(element, before, ...arg)
|
||||||
else
|
else
|
||||||
for (let key in arg)
|
for (const key in arg)
|
||||||
if (key == "style" && typeof(arg[key])=="object")
|
if (key == "style" && typeof(arg[key])=="object")
|
||||||
insertStyles(element.style, arg[key])
|
insertStyles(element.style, arg[key])
|
||||||
else if (key == "dataset" && typeof(arg[key])=="object")
|
else if (key == "dataset" && typeof(arg[key])=="object")
|
||||||
for (let [key2, value] of Object.entries(arg[key]))
|
for (const [key2, value] of Object.entries(arg[key]))
|
||||||
element.dataset[key2] = parseAttribute(value)
|
element.dataset[key2] = parseAttribute(value)
|
||||||
else if (key == "shadowRoot")
|
else if (key == "shadowRoot")
|
||||||
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), null, arg[key])
|
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), null, arg[key])
|
||||||
|
@ -76,10 +76,9 @@ const parseArgs = (element, before, ...args) => {
|
||||||
element.setAttribute(key, parseAttribute(arg[key]))
|
element.setAttribute(key, parseAttribute(arg[key]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nop = object => object
|
|
||||||
const node = (name, args, options) => {
|
const node = (name, args, options) => {
|
||||||
let element
|
let element
|
||||||
let custom = getCustom(args)
|
const custom = getCustom(args)
|
||||||
if ("nameFilter" in options) name = options.nameFilter(name)
|
if ("nameFilter" in options) name = options.nameFilter(name)
|
||||||
if (options.xmlns)
|
if (options.xmlns)
|
||||||
element = document.createElementNS(options.xmlns, name, {is: custom})
|
element = document.createElementNS(options.xmlns, name, {is: custom})
|
||||||
|
@ -90,8 +89,8 @@ const node = (name, args, options) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameSpacedProxy = (options={}) => new Proxy(Window, {
|
const nameSpacedProxy = (options={}) => new Proxy(Window, {
|
||||||
get: (target, prop, receiver) => { return (...args) => node(prop, args, options) },
|
get: (_target, prop, _receiver) => { return (...args) => node(prop, args, options) },
|
||||||
has: (target, prop) => true,
|
has: (_target, _prop) => true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const bind = transform => {
|
export const bind = transform => {
|
||||||
|
|
1
use.js
1
use.js
|
@ -10,6 +10,7 @@ const apply = (func, node) => {
|
||||||
|
|
||||||
export const use = node => {
|
export const use = node => {
|
||||||
const code = Function("return (" + node.getAttribute("use") + ")")
|
const code = Function("return (" + node.getAttribute("use") + ")")
|
||||||
|
node.removeAttribute("use")
|
||||||
const func = code()
|
const func = code()
|
||||||
apply(func, node)
|
apply(func, node)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue