Revisiting Shadow DOM: Nested items, dynamic templates, shadowRoot
Matt Kenefick
Posted on August 12, 2021
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/
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
.
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);
}
}
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>
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');
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.
Posted on August 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.