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") { 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

View File

@ -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) =&gt; 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>

View File

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

View File

@ -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 &xrarr; new-element</dd> <dd>...data &xrarr; 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 &xrarr; new-element</dd> <dd>...data &xrarr; 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
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 => { 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 = []

View File

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

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") 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
View File

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