Skip to content

Instantly share code, notes, and snippets.

@AdaRoseCannon
Last active March 3, 2023 11:33
Show Gist options
  • Save AdaRoseCannon/60dc448b1124bcfb7be14209b8124f5c to your computer and use it in GitHub Desktop.
Save AdaRoseCannon/60dc448b1124bcfb7be14209b8124f5c to your computer and use it in GitHub Desktop.
HTML Element Plus for Web Components

My Custom Elements Reusable Bits

I have extended HTMLElement as HTMLElementPlus to contain the bits of functionality I keep reimplementing.

There is an example usage in the HTML file below. And a Glitch here: Edit on Glitch

Attribute Callbacks

class MyEl extends HTMLElementPlus {...

Provides a callback when all attributes have been parsed, rather than one-by-one. allAttributesChangedCallback useful for waiting to handle all at once.

allAttributesChangedCallback gets called with an object with parsed attributes.

The parser can be set by setting the function static parseAttributeValue(name, value) in the class.

Default attribute values can be provided by setting the static defaultAttributeValue(name) function, so you can provide sensible fallback values.

Query the shadow dom by reference

E.g. an element in the shadow dom: <span ref="foobar"></span> can be queried using this.refs.foobar;

Easy event firing.

Fire an event using this.emitEvent('event-name', {foo: 'bar'});

This can be listed for using, el.addEventListener;

'use strict';
class HTMLElementPlus extends HTMLElement {
static defaultAttributeValue() {
/* the name of the attribute is parsed in as a parameter */
return;
}
static parseAttributeValue(name, value) {
return value;
}
constructor() {
super();
this.refs = new Proxy(
{},
{
get: this.__getFromShadowRoot.bind(this)
}
);
// Gets populated by attributeChangedCallback
this.__attributesMap = {};
this.__waitingOnAttr = (this.constructor.observedAttributes
? this.constructor.observedAttributes
: []
).filter(name => {
if (!this.attributes.getNamedItem(name)) {
this.__attributesMap[name] = this.constructor.defaultAttributeValue(name);
}
return !!this.attributes.getNamedItem(name);
});
// No attributes so update attribute never called.
// SO fire this anyway.
if (this.__waitingOnAttr.length === 0) {
this.allAttributesChangedCallback(this.__attributesMap);
}
}
__getFromShadowRoot(target, name) {
return this.shadowRoot.querySelector('[ref="' + name + '"]');
}
attributeChangedCallback(attr, oldValue, newValue) {
this.__attributesMap[attr] = this.constructor.parseAttributeValue.call(this,
attr,
newValue
);
if (this.__waitingOnAttr.length) {
const index = this.__waitingOnAttr.indexOf(attr);
if (index !== -1) {
// Remove it from array.
this.__waitingOnAttr.splice(index, 1);
}
}
// All attributes parsed
if (this.__waitingOnAttr.length === 0) {
this.allAttributesChangedCallback(this.__attributesMap);
}
}
emitEvent(name, detail) {
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
}
allAttributesChangedCallback() {}
}
window.HTMLElementPlus = HTMLElementPlus;
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.rawgit.com/webcomponents/webcomponentsjs/edf84e6e/webcomponents-sd-ce.js"></script>
<script src="/html-element-plus.js"></script>
</head>
<body>
<my-el attr1="1" attr3="3" other-attr="foo:bar;"></my-el>
<template id="my-el-template">
<style>
:host pre {
background: lightgrey;
padding: 1em;
}
</style>
<h1>
My El Demo:
</h1>
<code><pre ref="content"></pre></code>
<slot></slot>
</template>
<script>
var myElTemplate = document.querySelector('#my-el-template');
if (window.ShadyCSS) {
window.ShadyCSS.prepareTemplate(myElTemplate, 'my-el');
}
class MyEl extends HTMLElementPlus {
constructor() {
super();
// Attach a shadow root to the element, so that the
// implementation is hidden in a 🎁.
let shadowRoot = this.attachShadow({mode: 'open'});
// Put the content of the template inside the shadow DOM.
this.shadowRoot.appendChild(document.importNode(myElTemplate.content, true));
if (window.ShadyCSS) {
window.ShadyCSS.styleElement(this);
}
}
static get observedAttributes() {
return [
'attr1',
'attr2',
'attr3'
]
}
static defaultAttributeValue(name) {
return 'no value, ' + name;
}
static parseAttributeValue(name, value) {
return Number(value);
}
allAttributesChangedCallback(data) {
this.refs.content.textContent = JSON.stringify(data, null, ' ');
}
}
window.addEventListener('DOMContentLoaded', function() {
customElements.define('my-el', MyEl);
});
</script>
</body>
</html>
@rafegoldberg
Copy link

@AdaRoseCannon this was an enlightening read; thanks for the code!

@patrickjaja
Copy link

thx for sharing!

@leegee
Copy link

leegee commented Oct 4, 2018

Much appreciated, esp the clever use of Proxy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment