File loading please wait...
Citation preview
Apress Pocket Guides
press Pocket Guides present concise summaries of cutting-edge A developments and working practices throughout the tech industry. Shorter in length, books in this series aims to deliver quick-to-read guides that are easy to absorb, perfect for the time-poor professional. This series covers the full spectrum of topics relevant to the modern industry, from security, AI, machine learning, cloud computing, web development, product design, to programming techniques and business topics too. Typical topics might include: •
A concise guide to a particular topic, method, function or framework
•
Professional best practices and industry trends
•
A snapshot of a hot or emerging topic
•
Industry case studies
•
Concise presentations of core concepts suited for students and those interested in entering the tech industry
•
Short reference guides outlining ‘need-to-know’ concepts and practices
More information about this series at https://link.springer.com/ bookseries/17385.
Beginning Shadow DOM API Get Up and Running with Shadow DOM for Web Applications
Alex Libby
Beginning Shadow DOM API: Get Up and Running with Shadow DOM for Web Applications Alex Libby Belper, Derbyshire, UK ISBN-13 (pbk): 979-8-8688-0248-5 https://doi.org/10.1007/979-8-8688-0249-2
ISBN-13 (electronic): 979-8-8688-0249-2
Copyright © 2024 by Alex Libby This work is subject to copyright. All rights are reserved by the Publisher, whether the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physical way, and transmission or information storage and retrieval, electronic adaptation, computer software, or by similar or dissimilar methodology now known or hereafter developed. Trademarked names, logos, and images may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, logo, or image we use the names, logos, and images only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. The use in this publication of trade names, trademarks, service marks, and similar terms, even if they are not identified as such, is not to be taken as an expression of opinion as to whether or not they are subject to proprietary rights. While the advice and information in this book are believed to be true and accurate at the date of publication, neither the authors nor the editors nor the publisher can accept any legal responsibility for any errors or omissions that may be made. The publisher makes no warranty, express or implied, with respect to the material contained herein. Managing Director, Apress Media LLC: Welmoed Spahr Acquisitions Editor: James Robinson Prior Development Editor: James Markham Editorial Assistant: Gryffin Winkler Cover designed by eStudioCalamar Distributed to the book trade worldwide by Springer Science+Business Media New York, 1 New York Plaza, Suite 4600, New York, NY 10004-1562, USA. Phone 1-800-SPRINGER, fax (201) 348-4505, e-mail [email protected], or visit www.springeronline.com. Apress Media, LLC is a California LLC and the sole member (owner) is Springer Science + Business Media Finance Inc (SSBM Finance Inc). SSBM Finance Inc is a Delaware corporation. For information on translations, please e-mail [email protected]; for reprint, paperback, or audio rights, please e-mail [email protected]. Apress titles may be purchased in bulk for academic, corporate, or promotional use. eBook versions and licenses are also available for most titles. For more information, reference our Print and eBook Bulk Sales web page at http://www.apress.com/bulk-sales. Any source code or other supplementary material referenced by the author in this book is available to readers on GitHub. For more detailed information, please visit https://www.apress. com/gp/services/source-code. Paper in this product is recyclable
This is dedicated to my family, with thanks for their love and support while writing this book.
Table of Contents About the Author���������������������������������������������������������������������������������xi About the Technical Reviewer�����������������������������������������������������������xiii Acknowledgments������������������������������������������������������������������������������xv Introduction��������������������������������������������������������������������������������������xvii Chapter 1: Getting Started��������������������������������������������������������������������1 Exploring the Different DOM Types�����������������������������������������������������������������������2 Creating a Simple Example�����������������������������������������������������������������������������������3 Understanding What Happened�����������������������������������������������������������������������9 Considering the Bigger Picture����������������������������������������������������������������������15 Understanding Terminology Around the API��������������������������������������������������������16 Creating the Component – A Process������������������������������������������������������������18 Browser Support for the API�������������������������������������������������������������������������������19 Practical Considerations for using the API����������������������������������������������������������21 Composition Using Slots and Templates�������������������������������������������������������������24 Breaking Apart the Code Changes�����������������������������������������������������������������28 Using Slots and Templates – A Postscript�����������������������������������������������������30 Applying Styles – What’s Different?��������������������������������������������������������������������31 Summary������������������������������������������������������������������������������������������������������������33
vii
Table of Contents
Chapter 2: Creating Components��������������������������������������������������������35 Creating an Alert Dialog��������������������������������������������������������������������������������������36 Constructing the Markup�������������������������������������������������������������������������������36 Creating a Toggle Switch�������������������������������������������������������������������������������42 Constructing the Demo����������������������������������������������������������������������������������50 Breaking Apart the Code��������������������������������������������������������������������������������55 Testing the Components�������������������������������������������������������������������������������������57 Understanding What Happened���������������������������������������������������������������������60 Packaging the Component for Release���������������������������������������������������������������62 Breaking Apart the Code Changes�����������������������������������������������������������������70 Publishing the Component – A Postscript�����������������������������������������������������71 Summary������������������������������������������������������������������������������������������������������������72
Chapter 3: Applying Shadow DOM API in Apps����������������������������������75 Working with Styling Frameworks����������������������������������������������������������������������76 Exploring the Code Changes��������������������������������������������������������������������������80 Mounting a React App Using Shadow DOM API��������������������������������������������������83 Understanding What Happened���������������������������������������������������������������������89 Implications of SEO and the API��������������������������������������������������������������������������90 Comparing with Bing�������������������������������������������������������������������������������������93 Challenges with Server-Side Rendering – A Postscript���������������������������������95 Summary������������������������������������������������������������������������������������������������������������97
Chapter 4: Supporting Other Frameworks������������������������������������������99 Adding Shadow DOM API Support to React Components������������������������������������99 Breaking Apart the Code Changes���������������������������������������������������������������104 Exploring Other Framework Examples��������������������������������������������������������������106 First Example: Using Svelte�������������������������������������������������������������������������106
viii
Table of Contents
Second Example: Exploring Alpine.js�����������������������������������������������������������118 Assessing Performance of the API��������������������������������������������������������������������124 Will Web Components Replace Tools Such As React?���������������������������������������126 And Now for Something Completely Different…����������������������������������������������127 Summary����������������������������������������������������������������������������������������������������������128
Appendix: API Reference������������������������������������������������������������������129 Index�������������������������������������������������������������������������������������������������133
ix
About the Author Alex Libby is a front-end developer and seasoned computer book author, who hails from England. His passion for all things open source dates back to his student days, when he first came across web development, and he has been hooked ever since. His daily work involves extensive use of JavaScript, HTML, and CSS to manipulate existing website content. Alex enjoys tinkering with different open source libraries to see how they work. He has spent a stint maintaining the jQuery Tools library and enjoys writing about open source technologies, principally for front-end UI development.
xi
About the Technical Reviewer Kenneth Fukizi is a passionate software engineer, architect, and consultant with experience in programming on different tech stacks. Prior to dedicated software development, he worked as a lecturer and was then head of IT at different organizations. He has domain experience working with technology for companies mainly in the financial sector. When he’s not working, he likes reading up on emerging technologies and strives to be an active member of the software community. Kenneth currently leads a community of African developers, through a startup company called AfrikanCoder.
xiii
Acknowledgments Writing a book can be a long but rewarding process – I still remember starting to write my first book back in 2011, which seems such a long time ago! Whatever the size or nature of any book, it is not possible to complete it without the help of other people. To that end, I would like to offer a huge vote of thanks to my editors – in particular, Sowmya Thodur and James Robinson-Prior; my thanks also to Kenneth Fukizi as my technical reviewer, James Markham for his help during the process, and others at Apress for getting this book into print. All have made writing this book a painless and enjoyable process, even with the edits! My thanks also to my family for being understanding and supporting me while writing. I frequently spend a lot of late nights writing alone, or pass up times when I should be with them, so their words of encouragement and support have been a real help in getting past those bumps in the road and producing the finished book that you now hold in your hands.
xv
Introduction Beginning Shadow DOM API is for people who want to learn how to quickly implement the Shadow DOM API to create encapsulated components that maintain the styles we want to use, without the need for external tools such as React. This mini project-oriented book first examines what the Shadow DOM API is all about and the benefits of using it and then touches on where it fits into the landscape as a browser API. We’ll examine the various parts of the API and understand why it may not be necessary to use a heavyweight tool such as React when the emphasis is on speed and efficiency in any project. Throughout this book, I’ll take you on a journey through using the API, exploring how easy it is to create simple examples that we can extend and make more dynamic with the minimum of fuss. We’ll use all of the techniques we gain from exploring the API, to make a more complex component – I’ll show you the process from start to finish, including examples of how we can see it in action. It may not be production-ready from the get-go, but it will give us a good grounding to develop it into something more complex at a later date. We’ll even take a look at using the API in noncomponent code and explore how tools such as React can use the API too – should we still need to use them! Beginning Shadow DOM API uses nothing more than standard JavaScript, CSS, and HTML, three of the most powerful tools available for developers: you can enhance, extend, and configure your components as requirements dictate. With this API, the art of possible is only limited by the extent of your imagination and the power of JavaScript, HTML, and Node.js.
xvii
CHAPTER 1
Getting Started Coding is a messy business. Ouch. Why? We all work on projects – some for clients, some for personal development; chances are we need to apply fixes to our code at some point, then apply fixes on fixes. Very soon, things become messy and unmanageable – sound familiar? We need a different approach – one way to achieve this is to create components or modules that serve a single responsibility, such as a custom button. We can encapsulate modular code, so we don’t need to worry about how it all works inside: pass in the various properties and out spits a button that suits our needs! Sounds sensible, right? Typically, we might reach for React and knock out something suitable ... but hold on for a moment. What if I told you we could build using one framework but use it across multiple different frameworks? Yes, I’m talking about creating something in Svelte and using it in Svelte, React, Angular, Vue, and more. To do this, we need to create web components – these are not your typical standard components that you might create in something like React and only use in React. There is much more to it than that – let me introduce you to what makes it all possible: the Shadow DOM API! Throughout this book, we’ll look at this API, dig into a few examples, and understand how it helps us to create truly reusable components. Sure, they may have a few quirks compared to standard components, but that’s part of the fun – we’ll see why using this API means we don’t always have to use a sledgehammer to crack a nut.... © Alex Libby 2024 A. Libby, Beginning Shadow DOM API, Apress Pocket Guides, https://doi.org/10.1007/979-8-8688-0249-2_1
1
Chapter 1
Getting Started
Exploring the Different DOM Types Okay – so before I get too melodramatic (“dom, da, da, dom...” – sorry!), there’s something we need to cover first: What the heck is the Shadow DOM? As developers, we all know about the DOM (or “Real DOM”) – as the HTML tree of the website, we can access any element within this object using object dot notation. The trouble is it has to update when there’s been a change to the DOM; this can get very expensive if we have to make many changes!
You may also see references to Light DOM – this is the same thing as the Real DOM; it’s the DOM we know inside an HTML element. Can we improve on this? Enter the alternative – Virtual DOM. It forms the basis of most current frameworks, such as React. It compiles a copy of the real DOM into JavaScript before applying any changes to the copy. React then compares the copy to detect changes and update only those elements. Okay, as developers, we may be used to seeing the (Real) DOM and the Virtual DOM (when used in frameworks such as React). The thing is, how do they compare to the Shadow DOM? This is where things get a little different: instead of taking a copy of the real DOM into memory (which becomes the Virtual DOM), Shadow DOM allows us to add pockets of hidden DOM trees to elements in the real DOM tree. These are independent of each other – changes to one will not affect the other (although this might not be so true for styling, but we’ll come to that later in the book). Right – enough of the theory: let’s get down and dirty with some code! We’ll go through the various options available in the API shortly, but before we do so, let’s knock up a quick couple of examples so you can see the API in action. 2
Chapter 1
Getting Started
Creating a Simple Example Creating an instance of the Shadow DOM API is easier than it might initially seem. In addition to the code we want to use to add our feature or component, we only need to use the .attachShadow() method to initiate that instance. Once initialized, we can add whatever code we like and apply styles to our shadow element as necessary. To see what I mean, let’s look at a simple example using nothing more than vanilla JavaScript. For this demo, we will create the time-honored equivalent of the “Hello World” program that every developer starts with when they first start learning to code:
BUILDING AN EXAMPLE PART 1: VANILLA JS To create our demo, follow these steps: 1. First, head over to www.codepen.io, then hit Create ➤ Pen on the left.
I’ve used CodePen for convenience – there are a lot of ways to create this demo, which will work equally well. If you prefer to use a site such as CodeSandbox (www.codesandbox.io), or even build it locally, then please feel free to do so. 2. In the HTML section, go ahead and add this markup:
Hello World!
3
Chapter 1
Getting Started
3. Next, in the JS section, add this code: const existingElement = document.getElementById('foo'); const shadow = existingElement.attachShadow({mode: 'open'}); const message = document.createElement('p'); message.setAttribute('class', 'text'); message.textContent = 'Hello World!'; const styles = document.createElement('style'); styles.textContent = '.text { color: red };'; shadow.appendChild(styles); shadow.appendChild(message); 4. Finally, in the CSS section, add this: .body { margin: 10px } .text {font-weight: bold; text-transform: uppercase;} 5. CodePen will now show the results in the output window, as shown in Figure 1-1.
Figure 1-1. Example of using the Shadow DOM API (in vanilla JavaScript) 6. At face value, it looks like the same two pieces of text, albeit styled differently. The magic, though, is when we look at the Elements tab of the Developer Console in Chrome (or your browser’s equivalent) – try searching for div id="foo" as shown in Figure 1-2.
4
Chapter 1
Getting Started
Figure 1-2. An example of the Shadow DOM API in use Don’t worry if this doesn’t all make sense yet – we will go through this in more detail later! For now, though, the key message here is that you will see #shadow-root displayed in your code (as shown in Figure 1-2); if this is present, then we are using the API.
You can see an example of this demo in a CodePen at https:// codepen.io/Alex-Libby/pen/WNPNyeP; the code is also available in the download accompanying this book. Let’s move on and work through a quick example using a more modern framework – React. As the API forms the basis for creating web components, we will base it around a simple example so you can see what it would look like as an actual component (and not inline, as we did in the previous demo). For this next demo, I’ve elected to use Vite (www.vitejs.dev) as the basis for creating a placeholder site for our example. If you’ve not used it before, it’s a great tool for creating a basic front-end skeleton of a React site, with automatic browser reloading, quick boot, and folder structure for adding content when ready. You may have heard about create-react-app – Vite is very similar and is often seen as a more up-to-date replacement for create-react-app. 5
Chapter 1
Getting Started
BUILDING AN EXAMPLE PART 2: REACT To set up our React example, follow these steps: 1. First, fire up a Node.js terminal; then at the prompt, run this command: $ npx create vite@latest 2. npm will prompt you if Vite is not installed; select yes to install it if needed. 3. Next, Vite will prompt for some information – when prompted, use the responses highlighted here: √ Project name: » shadow-dom-react √ Select a framework: » React √ Select a variant: » JavaScript + SWC 4. Once completed, you will see messages similar to these appear – run the steps as outlined: Scaffolding project in C:\shadow-dom-react... Done. Now run: cd shadow-dom-react npm install npm run dev 5. Vite will fire up its development server – if all is well, you should see this appear: VITE v4.4.11 ready in 347 ms ➜ Local: http://127.0.0.1:5173/ ➜ Network: use --host to expose ➜ press h to show help 6
Chapter 1
Getting Started
6. We now need to add some code – first, go ahead and add a folder called components under \src. Inside the \src\ components folder, create a new file, then add this code: body { font-family: sans-serif; } 7. Save the file as my-customized-text.css. Next, create a new file in the same \src\components folder – this will contain the code for our component. Add this code, saving it at mycustomized-text.js: import "./my-customized-text.css"; class FancyElement extends HTMLElement { constructor() { super(); this.ShadowRoot = this.attachShadow({ mode: "open" }); this.ShadowRoot.innerHTML = "Shadow DOM imperative"; } } customElements.get("imperative-fancy-element") || customElements.define("imperative-fancy-element", FancyElement); 8. We now need to make some changes to the existing files in our “site” – first, crack open App.jsx and replace the entire contents with this: function App() { return ( 7
Chapter 1
Getting Started
Imperative Shadow DOM ); } export default App; 9. Next, open index.html at the root of the project, and look for the line starting with new CustomEvent(CHANGED, { detail: { checked }, }); 43
Chapter 2
Creating Components
3. Next, leave a line blank, then add this block – this creates the styling (and a placeholder) for our switch component: const template = document.createElement("template"); template.innerHTML = `
`; 4. Leave a link blank after the template block, then add this code – this is the core part of the switch component, starting with the opening constructor: class ToggleSwitch extends HTMLElement { static elementName = "toggle-switch"; // Currently, only Chrome adequately supports this part of the spec static formAssociated = true; static get observedAttributes() { return [CHECKED_ATTR]; } constructor() { super(); this.attachShadow({ mode: "open" }).appendChild( template.content.cloneNode(true) ); } 45
Chapter 2
Creating Components
5. This next block takes care of checking for the presence of role and tab index, then adds one of each if it’s not already present. At the same time, we also add two event handlers: connectedCallback() { if (!this.hasAttribute("role")) { this.setAttribute("role", "switch"); } if (!this.hasAttribute("tabindex")) { this.setAttribute("tabindex", "0"); } this._updateChecked(false); this.addEventListener("click", this.toggle); this.addEventListener("keydown", this._onKeyDown); } 6. When the component is dismounted, we should tidy up after ourselves – this function takes care of removing the component from the document: disconnectedCallback() { this.removeEventListener("click", this.toggle); this.removeEventListener("keydown", this._ onKeyDown); } attributeChangedCallback(name, oldValue, newValue) { if (name === CHECKED_ATTR) { this._updateChecked(true); } }
46
Chapter 2
Creating Components
7. As this is a component, we also need to set getters and setters – this takes care of working out if the button is checked (“switched on”) or unchecked (“switched off”). We also do something similar for the disabled state at the same time: get checked() { return this.hasAttribute(CHECKED_ATTR); } set checked(value) { this.toggleAttribute(CHECKED_ATTR, value); } get disabled() { return this.hasAttribute(DISABLED_ATTR); } set disabled(value) { this.toggleAttribute(DISABLED_ATTR, value); } toggle = () => { if (!this.disabled) { this.checked = !this.checked; } }; _onKeyDown = (e) => { switch (e.key) { case " ": case "Enter": e.preventDefault(); this.toggle(); break;
47
Chapter 2
Creating Components
default: break; } }; _updateChecked = (dispatch = false) => { this.setAttribute("aria-checked", this.checked. toString()); if (dispatch) this.dispatchEvent(changeEvent(this. checked)); }; } 8. This last part ties it all together – this is our call to define the component based on the class we’ve created: window.customElements.define(ToggleSwitch.elementName, ToggleSwitch); 9. Save the file as toggle-switch.js in the \toggle-switch\lib folder, then close all files – we will create a demo for our new component shortly in the next exercise. Phew – there’s a lot of interesting stuff going on in this demo! If you look carefully, you’ll see we’ve already used some of the elements we’ve touched on, such as customElements.define() or the attachShadow() method. We’ve used some additional functions for the first time in this demo, so let’s dive in and look at the code in more detail to see how it all hangs together.
48
Chapter 2
Creating Components
Breaking Apart the Changes At first glance, you’ll be forgiven for thinking that the last demo seemed to have a lot of code for a simple toggle switch! While this may seem true, a good chunk of it is styling, and our component uses some keywords we’ve already used earlier in this book. So – how did we build our component? We started by defining three variables, namely, CHECKED_ATTR, DISABLED_ATTR, and CHANGED – these store the appropriate state of the switch. We then added an event handler to determine if the change event has been triggered and to allow us to render the result to the end user. The next part is critical to the component – we create the template for our toggle switch. For this, we use standard JavaScript to create and initialize an element template with the appropriate HTML. While this is standard, the magic happens in the HTML and CSS we use – you will notice styles such as :host and [part="..."] in the code. The :host rule allows us to override these styles with external CSS if any is provided; if not, we use the styles defined in this template. When it comes to using the part property, think of this as a way of applying a class to a specific element, but one that sits inside a Shadow DOM-enabled component. If you look a little lower down, you will see a simple fragment of HTML, starting with
... – if we specify a rule like [part="slider"], this will override the one provided in the template. Moving on, we come to the JavaScript block for our component – here, we set up a class called ToggleSwitch, which extends the HTMLElement; this is a typical arrangement for creating web components that use the Shadow DOM API. Inside it, we add a static getter to return the CHECKED_ ATTR status of our component, as well as a constructor and the nowfamiliar this.attachShadow to create and initialize an instance of the Shadow DOM, using our template.
49
Chapter 2
Creating Components
Next up, we then add a series of functions and event handlers – these add role tags if they are not present (connectedCallback()) or remove click or keydown event handlers if the component is removed from the page (disconnectedCallback()). The remainder of the functions take care of getting or setting the state of our switch’s attributes and enabling or disabling the component. We round off the demo with the now-familiar customElements.define() method, which creates our custom element as a web component. We now have a toggle switch component in place – the question is, does it all work? There is only one way to find out: write some unit tests to ensure it works as expected. However, that takes time – we can at least perform a visual check in a simple demo: let’s dive in and create something to show off our new component.
Constructing the Demo Testing a switch is a straightforward affair – after all, there isn’t much we can test, save for whether it is enabled or disabled and whether the results change based on the state of the switch! That said, a visual test is always helpful – we can prove it looks and works as expected before writing any unit tests to give a more scientific answer. To prove whether our new switch works, I’ve simplified a copy of the demo from the original author of the toggle switch component – this displays some text on-screen, which flips between two different styles.
50
Chapter 2
Creating Components
CONSTRUCTING THE DEMO To create a demo for our component, follow these steps: 1. First, crack open a new file in your editor – go ahead and add this markup, which we will do in two stages, beginning with the opening tags and block:
Toggle-Switch Element 2. Next, we need to add the main markup for our demo – for this, add the following code immediately below the closing tag of the previous step: Toggle-Switch Component Demo Fancy Switch 51
Chapter 2
Creating Components
Watch the switch flip the styling!
3. Save the file as index.html and close the file. Next, we need to create a script file to satisfy the call for demo.js in step 1 – go ahead and create that file at the root of the toggleswitch folder. 4. In the file, add the following code – this will change the font used in the demo: document .querySelector("#style-demo toggle-switch") .addEventListener("toggle-switch:change", (e) => { const result = document.querySelector("#style-demo .result"); result.classList[e.detail.checked ? "add" : "remove"]("styleish"); }); 5. Lastly, we should add some styling – for this, we’ll add a mix of rules to make our demo look presentable, but others will be to show off the new switch component. We’ll be adding quite a few rules, so let’s start with the ones needed to set up the essential elements of the demo: 52
Chapter 2
Creating Components
*, *::before, *::after { box-sizing: border-box; margin-top: 0; } body { font-family: "Roboto", sans-serif; padding: 8px; background: #ededed; } main { max-width: 800px; margin: auto; } h1 { font-size: 48px; font-weight: bold; text-align: center; } .demo { margin-bottom: 96px; } 6. This next block takes care of styling the button part of our new toggle switch component: button, ::part(button) { font-family: Arial, Helvetica, sans-serif; font-size: 12px; border-radius: 4px; border: none; padding: 10px 18px; color: #ffffff; text-transform: uppercase; letter-spacing: 1px; box-shadow: 0 4px 4px -2px rgba(0, 0, 0, 0.25); cursor: pointer; background: #468fdd; } button:hover, ::part(button):hover { box-shadow: 0 4px 4px -2px rgba(0, 0, 0, 0.25), 0 0 0 48px rgba(0, 0, 0, 0.2) inset; } 53
Chapter 2
Creating Components
button:active, ::part(button):active, button[disabled] { box-shadow: 0 0 0 48px rgba(0, 0, 0, 0.3) inset; } button[disabled] { pointer-events: none; opacity: 0.5; } 7. This last batch looks after the changes effected when enabling or disabling the switch: figure { font-size: 24px; background-color: #ffffff; padding: 16px; border-radius: 4px; border: 1px solid #468fdd; min-height: 150px; } figure p { text-align: center; } figure p:last-child { margin-bottom: 0; } .switch-container { display: flex; align-items: center; justify-content: center; } label { padding-right: 4px; } label::after { content: ": "; } .styleish { font-family: "Henny Penny", cursive; } .fancy { height: 18px; } 54
Chapter 2
Creating Components
.fancy::part(track) { padding: 2px; border-radius: 16px; background-color: #ababab; } .fancy::part(slider) { border-radius: 16px; backgroundcolor: #ffffff; box-shadow: 1px 1px 2px hsla(0, 0%, 0%, 0.25); } .fancy[checked]::part(track) { background-color: #468fdd; } 8. Save the file as demo.css in the toggle-switch folder. Go ahead and preview the results in a browser – if all is well, we should see something akin to that shown in Figure 2-3. Try switching it back and forth to see what happens!
Figure 2-3. Enabling the new toggle switch component Phew – there’s a lot of code there, but the reality is that much of it is standard HTML and CSS. There are, however, some interesting concepts in this demo, particularly within the CSS: let’s take a closer look at the styles we’ve created to understand how they work in more detail.
Breaking Apart the Code So what did we create in our last demo? We began by adding some basic markup to host our switch component – if you look closely, you can see standard HTML tags such as the usual , a link to an (optional) Google font, and CSS used in our demo. Nothing outrageous there! 55
Chapter 2
Creating Components
Where things start to get interesting is later in the of the markup. Notice that when we call the component, we don’t use the typical self-closing tags that we might otherwise use when working with frameworks such as React. Instead, we must specify the full tags – not doing so can lead to some odd effects when rendering the component on the page. The next point of interest is in demo.js – here, we added an event handler using standard JavaScript. We target the toggle-switch element inside the #style-demo block before finding the instance of #style-demo .result (which is the text). We assign it to a variable result, which we then use to determine if we add or remove the stylelish class based on whether we check or uncheck the box. Moving on, the last area of note is in the CSS rules we created – and the use of ::part. According to MDN, this “CSS pseudo-element represents any element within a shadow tree with a matching part attribute.” In other words, if we have a part="..." tag on an element, we can style it using ::part. Take a look back at the markup we created in the component; here’s a reminder of it: We can treat part="..." as a standard CSS class or ID; the only real difference is that we can only use this tag in a Shadow DOM component. It means that we can use rules such as button, ::part(button) { font-family: "Roboto", sans-serif; ... } 56
Chapter 2
Creating Components
to target the appropriate part of the component. Here we apply the Roboto (or sans-serif ) font to both standard buttons (using button) and those in the Shadow DOM (with the ::part attribute). Okay – let’s crack on: we have reached a significant milestone. Over the last few pages, we created two components that use the Shadow DOM API and can form the basis for something more detailed in a future project. The next task is to test these components properly – in a way, we’ve already done a visual test in a browser, but we still need to do unit tests. Ordinarily, I would say that we could use any of a dozen different testing suites, such as TestingLibrary or Jest, but as always, things are not as straightforward as they seem….
Testing the Components Although plenty of testing suites are available, such as Testing-Library or Cypress – and I’m sure you will have a preferred choice – here’s the rub: many of these libraries do not support the Shadow DOM API! Do not worry, though, as there is one framework that does – it’s from the Open Web Components site at https://open-wc.org/docs/testing/ testing-package/. It’s not a package that is as well known, but it uses a very similar syntax to Jest, so it should be easy to learn. For our next demo, I’ve elected to use the alert component as our working example – we’ll step through setting up the test framework and then write some simple tests to give you a flavor of how we might test our component. Let’s dive in and look at what’s involved, starting with installing the testing framework.
This should be a formality as we’ve already used it, but make sure you have Node.js and npm installed – the site doesn’t state which version(s) it supports, but anything relatively recent in the last two to three years should work. 57
Chapter 2
Creating Components
DEMO: TESTING THE COMPONENT To test our alert component, follow these steps: 1. First, create a new folder called testing-components – this needs to be inside the creating-component folder, at the same level as the alert-components folder.
This is important, as we will import the alert component directly from the folder used in the previous demo to save copying it over. 2. Next, go ahead and extract a copy of the package.json from the code download and put it into the testingcomponents folder. 3. Fire up a command prompt, then make sure the working folder is set to /creating-component/testing-components. 4 . Enter npm install to install dependencies at the prompt, and press Enter. 5. With the main testing framework now in place, let’s add the tests for our component. Go ahead and add a folder called __ tests__ inside the testing-components folder, so you should end up with \testing-components\__tests__. 6. Inside this folder, open a new file, then add this code: import { expect, fixture, html } from "@open-wc/ testing"; describe("my-test", () => { it("is accessible", async () => { const el = await fixture(html` This is a test `); 58
Chapter 2
Creating Components
expect(el).to.be.accessible(); }); it("does not show custom text", async () => { const el = await fixture(html` `); expect(el.innerHTML).to.equal(""); }); it("renders with a class of info", async () => { const el = await fixture(html` `); expect(el).to.have.class("info"); }); }); 7. Save the file as alert.test.js – you can keep it open. 8. Switch to a command prompt, then enter npm run test and press Enter to run the tests – if all is well, we should get this output: $ npm run test > [email protected] test > web-test-runner "__test__/**/*.test.js" --node-resolve (node:23580) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. (Use `node --trace-deprecation ...` to show where the warning was created) __test__\alert.test.js: Browser logs: 59
Chapter 2
Creating Components
Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information. Chrome: |█████████████████ █████████████| 1/1 test files | 3 passed, 0 failed Finished running tests in 2.9s, all tests passed! Excellent! We have the basis in place for our tests – while we may have added simple checks, we can at least say we have a working means to unit test our component. We can add to or alter the tests – what we have written is just a starting point! There are, however, a few interesting points of note, so let’s pause for a moment to explore the code in more detail.
Understanding What Happened As a developer, I’m sure you can appreciate that testing our work is an essential part of the development process – we need confirmation that what we create works as expected, be that a single atom component, all the way through to a complex feature! Ordinarily, we would use something like Jest, Cypress, or even TestingLibrary, but – as I mentioned just now – none of them support the Shadow DOM, or might if you were using a framework and not pure JavaScript! So – how could we test our component? We turned to a different OpenWC package from the Open Web Components team. It’s probably not as well known, but it works in the same way as Jest, so it should be easy to pick up. To prove this, we installed it into an empty project using Vite – OpenWC is npm based, so using Vite makes it easier to install. We then added a __tests__ folder before writing three tests: the first assertion to validate that the component is accessible, the second to
60
Chapter 2
Creating Components
confirm we don’t show any custom text (but render the default fallback text instead), and the last to validate that we are using the info class to style our component. Notice how we import and use the fixture method into each test – this renders and inserts a piece of HTML into the DOM so that we can then test our component with confidence. By default, this method is done asynchronously (hence the await) – we can ensure the component is rendered correctly before completing each expect statement. With the tests completed, we then ran the web-test-runner, which we installed as part of the initial project setup (it was preset to install when we ran npm install). The runner ran each test before outputting the results from our test file. To finish, it displays a statement to say whether everything has passed or if there are any failures – hopefully, you got the former when running your demo! Before we move on, though, there are a couple of interesting points we should explore that you may see or get: •
You may (or may not) get a deprecation notice for punycode, depending on which version of Node.js you use. This warning is a known issue, which you can ignore – punycode has been deprecated by Node.js and is now a runtime dependency for Open-WC. It might add a little noise but will not affect your testing.
•
You may also see a comment starting with “Lit is in dev mode. Not recommended…”. This is to be expected, as Open-WC was written using the Lit framework; Lit works with all manner of different frameworks.
61
Chapter 2
Creating Components
If you want to learn more about the testing project from Open Web Components, you can see the documentation on the main site at https://open-wc.org/docs/testing/testing-package/. Let’s move on: we have two basic components in place and have worked through testing one as an example. We could use the component as is, but that could get a little messy – we wouldn’t get the full benefits of using it if we don’t wrap it up as a package! This is where npm comes into its own – it provides a means to wrap our component into something that makes it easy to reuse or available for others to download and use via the npm manager. It’s easy enough to set up, although it requires quite a few steps – let’s dive in and look at how in more detail.
Packaging the Component for Release With our components now created, we can use them as they are, but – what about publishing them as a package to npm? Running them as they are might be sufficient for your needs, but if we publish them, we can make them available for others to use – either within your company or externally. The publishing process is straightforward – there are three steps involved:
62
•
Upload the source code to GitHub.
•
Add a package.json with details of our component so npm can find and install it – we’ll use the alert dialog as the basis for this demo.
•
Run npm’s publish command to push it up to its repository.
Chapter 2
Creating Components
It sounds complex, but there isn’t anything complicated – there are a couple of things we need to do first before we can crack on with the process. They are as follows: •
We need an area to upload the component; for this demo, I will assume you have one available in GitHub and sufficient access to log in and upload the code. If you want to use a site such as GitLab, this should work OK as long as it can publish to npm.
•
You will need Git installed for your platform, along with Git Bash – if you don’t have either installed, comprehensive instructions are available on the Git website at https://git-scm.com/book/en/v2/ Getting-Started-Installing-Git.
•
You will also need an account on npm – if you don’t already have one, head over to www.npmjs.com/signup to complete the process. I strongly recommend you complete 2FA, as this may otherwise lead to npm blocking you, even though you enter the correct password.
Note – you must verify the account with npm before using it, so make sure your email address is valid! You will get a 403 error when running the demo if you don’t, as the account will be unverified. Okay – let’s begin the process by creating a package.json file for our component package.
63
Chapter 2
Creating Components
PART 1: PACKAGING THE COMPONENT 1. First, go ahead and navigate to the alert-dialog folder we created earlier. We will create the package to publish from this folder, but if you want to create a separate folder, please feel free to do so! 2. Inside this folder, open a new Git Bash session – make sure the session shows the working folder as your component folder.
If you’re using Windows, right-click and select Git Bash Here to do this step (it may be under the Show more properties menu if using Windows 11). This method means you will be in the correct folder when the Git Bash session window appears. 3. Enter npm init -y at the prompt to create a basic package. json file, then press Enter. Once it has confirmed creation, open it in an editor and amend it so it looks like this, replacing XXXX with your username for npm: { "author": "Alex Libby", "name": "@XXXXX/alert-component", "version": "0.1.0", "type": "module", "description": "An Alert Dialog Web Component for my Shadow DOM API book", "main": "script.js", "module": " script.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", 64
Chapter 2
Creating Components
"dev": "vite", "build": "vite build", "serve": "vite preview" }, "devDependencies": { "vite": "^5.0.4" }, "repository": { "type": "git", "url": "git+https://github.com/shadowdombook/alert- dialog.git" }, "license": "Apache-2.0", "bugs": { "url": "https://github.com/shadowdombook/alert- dialog/issues" }, "homepage": "https://github.com/shadowdombook/alert- dialog#readme" } 4. To create a demo for our component, we will install Vite and use it to generate a basic site structure. We’ve already added vite as a dependency, so at the prompt, enter npm install and press Enter to install it. 5. Vite needs to have an index.html file – if you’ve done it in the same folder as the original demo, we already have what we need. Switch to a command prompt, then make sure the working folder points to the alert-component folder. Enter npm run dev to fire up the Vite dev server at the prompt.
65
Chapter 2
Creating Components
6. Browse to http://localhost:5173/ to view the index file we just created – if all is well, we should see something akin to that shown in Figure 2-4.
Figure 2-4. The original demo, now running under Vite Do not be alarmed if you don’t see any difference from the original version of this demo – that is to be expected! Vite needs an index.html file to work correctly – rather than creating afresh, we can reuse the one we built earlier in this chapter. We’re making good progress, but we still have more to go! Go and have a breather, get a drink, and let’s continue with the next part of the demo when you’re ready. 66
Chapter 2
Creating Components
The next task is to push our code into a repository from which we can publish to npm. I used GitHub for this demo; if you don’t already have a suitable repository, you can sign up at https://github.com/signup. You will need to create a repository area then; please make note of the details you use so you can reference them later! As the number of files we need to push is minimal, we can simply drag and drop all of them (excluding the node_modules folder) via the upload facility at https://github.com/XXX/ YYY/upload/main, where XXX is the name of your account and YYY is the name of your repository.
I have not gone into too much detail here, as I assume you will be familiar with the basics of uploading code to Git – you can do it manually via drag and drop, from the command line, or via a subversion application. Any method is fine: go with whichever you prefer! Assuming you are successful, then let’s continue with the next part of the process.
PART 2: PUBLISHING THE COMPONENT Publishing to npm is easy – we only need to run a few commands to complete the process: 1. Crack open a command prompt or revert to the one from the previous demo, then make sure we’re still in the same component folder. 2. At the prompt, enter npm login -access=public, then press Enter. 3. npm will prompt you for your username, password, and email address – go ahead and enter the details you created before the start of the previous exercise. 67
Chapter 2
Creating Components
4. Once done, enter npm publish –access=public at the prompt, and press Enter – if all is well, you will see something similar to this appear: npm npm npm npm npm npm npm npm npm npm
notice notice notice notice notice notice notice notice notice notice
@shadowdombook/alert-[email protected] === Tarball Contents === 15B README.md 675B index.html 738B package.json 734B script.js 506B styles.css === Tarball Details === name: @shadowdombook/alertcomponent npm notice version: 0.1.0-alpha npm notice filename: @shadowdombook/alertcomponent-0.1.0-alpha.tgz npm notice package size: 1.4 kB npm notice unpacked size: 2.7 kB npm notice shasum: a1eeaa446b703c5c1f1a8a715c745904803874cf npm notice integrity: sha512- nDr91vxi1BmVO[...]BoooaNydIVfaA== npm notice total files: 5 npm notice npm notice Publishing to https://registry.npmjs.org/ + @shadowdombook/[email protected]
If you have 2FA enabled, you may be prompted to supply a token – you can do this using the same command as before but append -otp= at the end.
68
Chapter 2
Creating Components
5. If the publishing process was successful, we should see it published on npm at www.npmjs.com/package/XXX, where XXX is the name of your package. It will look similar to the version I’ve created, which you can see in Figure 2-5.
Figure 2-5. The newly published component on npm's registry
69
Chapter 2
Creating Components
At this point, we’re free to test it – we can use a service such as Skypack, available from https://skypack.dev, which allows us to load optimized npm packages with no build tools. It would be a matter of replacing the src location of our script in the demo to point to a Skypack one, and … well, wait! It takes Skypack a few moments to build the package, but with a couple of refreshes, we should see the site run using the new package. This brings us nicely to the end of the creating and publishing process; this is now available for anyone to use. Throughout this, we’ve covered some critical tasks in the last couple of demos, so while we catch our breath, let’s pause for a bit to review the code changes in more detail.
Breaking Apart the Code Changes Publishing and packaging a component for distribution is something of a double-edged sword. Some will be proud to release a new component or update an existing one. But, at the same time, there’s always that question: How will people take it? Will it land well, or (heaven forbid) … will it bomb? Leaving emotions aside, packaging and publishing our process requires a few steps – many of them are standard for npm packages, but getting them right is essential (more on this anon). To start, we turned our demo folder into a package folder – this required us to run npm init -y to give us a package.json. (The -y parameter bypasses the need for the questions and uses sensible defaults.) Next, we edited the package.json file to add the various fields required for our package. These include fields such as name, type, module, and description. Once done, we ran npm install to set up the dependencies for our package – in this instance, Vite. To test whether it all works, we ran npm run dev to fire up Vite’s development server before previewing the result in a browser.
70
Chapter 2
Creating Components
At this point, we had to pause to get the files uploaded to our repository. Once done, we ran npm login from a command prompt: this ensures you are logged into npm with the correct account for publishing your component. It doesn’t require any special permissions over and above what you have by default; it’s more about ensuring your component lands in the right area in npm! When running npm login, it prompted for some details – we had to enter our username and password for the repo, followed by the email address. Notice that we applied the -access=public tag; this is necessary for public/open repos. Next up, we ran npm publish -access=public to publish our component – in the same vein as login, we had to provide the -access tag. To round things off, we then browsed to the URL given by npm for our new component – this should be a formality, but it’s a helpful check to ensure it displays in the npm registry. Excellent – we have a published component! Now that we’ve completed this step, we should also tidy up the repository and add documentation, screenshots, and the like to make it all look presentable. I’ve seen too many repositories where people have not done this; it doesn’t leave a good impression!
Publishing the Component – A Postscript Although we have reached the end of the creation and publishing process, the journey for me wasn’t always plain sailing! There are a couple of key points I want to bring up which will help you: •
Remember how I said taking care of the fields we added was important? Well, on my first attempt, I omitted to remove a space in the module="script.js" line – the net result was that when I tried to use Skypack to create a temporary package I could use to test the site, the Skypack process failed! 71
Chapter 2
Creating Components
Although I could remove the space and republish, Skypack got itself confused and still thought the original version was present! I will need to investigate it – hopefully, I can get a version working… I wanted to use it to help test that our new component worked, but it will have to wait until I can get it fixed.
•
Although we have created two components that are practical examples, it’s important to note that we should consider them as alpha versions. We still need to do more work before we can regard them as suitable for use in a production environment. At this stage, it’s more important to understand the process – once we have this, we can use the examples to develop something more solid for release.
•
Although I’ve used demo names for our package, I have a sneaking suspicion that other versions of alert components exist in the npm registry that other authors have created. It, therefore, pays to make sure that you choose a name that doesn’t conflict. In this instance, using the namespace is essential; otherwise, you may find your publishing process trying to publish to an account that isn’t yours!
These are just some things for us to consider when creating our components. I’m sure there may be more, but that will come with experience!
Summary Although many developers will naturally reach for their framework of choice when it comes to creating components, we’ve reached a point where there may be occasions where this isn’t necessary. We can achieve 72
Chapter 2
Creating Components
the same results using plain vanilla JavaScript. We’ve covered a lot of content around that process in this chapter, so let’s take a moment to review what we have learned. The central theme of this chapter was to create a component we could use in a practical context. We created two: an alert dialog and an exploration of a third-party version of a toggle switch. In both cases, we built the code for the components before demoing each on a simple site. We then switched to setting up the testing framework from the Open Web Components group and creating some basic tests for our components. We then rounded off the process by exploring how to package and publish our components into the npm registry – we learned that while the process uses the standard steps from npm, we still have to include the correct fields to ensure our component package publishes successfully. Phew – what a journey! We still have more to cover, though: so far, we’ve focused on creating components that use the Shadow DOM API. However, what if we didn’t want to develop components but still wanted to benefit from the Shadow DOM API? Can we do that … what does it mean for us? This opens up a few intriguing possibilities and avenues to explore – stay with me, and I will reveal all in the next chapter.
73
CHAPTER 3
Applying Shadow DOM API in Apps Throughout this book, we’ve focused on creating components that use the Shadow DOM API – it’s perfect for encapsulating styles, controlling what we can and can’t override, and generally maintaining consistency – rather than letting others run roughshod over your pride and joy! There is one thing, though – what about using the API generally in sites outside of components? As developers, we would probably create some form of component, but there may be occasions where you want to create something custom (i.e., that is not reusable) yet still want to benefit from using the API. Fortunately, we can use the API anywhere on a site – it’s not limited to just being used in components! There isn’t anything different we need to do, but there are a few things to consider. I’ve selected three examples to illustrate what we might expect to see – they are as follows: •
Using the API in styling frameworks
•
Hosting existing components (such as React) inside the Shadow DOM API
•
Implications of using the API for search engine optimization (SEO)
© Alex Libby 2024 A. Libby, Beginning Shadow DOM API, Apress Pocket Guides, https://doi.org/10.1007/979-8-8688-0249-2_3
75
Chapter 3
Applying Shadow DOM API in Apps
I’m sure there will be more, but this is more than enough to get us started – let’s dive in first and look at how we might use the API in a styling framework.
Working with Styling Frameworks Hands up – what name(s) come to mind if you think of styling frameworks? I’ll lay odds that for some, Tailwind will feature as the answer – its popularity is not something to be sniffed at! For those of you who have not yet had the opportunity to use Tailwind, we must add classes to an element using predefined tags, such as this example:
Tailwind then converts these tags into meaningful CSS, as part of building the site. We’re compiling the stylesheet on the fly, but only when we run up the site for the first time. Now – this all sounds good, but there is one drawback: we can’t use Tailwind if we want to implement the Shadow DOM API! Is there a way around this, I wonder…? Fortunately for us, there is – let me introduce you to one of many Tailwind “clones”: Twind. The word “clones” is probably not the most apt word to use, as it doesn’t copy Tailwind; it’s a small compiler that converts the Tailwind classes into valid CSS. We then use it to construct our stylesheet at build time. It sounds a little complicated, but it isn’t – there is a little bit we must set up, but once in place, we treat the demo largely like we’re using standard Tailwind. To understand what this all means, let’s dive in and look at a small demo showing Twind in action.
76
Chapter 3
Applying Shadow DOM API in Apps
USING A STYLE FRAMEWORK WITH THE API To set up our Twind demo, follow these steps: 1. First, go ahead and create a new folder called tailwind-inshadow-dom inside our project area. 2. Next, open a command prompt inside this folder, which should set the working folder for us. At the prompt, enter npm init -y, then press Enter. 3. When done, we should find a package.json file at the root of this folder. Crack open a copy of the code download accompanying this book, then copy the package.json from that download over the top of this one – it contains some dependencies we want to install very shortly. 4. Revert to the prompt we had open in step 2, then enter npm install and press Enter to install the dependencies. 5. While this is happening, switch to a blank file in your editor, then add this markup, saving it as index.html at the root of the tailwind-in-shadow-dom folder:
Using Web Components with Twind
77
Chapter 3
Applying Shadow DOM API in Apps
6. Next, open a new blank file and add this code – we’ll do it in blocks, starting with the imports and opening statements: import install from "@twind/with-web-components"; import config from "./twind.config"; customElements.define( "twind-element", class TwindElement extends install(config) (HTMLElement) { constructor() { super(); const shadow = this.attachShadow({ mode: "open" }); 7. Leave a line blank, then add this statement – it acts as the template for our component, with the Tailwind styling included: shadow.innerHTML = ` Running Tailwind inside Shadow DOM `; } } ); document.body.innerHTML = "";
78
Chapter 3
Applying Shadow DOM API in Apps
8. Save the file as index.js at the root of the folder. For Tailwind to work correctly, though, we need to add a small configuration file – this we take care of in tailwind.config.js, so open a new file and add this code, saving it as tailwind. config.js: import { defineConfig } from "@twind/core"; import presetAutoprefix from "@twind/presetautoprefix"; import presetTailwind from "@twind/preset-tailwind"; export default defineConfig({ presets: [presetAutoprefix(), presetTailwind()], /* config */ }); 9. Save and close all open files, then switch to a command prompt. 10. Make sure the working folder is set to the tailwind-inshadow-dom folder; then at the prompt, enter npm run start and press Enter. 11. Go ahead and browse to http://localhost:5173 – if all is well, we should see this text appear in your browser (Figure 3-1).
79
Chapter 3
Applying Shadow DOM API in Apps
Figure 3-1. Tailwind running inside an instance of the Shadow DOM API Perfect – we can now use Tailwind, the package we all know and love! Okay – some of you may not be so enamored with the classes we have to apply, but hey, it’s all within the markup, so we don’t have to switch files, and Tailwind deals with a lot of the heavy lifting for us. Although most of what we’ve done is standard Tailwind code, there are a couple of points of note in that demo, so let’s pause for a moment to review the changes in more detail.
Exploring the Code Changes Cast your mind back about 20 years. Do you remember a time when people had to code styles by hand? Yes, I’m old-school enough to remember that time; gone are the days of crafting each rule by hand, knowing which browsers support which attributes, and the quirks associated with rendering content in browsers. We now have frameworks or libraries such as Tailwind, Sass, and the like to do much of the heavy lifting for us! With that in mind, it’s essential to ensure they still work if we want to use tools such as the Shadow DOM API. 80
Chapter 3
Applying Shadow DOM API in Apps
Fortunately, some of this compatibility work has already been done – in this last demo, we used Twind, a small compiler that can turn standard Tailwind classes into styles that work in the Shadow DOM. To prove this, we first set up a mini-site using npm, ready to import the dependencies we need for our site. We then created the main index.html file, which hosts our Tailwind-driven component in standard markup. Next up, we built the core of our site: the component. We first imported two items – one is the twind package for web components, and the other is the twind configuration for our site. We then added the opening block for our component using customElements.define() – this we set as twindelement, with a slight change to the config, so it defines a TwindElement object using the twind configuration. In the second part of the code block, we then attachShadow(), which we assign to shadow, before inserting the innerHTML containing the markup for our component. It consists of standard Tailwind classes and HTML markup – the background will be purple with either gray or pink text, depending on our screen size. The most important part is at the end – we define the , which will be our component and which we add to the body of the page. Moving on, we created tailwind.config.js, which sets up the configuration for Tailwind. Here, we set it to use the core properties and standard Tailwind and Autoprefixer presets. We then round out the demo by running up the site in the Vite development server and previewing the results in our browser. We can prove the Tailwind configuration is working by taking a peek at the code in the console log first (Figure 3-2).
81
Chapter 3
Applying Shadow DOM API in Apps
Figure 3-2. An extract of the original Tailwind code in our component Let’s now also look at an example from the compiled styles in our browser (Figure 3-3).
Figure 3-3. An extract of the compiled Tailwind styles At first glance, this doesn’t look too dissimilar to standard CSS – but notice the constructed stylesheet entry on the right. This is a relatively newish concept, where we construct the stylesheet at compilation time; this is how we can build stylesheets when using tools such as Tailwind. It does mean that we can change properties if needed; we can also share the constructed stylesheet between a document and a Shadow DOM tree using methods such as ShadowRoot. adoptedStyleSheets and Document.adoptedStyleSheets.
82
Chapter 3
Applying Shadow DOM API in Apps
If you would like to learn more, then head over to the MDN documentation site for this feature, which is at https:// developer.mozilla.org/en-US/docs/Web/API/ CSSStyleSheet/CSSStyleSheet. Okay, let’s crack on: so far, we’ve focused on individual web components. Up until now, we’ve focused primarily on components. While this works very well, the downside is that we work with individual units. Each component would attach itself to the Shadow DOM. If this is enabled, we’d have a mix of styles in different places and must use different methods to access some style properties if they are encapsulated in the Shadow DOM. What an absolute pain…! Can we do anything about this, I wonder?
ounting a React App Using M Shadow DOM API The answer to the question at the end of the last section is yes: What if we could host the entire application in an instance of the Shadow DOM? Yes, you heard me correctly – I did say the whole application! It might sound a little bonkers at first, but there is an excellent reason for doing this; before I divulge details, let’s first take a look at how we might host that example application in the Shadow DOM.
MOUNTING A REACT APP IN THE API For this demo, I will create a straightforward demo using the Checkbox component from Radix, available from www.radix-ui.com, a UI library written for React. To see how I set it up, follow these steps: 83
Chapter 3
Applying Shadow DOM API in Apps
1. First, go ahead and create a folder called react-in-shadowdom inside our project area. 2. We will use Vite to create our React app – we could install each dependency manually, but to save time, I’ve created the contents of a package.json we can use to install everything in one go. In a new file, add the following code, saving it as package. json at the root of the react-in-shadow-dom folder: { "name": "react-in-shadow-dom", "version": "1.0.0", "description": "", "main": "src/index.jsx", "keywords": [], "author": "", "license": "MIT", "scripts": { "start": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@radix-ui/colors": "^3.0.0", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-icons": "^1.3.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "vite": "^4.0.1" } } 84
Chapter 3
Applying Shadow DOM API in Apps
3. Next, open a new file, then add the following markup – save this as index.html at the same root:
Hosting a React App in Shadow DOM Please click the box to accept the terms and conditions:
4. This is where the magic happens – we need to build the App inside our project. We can do this with the following code, so crack open a new file and add this to it: import React, { FC } from "react"; import { render } from "react-dom"; import * as Checkbox from "@radix-ui/reactcheckbox"; import { CheckIcon } from "@radix-ui/react-icons"; import "./styles.css";
85
Chapter 3
Applying Shadow DOM API in Apps
const App = () => ( Accept terms and conditions. ); const root = document.querySelector("#react-root"); root.attachShadow({ mode: "open" }); render(, root.shadowRoot); 5. Save the file as index.jsx at the root of the react-in-shadowdom folder. 6. Next, we will add some basic styling – this isn’t essential to the project, but given we’re using a Radix component, it’s useful to show that it will still work as expected. Open a new file, then add the following styles, saving it as styles.css: @import "@radix-ui/colors/black-alpha.css"; @import "@radix-ui/colors/violet.css"; /* reset */ button { all: unset; } 86
Chapter 3
Applying Shadow DOM API in Apps
#container { width: 450px; display: flex; flexdirection: column; margin: 30px auto 0 auto; border: 1px solid #000000; font-family: Arial, Helvetica, sans-serif; } span { background-color: #ffffff; padding: 5px; } #react-root { padding: 10px; background-color: #d3d3d3; display: flex; height: 50px;} .CheckboxRoot { background-color: #ffffff; width: 25px; height: 25px; border-radius: 4px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 10px var(--black-a7); } .CheckboxRoot:hover { background-color: var(--violet-3); } .CheckboxRoot:focus { box-shadow: 0 0 0 2px #000000;} .CheckboxIndicator { color: var(--violet-11); } 7. Save and close all open files. Switch to a command prompt, then make sure it’s set to the react-in-shadow-dom folder as the working folder. 8. Enter npm run start at the prompt and press Enter to start the Vite development server. When prompted, browse to http:// localhost:5174/. If all is well, we should see something similar to that shown in Figure 3-4.
Figure 3-4. Demo of a React app inside Shadow DOM 87
Chapter 3
Applying Shadow DOM API in Apps
9. At face value, it doesn’t really look any different from a regular React app – could you tell if it was hosted in an instance of the Shadow DOM? Probably not – we will only know once we crack open the browser console and peer under the hood. The evidence that we’re indeed running in the Shadow DOM is shown in Figure 3-5.
Figure 3-5. Proof we're running the React app in the Shadow DOM Fantastic – even though we created a straightforward application using one component, it still shows we can take advantage of the Shadow DOM API. Granted, we used React, but there is no reason why this shouldn’t work for other frameworks: most front-end ones are written using JavaScript, so it should still work. That all aside, there are a couple of interesting points in this last demo that help make it all work – let’s take a moment to dive in and explore the code in more detail.
88
Chapter 3
Applying Shadow DOM API in Apps
Understanding What Happened To set up our demo, we kicked off by creating a standard Vite application using a prebuilt package.json – inside this, we set up dependencies for the Radix UI colors, react-checkbox, and icons, as well as the standard packages for React. We then put together a simple index.html file, which we use to display our component; this calls index.jsx from the /src folder, which represents our component. Inside the component file, we first import various packages – React and FC (or Functional Component) packages, as well as our checkbox component and the Radix icons. We then create an App() instance – inside this, we add the markup for our Checkbox instance; there’s nothing complex here, but the real magic happens at the end of the file. Here, we query the DOM for the #reactroot selector; once found, we attach an instance of the Shadow DOM to it before using the render method to display it all on screen. To finish the demo, we also added a simple stylesheet – this included both styles for the container markup and those used in the checkbox component itself.
I should mention at this point that if we had built our checkbox component with the Shadow DOM API, then we wouldn’t have been able to change styling within it unless it had been set up with ::host or ::part tags. As we’re encapsulating the whole application in the API, this constraint is no longer relevant – it’s what others do to our app that counts! At this point, you’re probably thinking – how is this technique useful? It’s a great question: being able to do something doesn’t mean we should do it!
89
Chapter 3
Applying Shadow DOM API in Apps
There is, however, a good reason for using this technique: in a themed environment. Imagine you create an online application that others use (such as an online form) in a themed environment. If they change the theme, you want to be sure that the theme’s styling won’t affect your own – one way to do it is to host it inside an instance of the Shadow DOM API.
Implications of SEO and the API Search engine optimization – or as we developers know it, SEO. Three letters that are probably the most critical to the success of any website – we want our offering to be visible, available to all, and searchable by the likes of Google and Bing. After all, that’s how the revenue comes in, right? That all aside, implementing the Shadow DOM API can have an effect on SEO – in some instances, content may render fine (using the Light DOM approach) or may not even render at all (using the attribute approach). This quirk may seem odd to you, as content should at least render, even if it isn’t styled? Well – this is one of the challenges we face with using the Shadow DOM API: to understand better what this means for us, let’s dive into a demo that shows what happens in more detail. Before we get started, though, there are a couple of things you should be aware of:
90
•
You might want to do this demo over two days – a part of the process requires Google to index your test site before we can assess the SEO implications; this can take a while to complete!
•
You will need your GitHub account credentials for this – they are required in order to authenticate access.
•
We will be setting up an account with Netlify – although there is usually a cost for this, using the free tier is fine for this demo.
Chapter 3
Applying Shadow DOM API in Apps
Okay – with the formalities out of the way, let’s move on to starting the demo.
TESTING SEO PART 1: SET UP THE SITE To test the effects of different implementations, follow these steps: 1. First, go ahead and download a copy of the dom-and-seo folder from the code download – this contains a simple demo highlighting the various ways we can use the Shadow DOM API. 2. Next, browse to www.netlify.com and log in – use your GitHub credentials to authorize access and set up the account. 3. When completed, select the option to drag and drop the code folder onto the site and wait for it to confirm it has published your code. 4. When prompted, grab the URL of your published site – go ahead and browse to confirm you can see content appear. Keep a copy of the URL you got from step 4 – you will need it for the next part of this demo. With the test site set up, we can move on to the next stage: linking your site to Google Search Console Tools.
TESTING SEO PART 2: CHECKING WITH GOOGLE WEBMASTER TOOLS To find out what Google thinks of our site, follow these steps: 1. Go to https://search.google.com/search-console, then click Start Now. 2. When prompted, use your GitHub credentials to authenticate access, then run through the verification process. 91
Chapter 3
Applying Shadow DOM API in Apps
3. Once logged in, enter your site’s URL in the search box at the top. 4. On the right side of the page, select Request Indexing. 5. Google will perform some checks. When it is complete, you should see “URL is available to Google.” 6. Next, click View Tested Page, then the Screenshot tab. 7. We can see everything is rendered successfully (Figure 3-6).
Figure 3-6. Our tested page 8. Click URL Inspection ➤ View Tested Page, then the HTML tab. We can see the HTML markup behind the demo – even though we have four different instances rendering on the page, all display text on the screen correctly, which can be read for SEO purposes.
92
Chapter 3
Applying Shadow DOM API in Apps
So far, so good – we can see how Google would see our site and confirm that we get four instances rendered, along with the relevant markup for each instance. But what if we tried the same test in Bing? Will it show the same content, or will it display something different, I wonder…?
Comparing with Bing Although Google has become the ubiquitous browser we all know and love, we must not forget that other browsers exist – Microsoft’s Bing being one of them! What works in Google may or may not work as well (or at all) in Bing, so we should at least check both browsers when using the API. Fortunately, both use a similar process, although Bing is good enough to import settings from Google’s checker, which will bypass some of the procedure. Let’s take a closer look at what we need to do.
A heads-up: I used a Gmail account for this demo, which makes life easier; you may want to do the same or adapt the steps accordingly if you prefer to use a different method.
COMPARING WITH BING To check our site against Bing’s tool, follow these steps: 1. First, browse to https://bing.com/webmaster, then sign in and choose the option to use Google. 2. Next, follow the prompts to authenticate with your chosen method. 3. Once done, click Import from Google Search Console, then hit Continue. 93
Chapter 3
Applying Shadow DOM API in Apps
4. On the next step, select Choose account – used Google, then select Allow ➤ Import, then Continue. 5. Next, click URL Inspection, then enter the URL of your test Netlify site into the URL Inspection field, and hit Inspect. 6. Once done, click Request Indexing ➤ Submit, then wait a few minutes before clicking the Live URL tab. At this point, Bing should show it’s found 2 SEO issues – they will be missing meta description and language tags. 7. Next, click SEO ➤ Site Scan, and enter Attempt 1 for the name field. 8. Next, add the URL of your test site and set the page limit to 10. (We could even go down to one page, but ten will give us a bit of leeway). Click OK to continue. At this point, you will need to wait – the page will update with issues found, but this can take a while to do as it depends on Bing indexing our site. It might be easier to return to this demo later or the following day. Thankfully for us, I’ve gone ahead to see how Bing sees our site – the results are not as good as Google! To see what I mean, this is what our site page shows when clicking URL Inspection ➤ Live URL ➤ View Tested Page once the site has been indexed by Bing (Figure 3-7).
Figure 3-7. This is what Bing sees when indexing our site 94
Chapter 3
Applying Shadow DOM API in Apps
Notice how, when rendered in Google Chrome, we could see all four instances rendered correctly? In Bing, we only get three instances rendered – to see what I mean, try rendering the site in IE11 mode. In our case, Bing doesn’t render each entry with the same font as Chrome; it also drops the Header without a Slot instance.
If you’re not sure how to switch to IE11 mode in Edge, then search for “render Edge in IE11 mode” on Google – there are plenty of articles that will explain the process. To see what I mean, Figure 3-8 displays what we see when looking under the hood in Edge’s browser console log area.
Figure 3-8. Evidence that the last entry is not rendered correctly in IE11 It does show that even though using the Shadow DOM API can be beneficial for encapsulating styles, we can’t use all the options we tried in the demo, as not all browsers will render them as expected!
hallenges with Server-Side Rendering – C A Postscript Throughout this book, we’ve talked about different ways of rendering content using the Shadow DOM API – some might force us to render static content, or we can create templates for reusability. While this can work 95
Chapter 3
Applying Shadow DOM API in Apps
very well, it can present a challenge for SEO, which is why we may need to consider which method of encapsulation we use when working with the API. If we’re building a site using a framework such as React, we would likely use server-side rendering to help cache content and speed up site delivery. There is an issue here, though: while the skeleton of the page may be quick to load, we have to get data for the page separately, so this could take longer, at least for the first page. It’s not helped by the fact that we frequently might change content using JavaScript, which can cause a rendering mismatch between client and server! It can affect SEO, as we need to see all the content; if content is delivered by JavaScript and not static HTML, it won’t see what we’re trying to render on-screen. To get around this, we should consider using Declarative Shadow DOM (or DSD) – we touched on this earlier in this book. It allows us to write the HTML template directly in the component and add it to a Custom Element using a template element with a shadowrootmode attribute. Using this method has two benefits – it’s faster and means we can deliver content without JavaScript, reducing the impact on SEO for the site. There is also a bonus: DSD only attaches a Shadow Root to an element. In other words, DSD takes care of rendering the HTML in our component; we still need to register and make it interactive as a Custom Element. This process is known as hydration – we need to supply a class definition to load it, plus any event handlers; the bonus is that we can choose when we want to trigger this process. If someone browsing through the site isn’t interacting with it (i.e., clicking buttons, etc.), rendering the content is enough; we only need to hydrate if the end user is performing an action on the site. This is one of many things we must consider when using the Shadow DOM API – it’s still a relatively young technology. While developers are likely to favor frameworks such as React, I suspect there will come a time when we shift away from using them to more native code we can run directly in our browser without runtime libraries. 96
Chapter 3
Applying Shadow DOM API in Apps
Summary One of the great things about using the Shadow DOM API (and, by extension, Web Components) is not only their interoperability between component frameworks but that we can also use them in a broader context, such as styling or even adding support to applications! In this chapter, we explored three examples of how we might do this; let’s take a moment to review what we have learned. We started by exploring how styling frameworks can use the Shadow DOM API – for this, we used Tailwind as our example in the guise of Twind. This was interesting as we typically can’t render Tailwind styles in the Shadow DOM API without some help, so it was great to see a lightweight compiler available to help in this respect! For the second example, we took the principles of Shadow DOM API that we’ve used so far and extended them to mount an entire React application inside an instance of the Shadow DOM. We saw how, instead of worrying about using different methods of styling elements, we could do all of it using the same process – this is great for those who have apps hosted in a themable environment and want to make sure themes do not affect functionality. For the last example, we dug a little deeper and looked at three letters that are super important to any site: SEO. We saw how our choice of writing Shadow DOM–enabled components can have an effect and learned that DSD is probably the best one to ensure reusability while not affecting SEO! Phew – it seems a lot, but we’re making good progress on our introductory journey through the world of the Shadow DOM API! We still have a few things I want to explore with you, such as performance, adding support to existing components, and more – as well as ask ourselves this question: “Will Web Components ever replace the likes of React?” Intrigued? Stay with me, and I’ll explain more in the next chapter.
97
CHAPTER 4
Supporting Other Frameworks Now that we have an understanding of the Shadow DOM API and how it operates, it is time to turn our attention to the broader picture – what if we use a framework such as React or Svelte? It presents a challenge, given that not all frameworks use Shadow DOM API by default. All is not lost, though – take, for example, React: with some work, we can use the API without too much difficulty. In this chapter, we’ll explore some of the tricks we can use to reconfigure our sites to allow us to run the Shadow DOM API and see that we can use it both in components and elsewhere on our projects. We have a lot to cover, so without further ado, let’s start with a look at adding Shadow DOM API support to React.
dding Shadow DOM API Support A to React Components Hold on a moment – you said React. Surely, that’s what we’ve been doing until now, right? Yes, that is what we’ve been doing, but this time, there is a twist: we will add Shadow DOM API support to existing components. Let me explain more. © Alex Libby 2024 A. Libby, Beginning Shadow DOM API, Apress Pocket Guides, https://doi.org/10.1007/979-8-8688-0249-2_4
99
Chapter 4
Supporting Other Frameworks
So far, we’ve learned about using the Shadow DOM API in new components, which (albeit simple ones) we’ve built from the ground up. However, there will be components out there that we use and where we would like to add support. What can we do? In some cases, we might be able to edit the source code and add it directly – however, that will probably be few and far between. Instead, we can create a component to wrap an existing one in the markup to have the same effect when compiling it during the build process. There is a bonus to this: we can reuse the code across multiple element types, limited only by those element types the API supports. Now – I know others have already created examples, but many of these are outdated and may not work, at least with current versions of React. An example is the component available from https://apearce.github. io/react-shadow-root/, but this one hasn’t been updated in four to five years! Instead, I will take a different route – we’ll create a simple React application using Vite, but this time, create our own ShadowRoot component. As an example, I’ll wrap it around some MUI buttons, but the same principle should apply to different components.
DEMO – ADDING SHADOW DOM SUPPORT Creating the core part of our component isn’t difficult – we might want to test and develop it further before using it in a production environment! To add Shadow DOM support to an existing component, follow these steps: 1. First, go ahead and fire up a terminal session from the project folder; make sure it shows the working folder as being the project folder. 2. At the prompt, enter npm create vite@latest, and press Enter. 100
Chapter 4
Supporting Other Frameworks
3. Vite will prompt for some options – when prompted, choose the following: √ Project name: ... adding-shadow-dom-support √ Select a framework: » React √ Select a variant: » JavaScript 4. Next, go ahead and extract a copy of the package.json for this demo from the code download accompanying this book. 5. At the prompt, enter cd adding-shadow-dom-support, then enter npm install && npm run dev and press Enter. 6. While Node.js creates our site, we can now create the various files – we’ll start with updating App.jsx at the root with this code: import import import import
React from "react"; { ShadowRoot } from "./components/ShadowRoot"; BasicButtons from "./components/Button"; "./App.css";
function App() { return ( ); } 7. Save and close the file. Next, in a new file, add this code, saving it as Button.jsx in the \src folder: import * as React from "react"; import Stack from "@mui/material/Stack"; import Button from "@mui/material/Button"; 101
Chapter 4
Supporting Other Frameworks
export default function BasicButtons() { return ( Text Contained ); } 8. We have one more component to build – it’s ShadowRoot.js. Go ahead and add the following code to a new file, saving it as ShadowRoot.js in the \src folder: import React from "react"; export class ShadowRoot extends React.Component { attachShadow(host) { if (host == null) { return; } host.attachShadow({ mode: "open" }); host.shadowRoot.innerHTML = host.innerHTML; host.innerHTML = ""; } render() { return {this.props. children}; } } 9. Last but by no means least, we need to update index.html in the \public folder – go ahead and replace the contents of that file within this code:
102
Chapter 4
Supporting Other Frameworks
Adding Shadow DOM Support Demo
10. Save and close all open files. Next, revert to a terminal session, then enter npm run start at the prompt to fire up the Vite development server. 11. Once running, go ahead and browse to the URL when prompted. If all is well, we should see our component now operating with Shadow DOM support, as shown in Figure 4-1.
Figure 4-1. The component – now running with Shadow DOM API support Okay – nothing looks like it’s changed … to be doubly sure, let’s peek at the underlying code in our browser’s console (Figure 4-2).
103
Chapter 4
Supporting Other Frameworks
Figure 4-2. Proof that we are using the Shadow DOM Wow – a lot is going on there! Most of the code we created in this demo is primarily for the application itself; the magic happens in ShadowRoot.js. It’s a valuable little component that we can drop in any location where the Shadow DOM API is supported, and it will add it automatically. We covered some helpful pointers in this code, so let’s take a few moments to review the changes in more detail.
Breaking Apart the Code Changes Until now, we’ve worked on adding Shadow DOM support for new components – existing components can use the same principles, but as we’ve seen, we have to nest them inside our ShadowRoot component. We end up with some extra tags in the markup, but we might have to live with this in the short term. That all aside, to create the component, we started by creating a simple test site using the standard process for all Vite sites, ready to host our new component. Once done, we added the various files, starting with A pp.jsx as the main page for our site. I’ll return to this page after reviewing the other files we created in this demo.
104
Chapter 4
Supporting Other Frameworks
We then created a new component called BasicButtons – in this component, we import an instance of the Stack and Button components from MUI, which we then render on the page. In reality, I could have chosen to use any components here or a completely different library; the critical thing to note is that we use an element supported by the Shadow DOM API (in this case, button). Next up comes the most critical part – our ShadowRoot component. We set it up as a React.Component class, inside of which we create an attachShadow function, which defines an instance of the Shadow DOM to apply to our target element. We first check to ensure there is a valid host element and exit if not; assuming we have one, we then attachShadow the Shadow DOM and assign the contents of innerHTML to our host. In the render() method, we return an instance of the element, with the Shadow DOM attached, before rendering it on screen. At this point, let’s return to the App.jsx page – we updated it to include two instances of our BasicButtons component. The first is the standard version from MUI; the second we wrapped inside our ShadowRoot component. Notice something about the latter and that it doesn’t appear to have any styling? It’s an odd side effect but not entirely unexpected: I suspect that the MUI component doesn’t have a ::host attribute, which would allow us to style the components from externally. It’s something to remember if you plan on using this method; you may find that you must alter styles within the component to allow it to render as expected! Okay – let’s crack on: so far, we’ve seen a few examples of using React with the Shadow DOM API. I think it’s time for a change – I know React is super popular, but what about other frameworks or libraries out there? How do they support the API…?
105
Chapter 4
Supporting Other Frameworks
Exploring Other Framework Examples I hinted earlier that not every framework supports the Shadow DOM API, or may cater for it differently. To see what I mean, let’s go through a couple of simple examples of other frameworks – Svelte and Alpine.js. The former abstracts much of the boilerplate away, allowing you to focus on the important stuff; the latter is a framework but much smaller and still supports the API. Let’s dive in and take a closer look, beginning first with the Svelte example.
First Example: Using Svelte As frameworks go, Svelte is a relative newcomer to the scene; however, it is quickly getting traction. It was designed to abstract away some of the more mundane tasks, allowing you to focus on what’s important: writing code! The best way to think of it is writing a CodePen but on steroids … but I digress. I chose Svelte as it makes it very easy to add Shadow DOM support – we have to add a few lines of code into the configuration and then add a simple tag to the top of a component, and Svelte takes care of the rest. For this next demo, I’ve adapted something I found online – it uses a slightly different coding style, but that shouldn’t matter: it’s the principle of using customElements that counts! I’ll explain more later in this chapter when we review the changes made, but for now, let’s get down and dirty with coding the next demo.
106
Chapter 4
Supporting Other Frameworks
FRAMEWORK EXAMPLE PART 1: SVELTE To build our Svelte example, follow these steps: 1. First, we need to create a starting site to host our component – for this, fire up a Git Bash session, then make sure the working folder is set to our project area. 2. At the prompt, enter npm create svelte@latest comparing-to-svelte and press Enter. 3. Svelte will ask a few questions about how we want to create our site – when prompted, choose the following answers: • Which Svelte app template? Skeleton project • Add type checking with TypeScript? No • Select ESLint for code linting, Add Prettier for code formatting, Add Playwright for browser testing, and Add Vitest for unit testing. 4. When done, press Enter, and wait for Svelte to display Your project is ready! 5. With the site in place, we have a few files to create or update – the first is our component, Rate. Crack open a new file, then add the following code, block by block, starting with some imports and variable definitions:
109
Chapter 4
Supporting Other Frameworks
8. Miss a line, then add this style block:
111
Chapter 4
Supporting Other Frameworks
9. We finally come to the meat of the component – miss a line, then add this Svelte script. We’ll start with the initial opening markup: {#if length > 0}
star-full 10. This next part takes care of rendering each star as a button, creating the star SVG, and setting the appropriate event handlers: {#each arr as n} { setRate(n); }} on:keyup={() => { onOver(n); }} on:keyup.enter={() => { setRate(n); }} {disabled}> {/each} 11. We’re almost there for the component – the last part is to add the markup that will render both the selected stars and text in our component: {#if showCount && over > 0} {over} {/if} {#if ratedesc.length > 0 && over > 0} {ratedesc[over - 1]} {/if} {/if} 113
Chapter 4
Supporting Other Frameworks
At this point, have a break for a moment – we’ve done the hard part of this component! We still have two more files to edit, which are +page.svelte and svelte.config.js. When you’re ready, let’s crack on with the +page.svelte. 12. Go ahead and open the +page.svelte template file that’s in the \src\routes folder – inside it, remove everything there, and replace it with this code:
A Rate Web Component Demo
13. The last file we need to change is svelte.config.js, so that Svelte knows that we’re creating web components – for this, edit the file so it looks like this: import { vitePreprocess } from '@sveltejs/viteplugin-svelte'; export default { preprocess: vitePreprocess(), compilerOptions: { 114
Chapter 4
Supporting Other Frameworks
customElement: true } }; 14. Go ahead and save all the open files, then close them. 15. Next, switch to a command prompt – enter npm run dev and press Enter; when prompted, browse to the URL shown. If all is well, we should see something similar to that shown in Figure 4-3, where I’ve already selected the Normal setting or three stars.
Figure 4-3. Our rating web component in action As we’ve seen before, it looks like a perfectly normal component – the real magic, though, is in what we see if we browse in the console log area. Figure 4-4 shows what it looks like, and we can see the #shadowroot in use.
Figure 4-4. Under the hood of the rating component
115
Chapter 4
Supporting Other Frameworks
Phew – that was a mammoth exercise, but in my defense, most of it was for setting up the component! Despite the somewhat lengthy code, the changes we need to make to turn a Svelte component into a web one are minimal – you may have noticed that we don’t even add attachShadow(), or any of the Shadow DOM API calls, as we have done in previous demos! This simplicity is what I like about Svelte – it’s designed to allow us to focus on the critical tasks at hand and not worry about features it can automate or abstract away from us. With that in mind, let’s take a walk through the code we created in more detail.
Exploring the Code in Detail Throughout this book, we’ve focused on adding Shadow DOM support – we’ve had to use attachShadow() a few times, specify the open property, and so on. Svelte is different: we haven’t had to do any of that! What’s going on, I hear you ask…? It comes down to one of the key tenets of Svelte – don’t bother developers with mundane tasks that it can automate! Instead, it lets us deal with the critical stuff, namely, the HTML, JavaScript, and CSS needed for our feature. We began way back when by first setting up a holding site using the create-svelte template in Vite – Svelte is npm-driven, so it needs something to run. Once done, we put together our component – we first specified the all-important customElement tag before adding a bunch of exports, a single import for onMount, and beforeUpdate from Svelte. Next, we set a handful of variables and a Svelte reactive (or $:) statement. The latter is a little tricky to understand, but in a nutshell, it allows us to run any statements as soon as a variable changes value. In this case, we set value – if this changes, then we run convertValue(value) and assign the result to both rate and over. The following two functions manage the latter two – we use them to make sure we have a suitable value we can use to render the correct number of stars on the page. 116
Chapter 4
Supporting Other Frameworks
We then created a second batch of functions – isFilled, which works out if the star should be filled (i.e., selected); createArray, which builds the number of stars to display; beforeUpdate, which triggers the createArray function; and onMount, which runs the convertValue() function when the component is initialized on-screen. Moving on, we added a whole host of styles. There isn’t anything unusual here except that we use the BEM notation of styling. The method simplifies instances where we might otherwise have to specify multiple layers of elements to target the right one.
You can learn more about BEM in a great article on the CSSTricks website at https://css-tricks.com/bem-101/, or a search on Google will turn up alternatives! For the last part of the component, we set up the markup for each star – we use Svelte’s if conditional to display the appropriate number based on what we pass to the component. Inside the conditional check, we add markup for a star SVG image, followed by a button for each star. We style the buttons to look like the stars we would expect to see when clicking a review component like ours. To finish it off, we add two if statements to render the value of the star selected and an equivalent rating in plain text on the screen. Phew – with that monster review out of the way, the rest will seem like a walk in the park! To display our component, we edited the +page.svelte file to add an instance – in this case, we could pass in some props such as length (5), rating descriptions, and a Boolean to control the display of the text. The real magic happens in the final change. In svelte.config.js, we add the compilerOptions attribute, which tells Svelte to compile our component as a web component, and automatically add support for the Shadow DOM API.
117
Chapter 4
Supporting Other Frameworks
Okay – let’s move on: for our next demo, I’ve elected to use a framework that isn’t anywhere near as popular as React, but that shouldn’t matter: it still supports the Shadow DOM API, which is more important! The tool in question is Alpine.js: it was designed to be a lightweight and rugged alternative to some of the big heavyweights we use today. I know some of you may or may not have heard of it or even ask why I’ve chosen it! The answer is simple: why not? I like to be different – it shouldn’t matter what the tool is, as long as it supports the API, that’s more important. With that in mind, let’s dive in and look at how we can set up a component with Shadow DOM API support.
Second Example: Exploring Alpine.js Alpine.js is not something I’ve admittedly used before, but as a developer, I always like to experiment with different tools, particularly for the front end. I came across Alpine some time ago and love its simplicity; it’s designed to be a lightweight, rugged alternative and works more like jQuery but for the modern web. Given that we’ve already spent time with React, vanilla JavaScript, and (just now) Svelte, I wanted to pick something that is completely different – and hopefully answer that “why not?” in producing a demo. As it so happens, I came across an existing demo online – this creates a simple counter using two buttons. I’ve adapted the original as a basis for my version, which we will go through in the next demo.
118
Chapter 4
Supporting Other Frameworks
FRAMEWORK EXAMPLE PART 2: ALPINE.JS To build our demo in Alpine.js, follow these steps: 1. First, create a new folder called comparing-with-alpinejs, at the root of our project folder. 2. Inside this folder, add a new file – we have a good chunk of code to go through, so let’s do it block by block, starting with the headers:
Testing Shadow DOM with Alpine.js 3. Next, immediately below it, add this block of styles:
119
Chapter 4
Supporting Other Frameworks
4. We have one more block to add, which is the body – immediately below that last block, add these lines: Web Component Reactive State - Shadow DOM - 55 +
5. Save the file as index.html, and close it. Next, open a new blank file – we need to add our button component. For this, add the following code, starting with the opening event handler call, a variable definition for our template, and the inner HTML for that template: document.addEventListener("alpine:init", () => { const template = document.createElement("template"); template.innerHTML = ` 120
Chapter 4
Supporting Other Frameworks
- + `; 6. We need to add the class that will form our component, so leave a line blank, then add this code: class ShadowDOMCounter extends HTMLElement { state = Alpine.reactive({ counter: this.hasAttribute("start") ? parseInt(this.getAttribute("start")) : 0, inc: this.inc.bind(this), dec: this.dec.bind(this), }); 7. This next block is the constructor for our component: constructor() { super(); const shadow = this.attachShadow({ mode: "open" }); shadow.appendChild(template .content .cloneNode(true) ); Alpine.addScopeToNode(shadow, this.state); Alpine.initTree(shadow); }
121
Chapter 4
Supporting Other Frameworks
8. The final block takes care of incrementing or decrementing our counter, based on using the connectedCallback() function: connectedCallback() {} inc() { this.state.counter++; } dec() { this.state.counter--; } } customElements.define("my-reactive-shadow", ShadowDOMCounter); }); 9. Save the file as reactiveshadow.js, then close it. 10. Go ahead and open index.html in your browser – if all is well, we should see something akin to the screenshot shown in Figure 4-5.
Figure 4-5. An example using Alpine.js Yes – something lightweight at last! I make no bones about saying that, as I have always been a staunch advocate of the KISS (keep it simple … yes, you get the idea…) principle. Granted, Alpine was never designed to work or compete with tools such as React, but sometimes “less is more,” I always say…. 122
Chapter 4
Supporting Other Frameworks
But I digress. While this book isn’t about Alpine, we’ve still covered some valuable features, so let’s take a moment to explore the code in more detail.
Understanding the Changes I don’t know why, but when working with Alpine, I get mental images of a beautiful Swiss mountainous landscape with fresh air. I guess Alpine is that breath of fresh air, proving we don’t always need a heavyweight tool to crack a nut! But I digress – for this demo, we created two files: the first was index.html, inside which we added a call to the Alpine framework before adding the my-reactive-shadow component, with a start prop value of 33. We also added buttons and the starting value but didn’t hook them up – there is a reason for this, which I will come onto in a moment. Next up, we created our Shadow DOM component – we created an event listener method that kicks when Alpine initializes our component, in this case, my-reactive-shadow. We first define a template variable, into which we assign a set of styles for the two buttons and span we will use in our component. We then add markup for our buttons and a span to display the value on screen. Notice that we have some Alpine tags in place – @ click is the equivalent of onClick, and x-text will set the text value of our span to the value of our count. In the second part of our component, we create a ShadowDOMCounter class. In this class, we first retrieve the value from the start property or set it to zero if one is not provided. We also bind in two functions – inc and dec – which we will use to increase or decrease our count value. We then have our constructor() function, inside which we create and initialize an instance of the Shadow DOM for our component. At the same time, we define the inc and dec functions – these will adjust the count accordingly and are triggered when clicking on our two buttons. We then rounded out the demo by previewing the results in our browser, and you can see our component using the Shadow DOM in Figure 4-6. 123
Chapter 4
Supporting Other Frameworks
Figure 4-6. Proof that our Alpine component is using the Shadow DOM Okay – it’s time for a change: we’ve covered two examples of using the Shadow DOM API in different frameworks, but there are two questions I want to answer. The first is performance – what is it like when using the Shadow DOM API? Of course, there will be a lot of factors at play that can affect it; nevertheless, it’s still important to understand what it means for us.
Assessing Performance of the API Performance. Speed. Sub-2 seconds. These are all terms I’ve heard many times over the years when developing code – trying to create something easy to maintain and performant and doing what people expect can be challenging at times! That all said, the issue of performance was brought home when I came across an interesting article by Nolan Lawson, at https://nolanlawson. com/2022/06/22/style-scoping-versus-shadow-dom-which-is-fastest/. In it, he talks about how different style scoping methods can impact performance, particularly when using the Shadow DOM API. His research shows a mixed bunch of results, but that attribute scoping (in red; see Figure 4-7) is not the fastest method. What is interesting, though, is that scoping inside a Shadow DOM component (yellow) isn’t always the fastest either, as shown in Figure 4-7.
124
Chapter 4
Supporting Other Frameworks
Figure 4-7. Performance tests of different style scoping methods (source: Nolan Lawson, as quoted in text) The results from his initial research proved to be a mixed bag – he had comments from both ends of the spectrum! Such was the polar response that Nolan redid his research: it showed that while using attributes was still slow, classes scoped in a Shadow DOM component were still fast but marginally slower than unscoped classes. It does raise an interesting point – it’s not sensible to base choices on the performance of the Shadow DOM on its own; we should also factor into the mix aspects such as the styling engine of the browser, your development paradigm, and requirements for encapsulation. Indeed, according to Nolan – Firefox’s Stylo engine is so fast that if it were in every browser, we wouldn’t even need to worry about performance! Just something to think about when working with the Shadow DOM…. With that question out of the way, let’s move on to the second question.
125
Chapter 4
Supporting Other Frameworks
ill Web Components Replace Tools Such W As React? That is a good question, if perhaps a little contentious – after all, they can both be used to create components, even if the methodology might be different! Over the last few years, we’ve become accustomed to using tools such as React, Vue, and Angular – all good tools in their own right. However, they are somewhat heavyweight frameworks, and there is an increasing trend toward using more native or lightweight tools. It’s one of the reasons I like tools such as Svelte or Web Components – both are more lightweight than the likes of React and do not require the same mindset you need to have when using React. Both do not require us to write as much code as we might find we have to do with React, but Web Components doesn’t have quite the same level of documentation as React – at least not yet! One of the best features of Web Components – at least for me – is reusability: with Web Components, you can write a component that will work anywhere, be that React, Svelte, Vue, or other libraries. You can’t do that with React – we have to use it within React. Web Components may have some way to go in terms of maturing, but you can’t beat real reusability that it offers! In short – do I think React may go? I have mixed feelings about it – in some respects, web components are a bit limiting, at least in the types of values or properties you can pass between each component. React is far more mature but has been around longer than Web Components. Given time, we could see frameworks die out when people see that more native solutions can offer the same functionality but with less baggage. After all – look at something like jQuery: for a time, it provided its own solution to fixing an issue. However, now, it has deprecated APIs in its package to support more native solutions directly in the browser. Who knows what might happen…?
126
Chapter 4
Supporting Other Frameworks
nd Now for Something A Completely Different… We’ve almost come to the end of our journey through the Shadow DOM API – we’ve covered a lot of content, introduced you to the basics of the API, and explored how we can use it in our projects. However, when researching for this book, I came across a couple of articles that piqued my interest – it made me wonder: Could either (or both) be done using the Shadow DOM API? Now, before you say anything (and yes, I know what’s coming!) – I did ask myself this question rhetorically; you will understand why when I go through each subject: •
Building a tabbed Chrome extension: https://webhighlights.com/blog/building-your-own-new-tabchrome-extension/
•
Building my version of React: www.babbel.com/en/ magazine/build-your-own-react-episode-2
•
Building a JavaScript framework that uses the Shadow DOM API: https://mikeguoynes.medium.com/part-1build-your-own-js-framework-from-scratchf4e35d0dffa6
Yes – don’t say I didn’t warn you! These would fall way outside the scope of what we could do in this book, but it made me wonder what could be possible. For example, I know the middle article uses Virtual DOM – there are some similarities with Shadow DOM, so it could potentially be adapted to work with the API. The important thing here is that we’re not limited to creating components – options are available further in the field, and it’s up to us to explore what we want to do with the Shadow DOM API.
127
Chapter 4
Supporting Other Frameworks
Summary Well – as someone once said, “all good things must come to an end sometime…” Indeed – we have reached the end of our journey through the Shadow DOM API. We’ve covered a lot of topics throughout this book, ranging from getting familiar with the basic API features to creating components, adding Shadow DOM support to an application, and more – hopefully, something here will have piqued your interest! Over the last few pages, we focused first on adding Shadow DOM support to existing React components – not all are blessed with the API from the get-go, so it’s helpful to know how to apply it where needed. We then explored a couple of examples of different frameworks to see how they implement the Shadow DOM API – in Svelte’s case, it was done automatically, while with Alpine, we had to add it manually as we did with React and vanilla JavaScript. We then moved on to briefly exploring performance – much of this came from work done by the developer Nolan Lawson, where he found that although it wasn’t the fastest, Shadow DOM–scoped components and applications were not too far behind! We then rounded out with a short debate around whether the Shadow DOM API (and Web Components) could ever replace tools such as React before finishing with a quick postscript on a couple of articles about taking Shadow DOM API support further afield. Phew – there’s a lot there! We have reached the end of our journey through the API; I hope you’ve enjoyed learning about it as much as I have enjoyed writing this book and that there will be something you can use in your future projects.
128
APPENDIX
API Reference In this reference, we list the key interfaces, properties, and methods for the Shadow DOM API – it’s worth noting that more details are available on the MDN website; I’ve provided links as a starting point for each section.
I nterfaces The list of key interfaces is defined in Table A-1.
Table A-1. Main interfaces for the Shadow DOM API Interface
Purpose
CustomElementRegistry Includes methods for registering and querying custom elements For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/CustomElementRegistry HTMLSlotElement
Provides access to the name and assigned nodes of a element For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/HTMLSlotElement (continued)
© Alex Libby 2024 A. Libby, Beginning Shadow DOM API, Apress Pocket Guides, https://doi.org/10.1007/979-8-8688-0249-2
129
Appendix
API Reference
Table A-1. (continued) Interface
Purpose
HTMLTemplateElement
Provides access to the contents of an HTML element For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/HTMLTemplateElement.
ShadowRoot
The root Node.js of a DOM subtree created using the Shadow DOM API, which is separate from the document’s main DOM tree For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/ShadowRoot
P roperties The key properties for the Shadow DOM API are listed in Table A-2.
Table A-2. The list of key properties for the Shadow DOM API Property
Purpose
Element.shadowRoot
Represents the shadow root for the targeted element For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/Element/shadowRoot
Element.slot
Returns the name of the Shadow DOM slot into which the target element is inserted For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/Element/slot (continued)
130
Appendix
API Reference
Table A-2. (continued) Property
Purpose
Event.composed
A read-only Boolean property used to ascertain if the event will propagate across the Shadow DOM into the standard DOM For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/Event/composed
Event.composedPath
Returns the event’s path as an array of objects on which listeners will be invoked For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/Event/composedPath
Node.isConnected
A read-only Boolean value to determine if the Node.js is directly (or indirectly) connected to a Document object For more information, please see https:// developer.mozilla.org/en-US/docs/Web/ API/Node/isConnected
Window.customElements This read-only property returns a reference to the CustomElementRegistry object to register new custom elements and retrieve information about previously registered custom elements For more information, please seehttps:// developer.mozilla.org/en-US/docs/Web/ API/Window/customElements
131
Appendix
API Reference
Methods The main methods for the API are listed in Table A-3.
Table A-3. The key methods for the Shadow DOM API Method
Purpose
Document.createElement()
Used to create an HTML element using a tagName if supplied or HTMLUnknownElement if one is not recognized or provided For more information, please see https:// developer.mozilla.org/en-US/docs/ Web/API/Document/createElement
Element.attachShadow()
Attaches a Shadow DOM tree to our chosen element and returns a reference to its ShadowRoot Note that not all elements accept a Shadow DOM tree, such as – for the complete list and more information, please see https:// developer.mozilla.org/en-US/docs/ Web/API/Element/attachShadow
Node.getRootNode()
Gets the context object’s root, including the shadow root if one is available For more information, please see https:// developer.mozilla.org/en-US/docs/ Web/API/Node/getRootNode
132
Index A, B, C Alert dialog components, 36 component, 49 connectedCallback() function, 42 console log, 41 customElements.define()/ shadowRoot. appendChild(), 36 demo constructing process, 51–56 functions/event handlers, 50 markup, 36–40 my-alert component, 40 publishing/packaging component/update, 70, 71 npm publish commands, 68–71 package.json file, 63–67 postscript, 71, 72 publishing process, 62 switch component, 43, 55–57 template, 41, 49
testing development process, 60 Testing-Library/ Cypress, 57–62 toggle switch creation, 42–49, 55 Alpine.js component, 123, 124 connectedCallback() function, 122 constructor() function, 123 demo online, 119–124 index.html, 123 Shadow DOM component, 123 Application programming interface (API) commands, 17 component, 18 desktop browser support, 19–21 methods/properties/ interfaces, 17 performance test, 124, 125 SEO, 90 styling frameworks, 76–83 web components, 16, 21
© Alex Libby 2024 A. Libby, Beginning Shadow DOM API, Apress Pocket Guides, https://doi.org/10.1007/979-8-8688-0249-2
133
INDEX
D, E, F, G, H, I, J, K, L, M, N, O, P, Q Declarative Shadow DOM (DSD), 30, 96 Document object model (DOM) .attachShadow() method, 3 “Hello World” program, 3–5 React site, 5–9 Real DOM, 2 Virtual DOM, 2
R React application checkbox component, 83 functional component packages, 89, 90 index.html, 85 Radix component, 86 Vite development server, 87 React components App.jsx, 101 attachShadow function, 105 BasicButtons, 105 browser’s console, 103, 104 Button.jsx, 101 existing components, 99, 104, 105 index.html, 102 production environment, 100 ShadowRoot.js, 102 Web components, 126
134
S, T, U, V, W, X, Y, Z Search engine optimization (SEO), 90 benefits, 96 Edge’s browser console log, 95 Google Search Console Tools, 91–93 hydration, 96 implementation, 90, 91 Microsoft’s Bing, 93–95 server-side rendering, 96, 97 tested page, 92 Shadow DOM API, 75, 127 advantages, 22 Alpine.js, 118–124 API, 16 applying styles, 31–33 bigger picture, 15, 16 browser’s console, 10 composition component, 28, 29 Declarative Shadow DOM, 24 postscript, 30, 31 slots and templates, 24–28 customElements layer, 15 demo code, 13, 14 disadvantages, 22, 23 DOM, 2 flattened page, 12 high-level concepts, 10–13 interfaces, 129, 130
INDEX
principles, 9 properties, 130–132 React app, 83–90 React components, 99 schematic level, 11 search engine optimization, 90 Svelte, 106–118 Styling frameworks compatibility work, 81
features, 76 frameworks/libraries, 80 predefined tags, 76 tailwind.config.js, 81, 82 Twind demo, 77–81 Svelte framework components, 106–118 rating web component, 115 steps, 107–117 Web components, 126
135