W3C home > Mailing lists > Public > public-webapps@w3.org > July to September 2016

Re: [Custom Elements] Should I write v1 Custom Elements in a way backwards compatible with v0 API?

From: Dominic Cooney <dominicc@google.com>
Date: Mon, 22 Aug 2016 18:06:56 +0900
Message-ID: <CAHnmYQ9YbRzqzoOx=O7zshGPQ7KYcQ2CByzZ2c2zcHPRL-+HVw@mail.gmail.com>
To: "/#!/JoePea" <trusktr@gmail.com>
Cc: public-webapps WG <public-webapps@w3.org>
I implemented custom elements in Chrome (twice.) This looks reasonable to
me.

The exact timing of createdCallback and constructor running, and errors
around element creation, are different. If authors stick to the
restrictions of custom elements "v1" they should be fine, because they're
more restrictive than v0.

On Sun, Aug 21, 2016 at 11:13 AM, /#!/JoePea <trusktr@gmail.com> wrote:

> 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)
>     }
>

I'm not sure this is right. Would it be simpler anyway to just say
constructor && (constructor === HTMLElement || constructor.prototype
instanceof HTMLElement)?


>     /**
>      * 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
>

I like the way you have the old callback invoke the new one. Chrome will
remove document.registerElement at some point and all this can be
jettisoned; having authors implement the new callbacks will make that
process easier.


>             disconnectedCallback() {
>                 // code removed for brevity...
>             }
>             detachedCallback() { this.disconnectedCallback() } //
> back-compat
>         }
>
>         classCache.set(elementClass, WebComponent)
>         return WebComponent
>     }
>
> Any thoughts?
>
>
> */#!/*JoePea
>
Received on Monday, 22 August 2016 09:07:26 UTC

This archive was generated by hypermail 2.3.1 : Friday, 27 October 2017 07:27:40 UTC