Revisiting Shadow DOM: Nested items, dynamic templates, shadowRoot
Tags: beginners, javascript, html, webdev
Published: 2021-08-12 14:17:31

In another article, I discussed some basics of Shadow DOM, parsing class names, and autoloading components. You can find that [article here](https://medium.com/@mattkenefick/snippets-in-javascript-converting-pascalcase-to-kebab-case-426c80672abc).

For this next example, you can pull a [Working GitHub Example](https://github.com/mattkenefick/sample-shadow-dom) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mayfbawfr838on4kvwo8.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pt9zy1qh2pomtsjgw3ue.png)

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](https://medium.com/@mattkenefick/snippets-in-javascript-converting-pascalcase-to-kebab-case-426c80672abc).

```javascript
/**
 * 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);
    }
}
```

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.

--

```html
<!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>
```

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:

```javascript
MyInputElement.attachRemote('../view/component/my-input.html');
```

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](https://github.com/mattkenefick/sample-shadow-dom) and tinker with it. You can combine different ways of dynamically loading vs locally loading, referencing css files vs defining styles, and nesting components.