Compare commits

..

10 commits

9 changed files with 155 additions and 23 deletions

View file

@ -46,7 +46,7 @@ export default Class => {
if (typeof prop.value == "function") {
Class.queues = new WeakMap()
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(() => {
this[name](...queue)
queue.length = 0

View file

@ -15,6 +15,8 @@
<p style="text-align: center;">
A collection of <em>JavaScript modules</em> to make <em>front-end</em> easier.
</p>
<p>
</p>
</section>
<section>
@ -81,6 +83,22 @@
</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>
<code-block>
const speaker = new Speaker()
speaker.listen((...args) =&gt; console.log(...args))
speaker.speak("First", "second", "third")
</code-block>
No, this has nothing to do with playing audio.
</p>
</section>
<section>
<h2>Debounce</h2>
<p>

View file

@ -4,11 +4,11 @@ properties and react accordingly.
Example:
const l = listener()
l.listen("contract", contract => speaker.handle(contract))
l.contract = new Contract()
l.listen("contract", contract => speaker.speak(contract))
l.contract = Sithis.getNewContract()
*/
const registry = new Map()
const registry = new WeakMap()
const listener = (target={}) => {
const callbacks = new Map()
const methods = Object.create(null)

View file

@ -270,6 +270,12 @@
<section>
<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>
<code>
<dt>bind</dt>
@ -281,7 +287,7 @@
<dl>
<dt>Transform function</dt>
<code>
<dd>...data &xrarr; new-element</dd>
<dd>...data &xrarr; element</dd>
</code>
<dd>
A function that takes the current state and returns a new HTML element.
@ -294,7 +300,7 @@
</dd>
<dt>Update function</dt>
<code>
<dd>...data &xrarr; new-element</dd>
<dd>...data &xrarr; element</dd>
</code>
<dd>
A function that passes its arguments to the transform function and

88
page/speaker.html Normal file
View 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 =&gt; { document.title = message })
speaker.listen(message =&gt; { 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) =&gt; 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>

View file

@ -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 => {
let result = promise.then(result => {
q.result = result
@ -9,6 +20,7 @@ const queryable = promise => {
return result
}
// Integer => Promise => Promise
export default (parallel = 1) => {
const running = []
const waiting = []

View file

@ -8,10 +8,11 @@ copy-pasting around more often than necessary.
So what does it all do?
## Better
## Skooma
an "improved" version of the builtin HTMLElement that's hopefully a lot easier
to build actual things with. It's really just another utility layer.
Generate HTML and SVG DOM nodes more easily and do stuff with them. Feels like
an in-between of using a templating language and writing lisp code. Overall very
recommendable.
## Element
@ -32,11 +33,9 @@ changed.
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
an in-between of using a templating language and writing lisp code. Overall very
recommendable.
Debouncing wrapper for functions to avoid repeated execution of expensive code.
## Template
@ -47,4 +46,13 @@ like 5 lines or so.
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
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.

View file

@ -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")
const insertStyles = (rule, styles) => {
for (let [key, value] of Object.entries(styles))
for (const [key, value] of Object.entries(styles))
if (typeof value == "undefined")
rule.removeProperty(keyToPropName(key))
else
@ -46,11 +46,11 @@ const getCustom = args => String(
const parseArgs = (element, before, ...args) => {
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")
element.insertBefore(document.createTextNode(arg), before)
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")
arg(element)
else if ("nodeName" in arg)
@ -58,11 +58,11 @@ const parseArgs = (element, before, ...args) => {
else if ("length" in arg)
parseArgs(element, before, ...arg)
else
for (let key in arg)
for (const key in arg)
if (key == "style" && typeof(arg[key])=="object")
insertStyles(element.style, arg[key])
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)
else if (key == "shadowRoot")
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]))
}
const nop = object => object
const node = (name, args, options) => {
let element
let custom = getCustom(args)
const custom = getCustom(args)
if ("nameFilter" in options) name = options.nameFilter(name)
if (options.xmlns)
element = document.createElementNS(options.xmlns, name, {is: custom})
@ -90,8 +89,8 @@ const node = (name, args, options) => {
}
const nameSpacedProxy = (options={}) => new Proxy(Window, {
get: (target, prop, receiver) => { return (...args) => node(prop, args, options) },
has: (target, prop) => true,
get: (_target, prop, _receiver) => { return (...args) => node(prop, args, options) },
has: (_target, _prop) => true,
})
export const bind = transform => {

1
use.js
View file

@ -10,6 +10,7 @@ const apply = (func, node) => {
export const use = node => {
const code = Function("return (" + node.getAttribute("use") + ")")
node.removeAttribute("use")
const func = code()
apply(func, node)
}