Search code examples
hyperhtml

How to create a HyperHTML custom element extended from HTMLButtonElement


I would like to have a semantic named custom element that extends from button: like fab-button

class FabButton extends HTMLButtonElement {
    constructor() {
        super();
        this.html = hyperHTML.bind(this);
    }
}
customElements.define("fab-button", FabButton);

Extending for HTMLButtonElement doesn't seem to work.

Is there a way to extend from a non-HTMLElement with the HyperHTML "document-register-element.js"?

Codepen example: https://codepen.io/jovdb/pen/qoRare


Solution

  • It's difficult to answer this, because it's tough to understand where to start from.

    The TL;DR solution though, is here, but it needs a lot of explanations.

    Extending built-ins is a ghost in the Web specs

    It doesn't matter what WHATWG says, built-ins are a de-facto death specification because Webkit strongly opposed to it and Firefox, as well as Edge, that never even shipped Custom Elements, didn't push to have them neither.

    Accordingly, as starting point, it's discouraged to extend built-ins with Custom Elements V1 specification.

    You might have luck with V0 though, but that's only Chrome API and it's already one of those APIs created to die (R.I.P. WebSQL).

    My polyfill follows specs by definition

    The document-register-element polyfill, born with V0 but revamped with V1, follows specifications as close as possible, which is why it makes extending built-ins possible, because WHATWG still has that part in.

    That also means you need to understand how extending built-ins works.

    It is not by defining a simple class that extends HTMLButtonElement that you get a button, you need to do at least three extra things:

    // define via the whole signature
    customElements.define(
      "fab-button",
      FabButton,
      {extends: 'button'}
    );
    

    ... but also ... allow the polyfill to work

    // constructor might receive an instance
    // and such instance is the upgraded one
    constructor(...args) {
        const self = super(...args);
        self.html = hyperHTML.bind(self);
        return self;
    }
    

    and most important, its most basic representation on the page would be like this

    <button is="fab-button">+</button>
    

    Bear in mind, with ES6 classes super(...args) would always return the current context. It's there to grant super constructors didn't return other instances as upgraded objects.

    The constructor is not your friend

    As harsh as it sounds, Custom Elements constructors work only with Shadow DOM and addEventListener, but nothing else, really.

    When an element is created is not necessarily upgraded yet. in fact, it won't be, most likely, upgraded.

    There are at least 2 bugs filed to every browser and 3 inconsistent behaviors about Custom Elements initialization, but your best bet is that once connectedCallback is invoked, you really have the custom element node content.

    connectedCallback() {
      this.slots = [...this.childNodes];
      this.render();
    }
    

    In that way you are sure you actually render the component once live on the DOM, and not when there is not even a content.

    I hope I've answered your question in a way that also warns you to go away from custom elements built-ins if that's the beginning of a new project: they are unfortunately not a safe bet for the future of your application.

    Best Regards.