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

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

From: /#!/JoePea <trusktr@gmail.com>
Date: Sat, 20 Aug 2016 19:13:29 -0700
Message-ID: <CAKU1PAU0nPgADSdzsWUrKs4KXezXo-VD4_f190BMEnr8M7wh5g@mail.gmail.com>
To: public-webapps WG <public-webapps@w3.org>
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?


*/#!/*JoePea
Received on Sunday, 21 August 2016 02:14:42 UTC

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