DEV Community

Matt Kenefick
Matt Kenefick

Posted on

Revisiting Shadow DOM: Nested items, dynamic templates, shadowRoot

In another article, I discussed some basics of Shadow DOM, parsing class names, and autoloading components. You can find that article here.

For this next example, you can pull a Working GitHub Example and try it out. It doesn’t require any dependencies or special servers to run. All relative paths from the index.html.

Demo: https://mattkenefick.github.io/sample-shadow-dom/

Example

Here’s a quick breakdown of the architecture for this demo. It uses our classic index.html, image, style, script format, with the addition of view.

File Tree

Most of the files here are pretty basic. For instance, script/component/my-form.js and script/component/my-input.js don’t have explicit functionality of their own for this example; they only extend the script/component/base.js.

Some are provide core functionality, like script/component/base.js and script/main.js.

The separation of my-form.css and my-input.html into their own respective folders are designed that way for demonstrative purposes. In a real application, you’d likely choose a direction and stick with it rather than the mix and match we have here.

Update base.js to accept dynamic templates

We’ve added an important method to our base.js file which allows us to remotely fetch a file, convert it to template, then attach it as we were before. When I mention “before”, I’m referring to this tutorial.

/**
 * Attempt to attach template over the network.
 * It attempts to derive an HTML tag from the filename,
 * but we could do anything here.
 *
 * @param string filename
 */
static async attachRemote(filename) {
    const filenameMatches = filename.match(/\/([^\.\/]+)\.html/i);

    if (filenameMatches) {
        const id = filenameMatches[1];
        const response = await fetch(filename);
        const text = await response.text();
        const fragment = document.createElement('template');
        fragment.innerHTML = text;
        fragment.id = id;

        this.attach(fragment);
    }
}
Enter fullscreen mode Exit fullscreen mode

This function makes an assumption that your desired HTML tag name will match the file you’re request, i.e. view/component/my-tag.html will be renderable as <my-tag>. You can see this functionality under filenameMatches and how it associates with the fragment.id section.

You can change this however you want, but the gist is that whatever you set for the id will be your tag name.

--

<!DOCTYPE html>
<html>
    <head>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;700&display=swap">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" />

        <style>
            body {
                background-color: #260C1A;
                color: #fff;
                font-family: Ubuntu, Helvetica, Arial, sans-serif;
            }
        </style>
    </head>
    <body>
        <main>
            <!--
                Include our `my-form` element that includes a <slot> which
                allows us to nest additional shadow-dom elements
            -->
            <my-form>
                <my-input></my-input>
            </my-form>

            <hr />

            <!--
                Include the dynamic `my-input` element outside of
                other shadow-dom elements
            -->
            <my-input></my-input>
        </main>

        <!--
            Here we include templates directly on the DOM so we can pick them up
            through our autoloader. It's fine for testing, but can make a mess.
            This version links to an external CSS file, where as our other
            example uses a directly included <style> tag.
        -->
        <template id="my-form">
            <link rel="stylesheet" href="./style/component/my-form.css" />

            <form class="my-form">
                <fieldset>
                    <legend>My Form Element</legend>
                    <slot></slot>
                </fieldset>
            </form>
        </template>

        <!--
            Initialize Application
        -->
        <script src="./script/main.js" type="module"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

In this demo, you can see that explicitly define the my-form component but we also use a my-input. The my-input template is dynamically fetched from within our main.js file using the command:

MyInputElement.attachRemote('../view/component/my-input.html');
Enter fullscreen mode Exit fullscreen mode

You can see from our index.html above that we’re able to easily nest custom elements within one another, but also use them separately in the same page.

Also note how the my-form template defined above uses the link tag to reference an existing CSS file. Our ShadowDOM elements are scoped so you’ll want to directly define styles within the templates or share the styles from another source.

--

I recommend you pull down the GitHub example and tinker with it. You can combine different ways of dynamically loading vs locally loading, referencing css files vs defining styles, and nesting components.

Top comments (0)