Skip to content

Element Design #2

@digitalsadhu

Description

@digitalsadhu

Working with custom elements brings challenges that don't exist in the React world with regards to designing our components. This issue uses our breadcrumbs component to illustrate this.

The challenge of shadowdom, slots and Fabric/Tailwind

Consider the following component structure that uses Shadow DOM and slots:

<nav class="flex space-x-8"><slot></slot></nav>

Which is to be used like so:

<fabric-breadcrumbs>
    <a href="#/url/1">Eiendom</a>
    <a href="#/url/2">Bolig til salgs</a>
    <a href="#/url/3" aria-current="page">Oslo</a>
</fabric-breadcrumbs>

When all is said and done, the nav and slot element are in the shadowdom and therefore subject to any internal styles we use while the a tags are not in shadowdom and will be styled with the rest of the page as usual. The actual fabric-breadcrumbs element too, unlike react, IS an actual element on the page. IE. the component itself is not replaced with the root element (nav) as in the case with React. This means that to get the actual styled effect we want, the styles can't be on the nav element and instead must be on the fabric-breadcrumbs element like so:

<fabric-breadcrumbs class="flex space-x-8">
    <a href="#/url/1">Eiendom</a>
    <a href="#/url/2">Bolig til salgs</a>
    <a href="#/url/3" aria-current="page">Oslo</a>
</fabric-breadcrumbs>

Solutions

There are some design challenges implicit in trying to build such a web component to match the React/Vue implementations. There are a plethora of solutions to the challenge presented above, however, all feel like compromises.

Option 1 - Styling or class manipulation from within

One option is to try to style the component internally using special selectors ::slotted could be used to place styles on the a tags and :root could be used to place styles on the fabric-element itself.

It would also be possible to manipulate the classes on the fabric-element from within such that Tailwind could work.

Theres no shadow encapsulation available for the CSS in this approach and it would have to be either inlined style rules or reliance on an external Fabric style sheet. Far from what we are aiming for.

Option 2 - place the child elements within the shadowdom

It is also possible (P.O.C. example attached) to cut the child elements (the a tags) and paste them (using JS) into the shadowdom under the nav thereby subjecting the a tags to the same context as the nav and making internally loaded Tailwind/Fabric available. This makes the flicker effect of WCs worse but they need to be dealt with anyway.

Example implementation (without slots)

Content passed in is moved into a created Shadow DOM and / dividers are interleaved with the provided content.

export class FabricBreadcrumbs extends HTMLElement {
    connectedCallback() {
        const children = Array.from(this.children)
            .map((child) => child.outerHTML)
            .join('<span class="select-none" aria-hidden>/</span>');
        this.innerHTML = '';
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
            <link
                rel="stylesheet"
                type="text/css"
                href="https://assets.finn.no/pkg/@finn-no/fabric-css/v0/fabric.min.css"
            />
            <nav class="flex space-x-8">${children}</nav>
        `;
    }
}

Option 3. - no shadowdom

Don't use shadowdom at all and let styling happen externally. Straight forward though we'd lose the encapsulation and the Fabric stylesheet would have to be loaded alongside the component for it to work.

Example implementation

No shadow DOM is used at all. Provided children are interleaved with / spans.

export class FabricBreadcrumbs extends HTMLElement {
    connectedCallback() {
        const children = Array.from(this.children)
            .map((child) => child.outerHTML)
            .join('<span class="select-none" aria-hidden>/</span>');

        this.innerHTML = `<nav class="flex space-x-8">${children}</nav>`;
    }
}

Option 4. - data passed in as attributes

We could pass data in as attributes. These must be passed a string due to how Web Components work which is very clunky but would I think solve all issues.
eg.

<f-breadcrumbs data='[["Eiendom","#/url/1"],["Bolig til salgs","#/url/2"],["Oslo","#/url/3"]]'></f-breadcrumbs>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions