Skip to content

Instantly share code, notes, and snippets.

@kevinpschaaf
Last active June 25, 2020 21:21
Show Gist options
  • Save kevinpschaaf/9f8d8fc238a09656b8bad7f7b062a2fd to your computer and use it in GitHub Desktop.
Save kevinpschaaf/9f8d8fc238a09656b8bad7f7b062a2fd to your computer and use it in GitHub Desktop.
Patchable CustomElementsRegistry

Patchable CustomElementsRegistry

Allows calling customElements.define multiple times for the same tagname. Calling with a new class does not affect any already-instantiated instances of the old class; they are left unchanged. However, newly created instances will be of the new class type.

/**
* @license
* Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
const NativeHTMLElement = window.HTMLElement;
const nativeDefine = window.customElements.define;
const nativeGet = window.customElements.get;
const tagnameByConstructor = new WeakMap();
const constructorByTagname = new Map();
const callbacksByTagName = new Map();
let constructingCtor;
// User extends this HTMLElement, which supers to the native HTMLElement
// to construct the element, and then immediately swizzles the prototype
// to the user prototype so that constructors going back up the super
// chain see the correct type
window.HTMLElement = function HTMLElement() {
let instance = Reflect.construct(NativeHTMLElement, [], this.constructor);
// Swizzle prototype to the user's version before returning, so that the
// user constructors are working on the correct prototype
Object.setPrototypeOf(instance, constructingCtor.prototype);
constructingCtor = undefined;
return instance;
}
window.HTMLElement.prototype = NativeHTMLElement.prototype;
// Defines a stand-in element that delegates out to the user's class using Reflect.construct
const define = (tagname, elementClass) => {
// Each time define is called, we save/overwrite the class/callbacks in maps by tagName
tagnameByConstructor.set(elementClass, tagname);
constructorByTagname.set(tagname, elementClass);
const attributeChangedCallback = elementClass.prototype.attributeChangedCallback;
const observedAttributes = new Set(elementClass.observedAttributes || []);
callbacksByTagName.set(tagname, {
connectedCallback: elementClass.prototype.connectedCallback,
disconnectedCallback: elementClass.prototype.disconnectedCallback,
adoptedCallback: elementClass.prototype.adoptedCallback,
attributeChangedCallback,
observedAttributes
});
// Since we can't change observedAttributes, we approximate it by patching
// set/removeAttribute on the user's class
if (observedAttributes.size && attributeChangedCallback) {
const setAttribute = elementClass.prototype.setAttribute;
if (setAttribute) {
elementClass.prototype.setAttribute = function(name, value) {
if (observedAttributes.has(name)) {
const old = this.getAttribute(name);
setAttribute.call(this, name, value);
attributeChangedCallback.call(this, name, old, value);
} else {
setAttribute.call(this, name, value);
}
};
}
const removeAttribute = elementClass.prototype.removeAttribute;
if (removeAttribute) {
elementClass.prototype.removeAttribute = function(name) {
if (observedAttributes.has(name)) {
const old = this.getAttribute(name);
removeAttribute.call(this, name);
attributeChangedCallback.call(this, name, old, value);
} else {
removeAttribute.call(this, name);
}
};
}
}
// Nothing below should reference `elementClass`; the ctor/prototype should
// be referenced via the tagname maps
const existingClass = nativeGet.call(window.customElements, tagname);
if (!existingClass) {
const StandInElement = class {
constructor() {
// Delegate to the current user class for `tagName`
const ctor = constructingCtor = constructorByTagname.get(tagname);
const instance = Reflect.construct(ctor, [], this.constructor);
// Approximate observedAttributes from the user class, since the stand-in element had none
callbacksByTagName.get(tagname).observedAttributes.forEach(attr => {
if (instance.hasAttribute(attr)) {
instance.attributeChangedCallback(attr, null, instance.getAttribute(attr));
}
});
return instance;
}
// The following callbacks will be torn off of the class and only ever
// seen by customElements; when the constructor runs the prototype will
// be swizzled with the user prototype and any callbacks thereof
connectedCallback() {
const cb = callbacksByTagName.get(this.localName).connectedCallback;
cb && cb.apply(this, arguments);
}
disconnectedCallback() {
const cb = callbacksByTagName.get(this.localName).disconnectedCallback;
cb && cb.apply(this, arguments);
}
adoptedCallback() {
const cb = callbacksByTagName.get(this.localName).adoptedCallback;
cb && cb.apply(this, arguments);
}
// no attributeChangedCallback or observedAttributes since these
// are simulated via setAttribute/removeAttribute patches
};
nativeDefine.call(window.customElements, tagname, StandInElement);
}
};
const get = (tagname) => constructorByTagname.get(tagname);
// Workaround for Safari bug where patching customElements can be lost, likely
// due to native wrapper garbage collection issue
Object.defineProperty(window, 'customElements',
{value: window.customElements, configurable: true, writable: true});
Object.defineProperty(window.customElements, 'define',
{value: define, configurable: true, writable: true});
Object.defineProperty(window.customElements, 'get',
{value: get, configurable: true, writable: true});
// Safari 10 inexplicably throws `TypeError` intermittently when assigning to
// `constructor` when extending from HTMLElement via ES5-compiled class code.
if (navigator.userAgent.match(/Version\/(10\..*|11\.0\..*)Safari/)) {
const ctor = HTMLElement.prototype.constructor;
Object.defineProperty(HTMLElement.prototype, 'constructor', {
configurable: true,
get() { return ctor; },
set(value) {
// Here `this` will be the extended prototype that was assigned to;
// after this call the new value on the prototype will shadow this
// accessor on HTMLElement.prototype
Object.defineProperty(this, 'constructor',
{value, configurable: true, writable: true});
}
});
}
<script type="module">
import './patchable-custom-elements-registry.js';
import {LitElement, html, css} from '../../../lit-element/lit-element.js';
customElements.define('my-element', class extends LitElement {
static get styles() {
return css`
:host {
display: inline-block;
border: 1px solid red;
padding 5px;
margin: 5px;
background: pink;
}
`
}
static get properties() {
return {
foo: { type: String },
bar: { type: String }
};
}
constructor() {
super();
this.foo = 'foo';
this.bar = 'bar';
}
render() {
return html`
This is a test: ${this.foo} / ${this.bar}
`;
}
});
window.onload = function() {
const Sup = customElements.get('my-element');
customElements.define('my-element', class extends Sup {
static get styles() {
return [Sup.styles, css`
:host {
border: 1px solid green;
background: lightgreen;
}
`]
}
constructor() {
super();
this.foo = 'FOO';
}
});
let el = document.createElement('my-element');
document.body.appendChild(el);
el.setAttribute('bar', 'BAR')
customElements.define('my-element', class extends LitElement {
static get styles() {
return css`
:host {
display: inline-block;
border: 1px solid orange;
padding 5px;
margin: 5px;
background: yellow;
}
`;
}
static get properties() {
return {
ziz: { type: String },
zot: { type: String }
};
}
constructor() {
super();
this.ziz = 'ziz';
this.zot = 'zot';
}
render() {
return html`
Totally different: ${this.ziz} / ${this.zot}
`;
}
});
el = document.createElement('my-element');
document.body.appendChild(el);
el = document.createElement('my-element');
document.body.appendChild(el);
el.setAttribute('foo', 'does nothing');
el.setAttribute('bar', 'does nothing');
el.setAttribute('ziz', 'ZIZ')
el.setAttribute('zot', 'ZOT')
};
</script>
<my-element></my-element>
<my-element foo="Foo"></my-element>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment