Stencil: Question: How to extend elements

Created on 24 Aug 2017  路  4Comments  路  Source: ionic-team/stencil

Per the Custom Elements spec, it is possible to extend native elements.

I am wondering how to do this using stencil. A specific use case would be if I wanted to create a specific select component (call it yes-no-select) and encapsulate the options. Using plain old custom elements, I would automatically be able to inherit all of the select options using the following code:

(function(){

    customElements.define('yes-no-select', class extends HTMLSelectElement{
        constructor(){super()}

        options(selected) {
            return [
                {
                    name: '',
                    value: ''
                },
                {
                    name: 'Yes',
                    value: true
                },
                {
                    name: 'No',
                    value: false
                }].map(row => {

                return selected != undefined && Boolean(selected) === row.value ? `<option value="${row.value}" selected>${row.name}</option>` : `<option value="${row.value}">${row.name}</option>`
            }).join('')
        }

        connectedCallback() {
            this.setAttribute('style', 'display: block')
            this.innerHTML =  `${this.options(this.getAttribute('selected-value'))}`
        }
    }, {extends: 'select'})

})()

I am wondering how this would translate.

Most helpful comment

Hi there, sorry for commenting on a 1+ year old comment :) but this functionality is still not there.

I came across this blog post (https://hackernoon.com/extending-built-in-elements-9dce404b75b4) where the author suggests to use https://github.com/WebReflection/built-in-element/#built-in-element as a polyfill for Safari and others. It's very lightweight, so I was wondering if it could be included as part of the Stencil toolkit? Here's the demo: https://webreflection.github.io/built-in-element/test/es5/

Having said that, my current pain point is that I need to migrate an existing component library (which in this particular scenario means I don't have a lot of choice on the decisions made on the original component library), where I have components that implement a single html element, like <ti-link> which renders an <a>.

This means that if I want the <a> to have all the default attributes (and I can't use the <a> tag directly), then I need to replicate all the a attributes in the ti-link component. Having <a is="ti-link"> would solve my problems, but since it's not there I've come up with an alternative solution that I'd like to share with you.

I've created this helper function in a module:

export function cloneAttributes<T = HTMLAttributes | NamedNodeMap>(el: { attributes: HTMLAttributes | NamedNodeMap }) {
    return Object.values(<T>el.attributes).reduce((acc: any, attr: Attr): T => {
        acc[attr.name] = attr.value;
        return acc;
    }, {});
}

and it's used inside a component like this:

import {Component, Element, JSXElements} from "@stencil/core";
import {cloneAttributes} from "../../ti-component";
import ImgHTMLAttributes = JSXElements.ImgHTMLAttributes;

@Component({
    tag: 'ti-img',
    styleUrl: 'img.scss'
})
export class Img {
    @Element() el: HTMLImageElement;

    render() {
        return <img {...cloneAttributes<ImgHTMLAttributes<HTMLImageElement>>(this.el)}/>;
    }
}

All 4 comments

Hello! Thanks for using Stencil! While the spec does call for allowing you to extend native elements, like button, this has not been implemented in browsers as the webkit/Safari team had some concerns around this. So for now, you can only extend HTMLElement, which the web components that stencil generates do extend.

Hi there, sorry for commenting on a 1+ year old comment :) but this functionality is still not there.

I came across this blog post (https://hackernoon.com/extending-built-in-elements-9dce404b75b4) where the author suggests to use https://github.com/WebReflection/built-in-element/#built-in-element as a polyfill for Safari and others. It's very lightweight, so I was wondering if it could be included as part of the Stencil toolkit? Here's the demo: https://webreflection.github.io/built-in-element/test/es5/

Having said that, my current pain point is that I need to migrate an existing component library (which in this particular scenario means I don't have a lot of choice on the decisions made on the original component library), where I have components that implement a single html element, like <ti-link> which renders an <a>.

This means that if I want the <a> to have all the default attributes (and I can't use the <a> tag directly), then I need to replicate all the a attributes in the ti-link component. Having <a is="ti-link"> would solve my problems, but since it's not there I've come up with an alternative solution that I'd like to share with you.

I've created this helper function in a module:

export function cloneAttributes<T = HTMLAttributes | NamedNodeMap>(el: { attributes: HTMLAttributes | NamedNodeMap }) {
    return Object.values(<T>el.attributes).reduce((acc: any, attr: Attr): T => {
        acc[attr.name] = attr.value;
        return acc;
    }, {});
}

and it's used inside a component like this:

import {Component, Element, JSXElements} from "@stencil/core";
import {cloneAttributes} from "../../ti-component";
import ImgHTMLAttributes = JSXElements.ImgHTMLAttributes;

@Component({
    tag: 'ti-img',
    styleUrl: 'img.scss'
})
export class Img {
    @Element() el: HTMLImageElement;

    render() {
        return <img {...cloneAttributes<ImgHTMLAttributes<HTMLImageElement>>(this.el)}/>;
    }
}

Once again, sorry for the zombie resurrection, but there does not appear to have been any progress on this issue. Wrapping native HTML elements without loss of functionality is a super common issue, so it's a little baffling to me that this hasn't seen more action.

My question is for @fsodano - how does your solution play with accessibility? For example, if I want to use this technique to create a <my-select id="mySelect">, and I want to use a <label for="mySelect">, this will not actually work to pass-through focus to the underlying <select> element. Correct? I would need to manually pass-through the id, since it must be unique. I believe other a11y properties would be problematic for similar reasons.

Have you encountered this? And have you found a clean solution?

In your case, I would place the label inside the <my-select> component, this should be part of your components API, and everything should be sorted out inside .

But, it's been a while since I've used Stencil since I couldn't get around this issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

noahlaux picture noahlaux  路  3Comments

guidoknoll picture guidoknoll  路  3Comments

kensodemann picture kensodemann  路  3Comments

mitchellsimoens picture mitchellsimoens  路  3Comments

romulocintra picture romulocintra  路  3Comments