Due to the renaming of some class methods (attached/detached to connected/disconnected) and removal of createdCallback in favor of constructors (which is a good thing!), I find myself writing my WebComponent base class (class-factory mixin) as follows.
My question is, should I be doing what I'm doing, or do you recommend something else? Note that the attached/detachedCallback methods simply call the connected/disconnectedCallback methods, and also note what I've done with createdCallback
and the constructor
due to the differences between v0 and v1. Subclasses of my WebComponent base class still use createdCallback
rather than a constructor in order to be backwards compatible.
The code is as follows, with parts removed for brevity in order to show just the parts dealing with the v0/v1 APIs directly and how backwards compatibility is maintained:
// Very very stupid hack needed for Safari in order for us to be able to extend
// the HTMLElement class. See:
// https://github.com/google/traceur-compiler/issues/1709
if (typeof window.HTMLElement != 'function') {
const _HTMLElement = function HTMLElement(){}
_HTMLElement.prototype = window.HTMLElement.prototype
window.HTMLElement = _HTMLElement
}
// XXX: Maybe we can improve by clearing items after X amount of time?
const classCache = new Map
function hasHTMLElementPrototype(constructor) {
if (!constructor) return false
if (constructor === HTMLElement) return true
else return hasHTMLElementPrototype(constructor.prototype)
}
/**
* Creates a WebComponent base class dynamically, depending on which
* HTMLElement class you want it to extend from. Extend from WebComponent when
* making a new Custom Element class.
*
* @example
* import WebComponent from './WebComponent'
* class AwesomeButton extends WebComponent(HTMLButtonElement) { ... }
*
* @param {Function} elementClass The class that the generated WebComponent
* base class will extend from.
*/
export default
function WebComponentMixin(elementClass) {
if (!elementClass) elementClass = HTMLElement
if (!hasHTMLElementPrototype(elementClass)) {
throw new TypeError(
'The argument to WebComponentMixin must be a constructor that extends from or is HTMLElement.'
)
}
// if a base class that extends the given `elementClass` has already been
// created, return it.
if (classCache.has(elementClass))
return classCache.get(elementClass)
// otherwise, create it.
class WebComponent extends elementClass {
// constructor() is used in v1 Custom Elements instead of
// createdCallback() as in v0.
constructor() {
super()
// If the following is true, then we know the user should be using
// `document.registerElement()` to define an element from this class.
// `document.registerElement()` creates a new constructor, so if the
// constructor here is being called then that means the user is not
// instantiating a DOM HTMLElement as expected because it is required
// that the constructor returned from `document.registerElement` be used
// instead (this is a flaw of Custom Elements v0 which is fixed in v1
// where class constructors can be used directly).
if (document.registerElement && !customElements.define) {
// TODO: link to docs.
throw new Error(`
You cannot instantiate this class directly without first registering it
with \`document.registerElement(...)\`. See an example at http://....
`)
}
// Throw an error if no Custom Elements API exists.
if (!document.registerElement && !customElements.define) {
// TODO: link to docs.
throw new Error(`
Your browser does not support the Custom Elements API. You'll
need to install a polyfill. See how at http://....
`)
}
// otherwise the V1 API exists, so call the createdCallback, which
// is what Custom Elements v0 would call by default. Subclasses of
// WebComponent should put instantiation logic in createdCallback
// instead of in a custom constructor if backwards compatibility is
// to be maintained.
this.createdCallback()
}
createdCallback() {
// code removed for brevity...
}
connectedCallback() {
// code removed for brevity...
}
attachedCallback() { this.connectedCallback() } // back-compat
disconnectedCallback() {
// code removed for brevity...
}
detachedCallback() { this.disconnectedCallback() } // back-compat
}
classCache.set(elementClass, WebComponent)
return WebComponent
}
Any thoughts?