diff --git a/element.js b/element.js
new file mode 100644
index 0000000..23e5b9a
--- /dev/null
+++ b/element.js
@@ -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})
+}
diff --git a/index.html b/index.html
index b1accdc..0f705d6 100644
--- a/index.html
+++ b/index.html
@@ -30,6 +30,31 @@
Read more
+
+ A helper function that adds many convenient features to classes for custom elements.
+
+ Element
+ Code Sample:
+
diff --git a/page/element.html b/page/element.html new file mode 100644 index 0000000..7f6af51 --- /dev/null +++ b/page/element.html @@ -0,0 +1,147 @@ + + + + + +
+ The element
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.
+
+ The static attributes
property, when present,
+ is used to automatically add the static observedAttributes
+ property for the custom element API, as well as define getters and setters
+ for the listed attributes.
+
+ 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). +
+ Object keys in camelCase will be converted to kebab-case before being
+ used as attribute names in the auto-generated observedAttributes
.
+
+ The rules are: +
get
property, it will be incorporated as a filter into the getter.
+ set
property, it will be incorporated as a filter into the setter.
+ false
no setter will be added (read only attribute).
+
+ The attributeChangedCallback
method is added automatically to the class.
+ This auto-generated callback will first look up a ${attributeName}Changed
+ method and call it, if found, with the old and new values as arguments.
+ Then it will look for a changed
method and call it with the same arguments
+ as attributeChangedCallback
normally receives.
+
+ 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 foo-bar
will look for a fooBarChanged
method.
+
+ 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. +
+ To avoid having to repeatedly add complicated checks to handle these
+ event bursts, element
introduces the concept of dollar-methods.
+
+ Any method with a name starting with $
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.
+
+ The original method will then be called with the argument-lists of the individual + calls to its dollar-less counterpart all at once. +
+ It is of course still possible to manually call the dollar-method when + immediate execution is wanted. +
+ +
+ 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 foo
, bar
+ and baz
defined.
+
+ Here, this.render()
will not instantly re-render the whole component
+ but instead schedule the re-render in a micro-task, potentially avoiding lots of
+ repeated work.
+