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.
-
-
Save kevinpschaaf/9f8d8fc238a09656b8bad7f7b062a2fd to your computer and use it in GitHub Desktop.
Patchable CustomElementsRegistry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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}); | |
} | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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