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>
|
<a class="button" href="page/skooma.html">Read more</a>
|
||||||
</section>
|
</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>
|
<section>
|
||||||
<h2>Debounce</h2>
|
<h2>Debounce</h2>
|
||||||
<p>
|
<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);
|
border-bottom: .1em solid var(--color-ac);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3::before {
|
||||||
|
content: "\2023" ' ';
|
||||||
|
color: var(--color-ac);
|
||||||
|
}
|
||||||
|
|
||||||
em {
|
em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: bold;
|
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
|
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.
|
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
|
## CSS
|
||||||
|
|
||||||
Generate CSS from JS objects. Yes, you can generate CSS from JSON now. Or from
|
Generate CSS from JS objects. Yes, you can generate CSS from JSON now. Or from
|
||||||
|
|
Loading…
Reference in a new issue