Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
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 */
static parseAttributeValue(name, value) {
return value;
constructor() {
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) {
__getFromShadowRoot(target, name) {
return this.shadowRoot.querySelector('[ref="' + name + '"]');
attributeChangedCallback(attr, oldValue, newValue) {
this.__attributesMap[attr] =,
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) {
emitEvent(name, detail) {
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
allAttributesChangedCallback() {}
window.HTMLElementPlus = HTMLElementPlus;
<!DOCTYPE html>
<script src=""></script>
<script src="/html-element-plus.js"></script>
<my-el attr1="1" attr3="3" other-attr="foo:bar;"></my-el>
<template id="my-el-template">
:host pre {
background: lightgrey;
padding: 1em;
My El Demo:
<code><pre ref="content"></pre></code>
var myElTemplate = document.querySelector('#my-el-template');
if (window.ShadyCSS) {
window.ShadyCSS.prepareTemplate(myElTemplate, 'my-el');
class MyEl extends HTMLElementPlus {
constructor() {
// 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) {
static get observedAttributes() {
return [
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);

This comment has been minimized.

Copy link

@rafegoldberg rafegoldberg commented Feb 12, 2018

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


This comment has been minimized.

Copy link

@patrickjaja patrickjaja commented Jul 11, 2018

thx for sharing!


This comment has been minimized.

Copy link

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