Add element helper
This commit is contained in:
parent
737fb6d155
commit
70c702efc7
5 changed files with 252 additions and 0 deletions
70
element.js
Normal file
70
element.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
export default Class => {
|
||||
const proto = Class.prototype
|
||||
|
||||
const attributes = Class.attributes || {}
|
||||
|
||||
const props = []
|
||||
|
||||
Object.entries(attributes).forEach(([name, attribute]) => {
|
||||
let htmlName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
||||
props.push(htmlName)
|
||||
let prop = {}
|
||||
|
||||
prop.get = typeof attribute.get == "function"
|
||||
? function() { return attribute.get.call(this, this.getAttribute(htmlName)) }
|
||||
: function() { return this.getAttribute(htmlName) }
|
||||
|
||||
prop.set = typeof attribute.set == "function"
|
||||
? function(val) { return this.setAttribute(htmlName, attribute.set.call(this, val)) }
|
||||
: attribute.set === false
|
||||
? function(val) { throw(Error(`Attribute ${name} cannot be set`)) }
|
||||
: function(val) { this.setAttribute(htmlName, val) }
|
||||
|
||||
Object.defineProperty(proto, name, prop)
|
||||
})
|
||||
Object.freeze(props)
|
||||
|
||||
Object.defineProperty(Class.prototype, "props", { get() { return Object.fromEntries(props.map(prop => [prop, this[prop]])) } })
|
||||
|
||||
const observedAttributes = Object.freeze([...Object.keys(attributes)])
|
||||
|
||||
Object.defineProperty(Class, "observedAttributes", {
|
||||
get() { return observedAttributes }
|
||||
})
|
||||
|
||||
Class.prototype.attributeChangedCallback = function(name, oldValue, newValue) {
|
||||
name = name.replaceAll(/-(.)/g, (a, b) => b.toUpperCase())
|
||||
if (`${name}Changed` in this) this[`${name}Changed`](oldValue, newValue)
|
||||
if (`changed` in this) this.changed(name, oldValue, newValue)
|
||||
}
|
||||
|
||||
/* Enable batch-processing for dollar-methods */
|
||||
/* This would be much nicer if decorators were a thing */
|
||||
for (const name of Object.getOwnPropertyNames(Class.prototype)) {
|
||||
if (name[0] == "$") {
|
||||
const prop = Object.getOwnPropertyDescriptor(Class.prototype, name)
|
||||
if (typeof prop.value == "function") {
|
||||
const queue = []
|
||||
Class.prototype[name.slice(1)] = function(...args) {
|
||||
if (!queue.length) queueMicrotask(() => {
|
||||
this[name](...queue)
|
||||
queue.length = 0
|
||||
})
|
||||
queue.push(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.prototype[Symbol.toPrimitive] = function(hint) {
|
||||
const name = `to${hint.replace(/./, e => e.toUpperCase())}`
|
||||
return name in this
|
||||
? this[name]()
|
||||
: "toDefault" in this
|
||||
? this.toDefault()
|
||||
: `[object ${Class.name}]`
|
||||
}
|
||||
|
||||
name = Class.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
||||
customElements.define(name, Class, {extends: Class.is})
|
||||
}
|
25
index.html
25
index.html
|
@ -30,6 +30,31 @@
|
|||
<a class="button" href="page/skooma.html">Read more</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Element</h2>
|
||||
<p>
|
||||
A helper function that adds many convenient features to classes for custom elements.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<h3 class="all-unset"><b>Code Sample</b>:</h3>
|
||||
<code-block>
|
||||
import element from 'element.js'
|
||||
|
||||
element(class MyElement extends HTMLElement {
|
||||
static attributes = { foo: true }
|
||||
fooChanged(oldFoo, newFoo) {
|
||||
console.log(`Foo changed from ${oldFoo} to ${newFoo}`)
|
||||
render()
|
||||
}
|
||||
$render() { /* ... */ }
|
||||
})
|
||||
</code-block>
|
||||
</p>
|
||||
|
||||
<a class="button" href="page/element.html">Read More</a>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Debounce</h2>
|
||||
<p>
|
||||
|
|
147
page/element.html
Normal file
147
page/element.html
Normal file
|
@ -0,0 +1,147 @@
|
|||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<script type="module" src="codeblock.js"></script>
|
||||
<script type="module" src="filesize.js"></script>
|
||||
|
||||
<h1 class="js module">element.js</h1>
|
||||
|
||||
<code-block>import element from 'element.js'</code-block>
|
||||
|
||||
<section>
|
||||
<h2>Description</h2>
|
||||
<p>
|
||||
The <code>element</code> helper automates many utility features that
|
||||
often have to be added to every new custom element, like dispatching
|
||||
to different handler methods depending on the name of a changed attribute
|
||||
or adding getters and setters for these attributes.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Basic Usage</h2>
|
||||
|
||||
<code-block>
|
||||
element(class MyElement extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
console.log("An element walks into a bar...")
|
||||
}
|
||||
})
|
||||
</code-block>
|
||||
|
||||
<h2>Features</h2>
|
||||
|
||||
<section>
|
||||
<h3>Attributes</h3>
|
||||
<p>
|
||||
The static <code>attributes</code> property, when present,
|
||||
is used to automatically add the static <code>observedAttributes</code>
|
||||
property for the custom element API, as well as define getters and setters
|
||||
for the listed attributes.
|
||||
</p><p>
|
||||
A pair of filters can be given to modify the value after getting or before
|
||||
setting it. This can be used to convert between the string values that
|
||||
attributes restrict us to and a more sensible representation as well as
|
||||
some validation (attributes can be changed externally, after all).
|
||||
</p><p>
|
||||
Object keys in camelCase will be converted to kebab-case before being
|
||||
used as attribute names in the auto-generated <code>observedAttributes</code>.
|
||||
</p><p>
|
||||
The rules are:
|
||||
<ul>
|
||||
<li>Any truthy value registers the attribute with getter and setter
|
||||
<li>If the value has a <code>get</code> property, it will be incorporated as a filter into the getter.
|
||||
<li>If the value has a <code>set</code> property, it will be incorporated as a filter into the setter.
|
||||
<li><b>Unless</b> this value is <code>false</code> no setter will be added (read only attribute).
|
||||
</ul>
|
||||
</p>
|
||||
<code-block>
|
||||
element(class extends HTMLElement {
|
||||
static attributes = {
|
||||
plain = true,
|
||||
filtered = {
|
||||
get(string) { return Number(string) }
|
||||
set(value) { return Math.floor(value*100+0.5)/100 }
|
||||
},
|
||||
plainReadOnly = { set: false },
|
||||
}
|
||||
})
|
||||
</code-block>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Change Methods</h3>
|
||||
<p>
|
||||
The <code>attributeChangedCallback</code> method is added automatically to the class.
|
||||
This auto-generated callback will first look up a <code>${attributeName}Changed</code>
|
||||
method and call it, if found, with the old and new values as arguments.
|
||||
Then it will look for a <code>changed</code> method and call it with the same arguments
|
||||
as <code>attributeChangedCallback</code> normally receives.
|
||||
</p><p>
|
||||
Attribute names will be converted from camelCase to kebab-case when needed before
|
||||
being used to form a method name to look up.
|
||||
Changing an attribute <code>foo-bar</code> will look for a <code>fooBarChanged</code> method.
|
||||
</p>
|
||||
<code-block>
|
||||
element(class extends HTMLElement {
|
||||
static attributes = { fooBar = true }
|
||||
fooBarChanged(oldValue, newValue) {
|
||||
console.log(`foo-bar changed from ${oldValue} to ${newValue}`)
|
||||
}
|
||||
})
|
||||
</code-block>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Batched Methods</h3>
|
||||
<p>
|
||||
Certain methods, like re-rendering the content of a component
|
||||
should often happen in response to events that can happen repeatedly,
|
||||
or in response to many different events that can all happen at once.
|
||||
</p><p>
|
||||
To avoid having to repeatedly add complicated checks to handle these
|
||||
event bursts, <code>element</code> introduces the concept of <em>dollar-methods</em>.
|
||||
</p>
|
||||
<code-block>
|
||||
element(class extends HTMLElement {
|
||||
$render() {
|
||||
console.warn("Full Re-render...")
|
||||
}
|
||||
})
|
||||
</code-block>
|
||||
<p>
|
||||
Any method with a name starting with <code>$</code> will automatically
|
||||
have a sibling-method defined, with the dollar removed. Each time this
|
||||
auto-generated method is called, its argument list will be pushed to an array
|
||||
and a call of the original dollar-method will be scheduled as a micro-task.
|
||||
</p><p>
|
||||
The original method will then be called with the argument-lists of the individual
|
||||
calls to its dollar-less counterpart all at once.
|
||||
</p><p>
|
||||
It is of course still possible to manually call the dollar-method when
|
||||
immediate execution is wanted.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note how in the following example, all three change methods would be called
|
||||
in rapid succession if the browser applies the custom element class to an element
|
||||
it found on the page with all three attributes <code>foo</code>, <code>bar</code>
|
||||
and <code>baz</code> defined.
|
||||
</p>
|
||||
|
||||
<code-block>
|
||||
element(class extends HTMLElement {
|
||||
$render() { console.warn("Full Re-render...") }
|
||||
|
||||
fooChanged() { this.render() }
|
||||
barChanged() { this.render() }
|
||||
bazChanged() { this.render() }
|
||||
})
|
||||
</code-block>
|
||||
|
||||
<p>
|
||||
Here, <code>this.render()</code> will not instantly re-render the whole component
|
||||
but instead schedule the re-render in a micro-task, potentially avoiding lots of
|
||||
repeated work.
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
|
@ -41,6 +41,11 @@ h2 {
|
|||
border-bottom: .1em solid var(--color-ac);
|
||||
}
|
||||
|
||||
h3::before {
|
||||
content: "\2023" ' ';
|
||||
color: var(--color-ac);
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -13,6 +13,11 @@ So what does it all do?
|
|||
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.
|
||||
|
||||
## Element
|
||||
|
||||
the second iteration of improved `HTMLElement` but this time in a function to
|
||||
support inheriting from other classes for extending builtin elements.
|
||||
|
||||
## CSS
|
||||
|
||||
Generate CSS from JS objects. Yes, you can generate CSS from JSON now. Or from
|
||||
|
|
Loading…
Reference in a new issue