Let’s continue on with our Shadow DOM exploration! In the last chapter, we zeroed in on a really nice aspect of the Shadow DOM. As awesome as DOM encapsulation is, the CSS aspect of the Shadow DOM is even better! Despite coming up with clever ways to mitigate style creep in our web development work over the years, it has always been a problem.
Style creep can sometimes be a bit of a headache in web development work. To sum up, it’s when CSS rules come in and affect elements you didn’t intend to affect. You may be working to style an element in one place, but some style rules you’ve defined in your CSS for another element on your page are unintentionally picked up because the CSS selectors match. Although style creep isn’t limited to Web Components, let’s take a look at a Web Component example to see how it impacts us. Figure 9.1 shows a simple little Web Component that is essentially a stylized numerical stepper.
For this hypothetical use case, let’s say that no matter what the other buttons look like in our web application, it’s important that this stepper be red, and that the plus and minus buttons are flush around the number in the middle. We’re going for a very specific look here, and it needs to be perfect. The next listing shows us how this was achieved.
<html> <head> <script> class SampleComponent extends HTMLElement { connectedCallback() { this.innerHTML = ` <button class="big-button">-</button> 1 <span class="increment-number">5</span> 2 <button class="big-button">+</button> 3 <style> 4 sample-component { display: flex; } sample-component .increment-number { font-size: 24px; background-color: #770311; color: white; font-family: Helvetica; display: inline-block; padding: 11px; border: none; } sample-component button { 5 border-radius: 0 50px 50px 0; border: none; width: 50px; height: 50px; font-size: 36px; font-weight: bold; background-color: red; color: white; } sample-component button:first-child { border-radius: 50px 0 0 50px; } sample-component .big-button:active { background-color: #960000; } sample-component .big-button:focus { outline: thin dotted; } </style>`; } } if (!customElements.get('sample-component')) { customElements.define('sample-component', SampleComponent); } </script> </head> <body> <sample-component></sample-component> 6 </body> </html>
Notice how each style rule is prefaced with sample-component. In such a simple example with only one component on the page, specifying .sample-component button isn’t strictly necessary. After all, our component has all of the buttons in the entire page here. A button is such a common element, however, that as soon as we start adding other content to our page, this button style will start affecting that other content. By making the rule specific to our .sample-component, we’re avoiding style from this component leaking out into other elements we didn’t intend.
It’s good to have a refresher on how global styles like these work. In figure 9.2, we see that the CSS rules we define in our component become part of the page’s global style space. In turn, these styles will affect any and all elements on our page.
Even with this specificity, our button rules could leak the other way as well. What if we had another component within this one with buttons of its own? Those buttons still have <sample-component> somewhere in their ancestry, so the CSS here would creep into any components downstream.
It’s inevitable that you’ll face some style creep, no matter how specific your selectors are, and you’ll need to debug it. But again, web developers have faced this issue forever. That said, when using Web Components, it’s easier to overlook these kinds of problems because we tend to treat the components we work with as standalone, encapsulated objects and skip over the inner content when scanning the DOM in our debug tools.
So, let’s say you’ve covered all your bases. You’ve carefully planned your class names and CSS rules to be a good component developer and not let your styles leak out of your components. That’s only half the battle—style can still creep into your component from the page and miscellaneous parent components.
Let’s pretend your web app is driven by some sort of design system. Design systems, like Bootstrap, define a consistent look and feel in your web pages or applications. For example, you’d likely want most buttons in your application to adopt a single look, like in figure 9.3.
With the next listing, we’ll add this button to our page with a simple button element and some page-level CSS to style it.
<head> <style> button { 1 border-top: 1px solid #96d1f8; background: #65a9d7; background: linear-gradient(90deg, #3e779d, #65a9d7); padding: 5px 10px; border-radius: 8px; box-shadow: rgba(0,0,0,.5) 0 8px 8px; text-shadow: rgba(0,0,0,.4) 0 2px 2px; color: white; font-size: 14px; font-family: Helvetica; text-decoration: none; vertical-align: middle; } button:hover { border-top-color: #28597a; background: #28597a; color: #ccc; } button:active { border-top-color: #1b435e; background: #1b435e; } </style> <script> . . . same component definition as before </script> </head> <body> <sample-component></sample-component> <br /><br /> <button>Button from Design System</button> 2 </body> </html>
Looking at the results in figure 9.4, we can already see how the button style is creeping into our component and doing some bad things.
We’re starting to adopt some of the look of the button in our stepper. We have the drop shadow, and the blue gradient backgrounds, which of course don’t match the numeric text in the middle anymore. Things are even more broken when you click the button—the background changes to red. In short, things are getting messy!
This is all caused by the generic button styles having just a few different rules than our stepper component button. The stepper’s background color rule is overridden by the generic button’s background rule. And of course, the stepper button shouldn’t have a text shadow or box shadow rule like the generic button does.
We’re not even getting into rule specificity here! Pretend that our generic button had a “big-button” variation as well, which just so happens to match the rule name inside our component.
Let’s go back and make this variation by increasing the font size and padding of that button to make it a proper “big button.” Our goal is to get something that looks like our previous generic buttons in figures 9.3 and 9.4, just bigger in context.
The reality, however, is that when we define this variation by changing all of our button rules in the CSS outside of the component from button{} to button.big-button {}, we get some unexpected results. With more rule specificity like this, and the coincidental naming of “big-button” for both buttons (inside our component and out), we’ve just created a situation in which rules we’ve defined outside of our component are more specific than those within. This really hurts the shape of our stepper buttons, shown in figure 9.5, that we’ve carefully defined with the border-radius rule.
We can fix this, of course. We can add even more specificity in our CSS selectors inside the component, just like we did for the generic button. We can go from button {} to button.big-button {}. Also, though, we have to negate the properties that aren’t covered in our component that are defined in our generic button:
sample-component button.big-button { box-shadow: none; text-shadow: none; padding: 0; }
With these changes, we’re back to our component looking just fine. It’s obvious now that we have to be a little on guard for these types of problems. How much on guard really depends on how much you can control the surrounding application and anticipate how that style could creep in and affect you. The button versus stepper situation would have really been helped if rules for the <button> element as a whole weren’t defined in the global CSS. Creating more unique names would be helpful as well.
As much as this sounds like a mess, and it is, it’s something we as web developers have had to deal with forever. All that said, the Shadow DOM promises a fix!
In the last chapter, we saw that creating a shadow root on our component created a separate and independent DOM: access to this DOM was limited, and JS calls couldn’t leak through to change elements or query-select components. When all was said and done, it was super easy!
We can protect our Web Component’s DOM in the same way here. With the next listing, we can go back to our stepper component and use the Shadow DOM.
class SampleComponent extends HTMLElement { connectedCallback() { const root = 1 this.attachShadow({mode: 'open'}); root.innerHTML = `<button class="big-button">-</button> <span class="increment-number">5</span> <button class="big-button">+</button> <style> sample-component { display: flex; } span { 2 font-size: 24px; background-color: #770311; color: white; font-family: Helvetica; display: inline-block; padding: 11px; border: none; } button { border-radius: 0 50px 50px 0; border: none; width: 50px; height: 50px; font-size: 36px; font-weight: bold; background: none; background-color: red; color: white; } button:first-child { border-radius: 50px 0 0 50px; } button:active { background-color: #960000; } button:focus { outline: thin dotted; } </style>`; } }
Not only did I introduce the Shadow DOM into our stepper component, but I also got a little overly excited and removed all of my specific rules. My CSS selectors now specify only the rules for the generic <button> and <span> tags. After everything we’ve had to deal with in this example, as well as over the years of CSS pain in web development, this feels lazy and prone to breakage, doesn’t it?
But the point is, now that we have a separate DOM, and we know that our component is this simple, as with our stepper component, we can absolutely style our elements generically here, and it’s perfectly fine! Style won’t creep in, as shown in figure 9.6, and style won’t creep out and affect child components that also use Shadow DOMs.
Listing 9.3 isn’t perfect yet, though. For the most part, figure 9.7 looks OK, but the stepper component has some bad spacing in it.
What happened here? Well, our component used to have a display style of flex. The old rule is left in, but it’s not working:
sample-component { display: flex; }
That’s because the <sample-component> tag is now outside of our Shadow DOM. Technically speaking, the tag that represents our component is the shadow host, and this host contains the shadow root, which contains our Shadow DOM. Since CSS can’t leak into the Shadow DOM, this rule using sample-component is now meaningless for what we want to achieve here.
Instead, styling the Shadow DOM comes with a few new ways to use CSS selectors. The first is the new selector, :host. The :host selector is shorthand for styling what’s inside the shadow host, as figure 9.8 shows. Changing our selector to
:host { display: flex; }
puts our display: flex rule back in action.
There is a bit of nuance to Shadow DOM CSS encapsulation, however. The Shadow DOM works pretty well to guard against outside styles coming into your Shadow DOM-guarded component. The nuance is that we’re guarding against style creep when defined by a selector and not overall style. To explain what I mean, let’s try another example in the next listing, where we define some style on the <body> of the page, outside the Shadow DOM.
<html> <head> <style> .text { 1 font-size: 24px; font-weight: bold; color: green; } </style> <script> class SampleComponent extends HTMLElement { connectedCallback() { const root = this.attachShadow({mode: 'open'}); root.innerHTML = `<span>Some Text</span>`; 2 } } if (!customElements.get('sample-component')) { customElements.define('sample-component', SampleComponent); } </script> </head> <body class="text"> 3 <sample-component></sample-component> </body> </html>
So, what do you expect here? I promised that the Shadow DOM guards against styles coming into your component, yet when the example runs, as seen in figure 9.9, the <span> tag contains big, green, bold text!
This is because the nuance I’m talking about is that we’re really guarding against CSS selectors from the outside being able to latch onto classes on the inside. Yet when an ancestor of your component (Shadow DOM or no) has some style applied to it that doesn’t require selecting anything inside your component, that style will still affect the children. Now, if we removed that text class from the body like so,
<body>
and put that same class on the <span> inside our component like this,
root.innerHTML = `<span class="text">Some Text</span>`;
you’ll see that the text style has no effect, as shown in figure 9.10.
The "text" selector from our example can’t penetrate the Shadow DOM, yet those same rules as a style from the outside can. However, even something as simple as an outside <button> style won’t creep in in the same way because "button" is still a selector (albeit a generic one). This can be pretty useful and makes a lot of sense. If all the text on your overall page is styled a certain way, or your page has a specific background color, you don’t want your components to depart from these basic styles.
What if you didn’t want even that style to creep in? We can do a bit of a trick with the :host selector.
<script> class SampleComponent extends HTMLElement { connectedCallback() { const root = this.attachShadow({mode: 'open'}); root.innerHTML = `<span>Some Text</span> <style> :host { all: initial; 1 } </style>`; } } if (!customElements.get('sample-component')) { customElements.define('sample-component', SampleComponent); } </script>
While we certainly could set each individual style rule to "initial" to reset them, it’s more encompassing to reset everything in our shadow root using the all CSS property and the brand-new :host selector.
To go beyond the :host selector and explore a little more, let’s start a new demo project to properly give the Shadow DOM a try!
So, this demo has a bit of a dual meaning. Yes, we will be going through some Shadow DOM exercises to introduce some new concepts, but the demo we’ll be making is also an exercise browser and workout creator.
The final product in this chapter won’t be as interactive as it could be, and that’s because we’ll keep exploring this demo in chapter 14 as we cover events to implement the rest of the functionality. For this chapter, we’ll end up with an exercise library on the left and your custom workout plan on the right, as shown in figure 9.11. Clicking each exercise in the library will add it to your plan.
Exercise types are either “strength” or “cardio” and are represented by a blue or green stripe, respectively. To keep things simple on the page, and because I don’t personally own a bunch of exercise videos to share with you, my thumbnails and backgrounds are gray. However, in this book’s GitHub repo, I’ve included GIF links in my data model, defined in components/exerciselibrary/exerciselibrary.js, so that each exercise renders with a motion thumbnail that will let you properly preview the exercise.
As a first step, let’s create the overall application structure along with some placeholders for child components. Specifically, we’ll create an HTML page, CSS file, and <wkout-creator-app> component, where the file structure looks like figure 9.12. If you are following along, please remember to use some sort of simple web server, given that we do have dependencies loaded from our index.html that may not work just using the file system.
As with our other demos, our index.html will be extremely simple, as in the following listing.
<html> <head> <title>Workout Creator</title> <script type="module" 1 src="components/workoutcreatorapp/workoutcreatorapp.js"> </script> <link rel="stylesheet" type="text/css" href="main.css"> </head> <body> <wkout-creator-app></wkout-creator-app> 2 </body> </html>
Our CSS is even simpler, and is just negating the margin and padding of the page body while sizing the <wkout-creator-app> to the entirety of the page with a bit of padding.
body { 1 margin: 0; padding: 0; } wkout-creator-app { 2 height: calc(100vh - 20px); padding: 10px; }
For the <wkout-creator-app> itself, the component’s code, shown in the next listing, is also very simple.
import Template from './template.js'; export default class WorkoutCreatorApp extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); 1 this.shadowRoot.innerHTML = Template.render(); } } if (!customElements.get('wkout-creator-app')) { customElements.define('wkout-creator-app', WorkoutCreatorApp ); }
Note that, unlike in past demos, we are now using the Shadow DOM. Also, unlike what we did earlier in this book, we are doing all of our component setup in the constructor and directly using the shadowRoot property to access our local Shadow DOM.
Lastly, I’m going to be using Shadow DOM CSS features as well as doing some things you’d never do without the Shadow DOM. Neither of these are easy to back out of! So, here I’m going all in on the Shadow DOM with no turning back.
Continuing on from our WorkoutCreatorApp module that defines the <wkout-creator-app> component, let’s take a peek at the template.js module that holds our HTML and CSS in the next listing.
export default { render() { return `${this.css()} ${this.html()}`; }, html() { return `<wkout-exercise-lib> 1 </wkout-exercise-lib> <div id="divider-line"></div> 2 <wkout-plan></wkout-plan>`; 3 }, css() { return `<style> :host { display: flex; } wkout-exercise-lib, wkout-plan { flex: 1; height: 100%; background-color: #eaeaea; } #divider-line { width: 1px; height: 100%; margin-right: 25px; background-color: black; } </style>`; } }
First off, we’re creating three child elements. Two of them are components that aren’t defined yet, so they’ll just be rendered as empty <div> elements; they’re styled with a background color, so we can visualize their placement thus far, as figure 9.13 shows. In the middle of these two sits a black divider line.
Even with just this, we have two points to discuss with the Shadow DOM. First, we’re using the previously mentioned :host CSS selector to assign some style to our host component. In this case, we simply want to use a display type of "flex" to lay out our three elements.
The second point is an important one. It sounds like a small point, but it’s actually kind of huge. Our divider line is assigned the ID "divider-line" in <div id="divider-line"></div>. We then use this ID to assign style with CSS: #divider-line {}.
Why is this so important? Well, ingrained in every web developer is that we should use the ID attribute sparingly. The reason is that there can be only one element with that ID in your entire DOM. If you make a mistake and assign a second element with the same ID, you’re bound to get CSS or query-selection problems when you’re only able to select or style one of the multiple elements with the same ID.
Typically, our selectors will be multiple classes together to get the specificity required to accurately select or style an element. For our divider line, we might use a CSS selector that looks like
wkout-creator-app div.divider-line.center.thin {}
Yes, I got a little ridiculous with the selector just now using .center and .thin, but I’m just trying to underscore the point of overdoing the specificity, which is usually needed.
Now, however, we can use the Shadow DOM. Coming back to the point that each ID in your entire DOM must be unique, remember we’re now using multiple DOMs! Your ID needs to be unique only inside the scope of your Web Component. An element with an ID of #divider could easily exist elsewhere on the page or in other Web Components, and there would be no conflict.
Even better, given that there are only three elements in this Web Component, with just the divider line using a <div> tag, we could easily not bother with an ID, instead using a selector like this: div {}.
Personally, I think this is really exciting. Coming back to when I introduced the Shadow DOM in the last chapter, I said that it removes the brittleness of web development. This is a prime example. We can focus on the structure and style of our component and not worry about conflicts anywhere else. Our selectors can be as dead simple and easy to read as our component’s internal structure allows.
We’re going to continue on now with more of same concepts we just explored in order to get a grid of exercises and our workout plan list in place. That’s two more components, which makes our project structure look like figure 9.14.
Remember, we are actually rendering those <wkout-plan> and <wkout-exercise-lib> components already in the application component; it’s just that they aren’t defined yet, so they render as <div> elements. As such, our first step after creating the new files and folders for the components is to import those modules at the head of workoutcreatorapp/template.js:
import ExerciseLibrary from '../exerciselibrary/exerciselibrary.js'; import Plan from '../plan/plan.js';
With those defined, let’s get to work fleshing out these components!
Both are pretty simple, in fact. This is largely due to us not paying any attention to interactivity yet. The next listing shows our plan/plan.js and plan/template.js files.
// Plan.js import Template from './template.js'; export default class Plan extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = 1 Template.render(); } } if (!customElements.get('wkout-plan')) { customElements.define('wkout-plan', Plan); } // Template.js export default { render() { return `${this.css()} ${this.html()}`; }, html() { return `<h1>My Plan</h1> 2 <div id="container"></div> <div id="time">Total Time:</div>`; }, css() { return `<style> 3 :host { display: flex; flex-direction: column; } #time { height: 30px; } #container { background: linear-gradient(90deg, rgba(235,235,235,1) 0%, rgba(208,208,208,1) 100%); height: calc(100% - 60px); overflow-y: scroll; } </style>`; }, }
Since our workout plan list is empty at the start of the application, we aren’t rendering anything except the container, header text, and a footer to show total plan duration.
Again, we’re using a Shadow DOM, which enables us to use element IDs to target both the time and container <div> tags for styling. On both of these, we’re just setting sizing and background fill color, as well as telling our exercise list container to scroll when it gets too tall. Also again, we’re using the :host selector to tell our component’s shadow root to display using a vertical flexbox.
The <wkout-exercise-lib> component is similar, except we actually do want to feed it with data. The purpose of this component is to show a list of exercises to choose from, so they should all be present when the application loads. As such, we’ll be rendering a header and container, just like the last component, but we’ll also be populating the container with all of our exercises. The next listing shows exerciselibrary/exerciselibrary.js and exerciselibrary/template.js.
// exerciselibrary.js 1 import Template from './template.js'; export default class ExerciseLibrary extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = Template.render([ { label: 'Jump Rope', type: 'cardio', thumb: '', time: 300, sets: 1}, { label: 'Jog', type: 'cardio', thumb: '', time: 300, sets: 1}, { label: 'Pushups', type: 'strength', thumb: '', count: 5, sets: 2, estimatedTimePerCount: 5 }, { label: 'Pullups', type: 'strength', thumb: '', count: 5, sets: 2, estimatedTimePerCount: 5}, { label: 'Chin ups', type: 'strength', thumb: '', count: 5, sets: 2, estimatedTimePerCount: 5}, { label: 'Plank', type: 'strength', thumb: '', time: 60, sets: 1} ]); } } if (!customElements.get('wkout-exercise-lib')) { customElements.define('wkout-exercise-lib', ExerciseLibrary); } // template.js 2 export default { render(exercises) { return `${this.css()} ${this.html(exercises)}`; }, html(exercises) { let mkup = `<h1>Exercises</h1> <div id="container">`; for (let c = 0; c < exercises.length; c++) { mkup += `<wkout-exercise class="${exercises[c].type}" ></wkout-exercise>`; 3 } return mkup + `</div>`; }, css() { return `<style> host { display: flex; flex-direction: column; } #container { overflow-y: scroll; height: calc(100% - 60px); } </style>`; } }
You’ll notice right away the big list of exercises we’re feeding into the Template .render function. Each exercise has a label as well as a type of either cardio or strength. Depending on whether you count each rep or just do the exercise for a set amount of time, the exercise will have a number for count and sets or for time. If we’re tracking count and sets, the only way we can estimate the total time of our workout is to estimate how much time each single rep of our exercise takes, so we use another property called estimatedTimePerCount.
Lastly, there is an empty thumb property on each exercise. Like I said at the beginning of this chapter, we’ll just leave this blank to not show a thumbnail in this book. You can search for your own images or GIFs online to populate these or look at the GitHub repo for this book for ones I’ve found. Also in my GitHub repo are more exercises for our data model.
Our exerciselibrary/template.js file is mostly the same as the previous plan/template.js. Of course, the main difference is that we’re accepting the list of exercises and rendering each one. Again, we’re waiting to define the <wkout-exercise> for now while we focus on everything else, which gives us something that looks like figure 9.15.
You’ll notice that even though we’ve rendered our exercises, they aren’t showing up. That’s because even though they are there in the DOM, they don’t have a size or background—so, despite being present, they have a zero-pixel height and don’t appear visually. We’ll address this with the <wkout-exercise> component. It is the last one to cover, and it’s actually pretty interesting.
Why do I find this <wkout-exercise> so interesting? Well, it’s because we’re going to start on a component that needs to look slightly different depending on how it’s used, and we’ll learn an alternate way of using the :host selector. In the next chapter, we’ll push even further on this adaptable component to make it look completely different in the workout plan container.
Since our workout plan needs some interactivity to function, let’s focus instead on the exercise library first, as it’s easier to iterate on style for something that appears on page load instead of requiring the extra step of clicking to add. We’re, of course, going to need to create the component files, and we’ll end up with the file structure shown in figure 9.16.
Since both the workout plan and exercise library render the exercise component, we should place that import into both plan/template.js and exerciselibrary/template.js modules:
import Exercise from '../exercise/exercise.js';
Let’s take a look at the Web Component definition for <wkout-exercise> in the following listing.
import Template from './template.js'; export default class Exercise extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); const params = { label: this.getAttribute('label'), type: this.getAttribute('type'), thumb: this.getAttribute('thumb'), time: this.getAttribute('time'), count: this.getAttribute('count'), estimatedTimePerCount: this.getAttribute('estimatedtimepercount'), sets: this.getAttribute('sets'), }; this.shadowRoot.innerHTML = Template.render(params); } get label() { return this.getAttribute('label'); } 1 set label(val) { this.setAttribute('label', val); } // more getters/setters for thumb, type, time, count, // estimateTimePerCount, and sets serialize() { 2 return { label: this.label, type: this.type, thumb: this.thumb, time: this.time, count: this.count, estimatedTimePerCount: this.estimatedTimePerCount, sets: this.sets, } } static toAttributeString(obj) { 3 let attr = ''; for (let key in obj) { if (obj[key]) { attr += key + '="' + obj[key] + '" '; } } return attr; } } if (!customElements.get('wkout-exercise')) { customElements.define('wkout-exercise', Exercise); }
To save space here, I’ve eliminated all but one of my getters/setters. In this component definition, we’re employing something we picked up in chapter 3. We’re using reflection to use attributes and properties interchangeably. We can use either element.setAttribute(property, value) on the element or element.property = value to set a property. Either way, we’re getting or setting some data that is internally based on the element’s attribute. If I didn’t cut it short for brevity, we’d have getters/setters for thumb, type, time, count, estimateTimePerCount, and sets as well.
The other two methods are ways to gather our data. First, we have serialize, which just assembles our data into one object we can pass around easily. The other static method, toAttributeString, is similar. It assembles all of our data like serialize does but creates a string that we can use to populate attributes. We’ll end up with a string in the format of
property="value" property2="value2" property3="value3"
This extra method might not seem necessary, but we want to weed out those undefined properties. Remember that because of the variation of the exercises, some will have a rep count property, like when you lift weights, while others will have a duration property, like when you’re jogging. So rather than having property="undefined" be an attribute on our tag when the actual undefined value gets converted to a string, or having to check for undefined on each property in our templates, making them a bit long and hard to read, this is a good alternative. All this is to explain why in exerciselibrary/template.js, we’ll modify our loop in the html() function to be
for (let c = 0; c < exercises.length; c++) { mkup += `<wkout-exercise class="${exercises[c].type}" ${Exercise.toAttributeString(exercises[c])}></wkout-exercise>`; }
With this, we can create attributes on our new element for each and every valid property in our data. As this is a static method (accessed from the class rather than the instance), we can use it either on the raw data objects we have in exerciselibrary/exerciselibrary.js before the component is created or against an already-created <wkout-exercise> component to copy those values. Whether a simple object or component, the properties are all there and can be used the same way by this method. The tag we get in the end looks like either of the following, depending on the exercise:
<wkout-exercise class="cardio" label="Jog" type="cardio" time="300" sets="1"></wkout-exercise> <wkout-exercise class="strength" label="Pushups" type="strength" count="5" sets="2" estimatedtimepercount="5"></wkout-exercise>
With all of the attributes we need set on the component, and the component definition created, there’s just one last thing to do: create the HTML and CSS seen in the next listing.
export default { render(exercise) { return `${this.css(exercise)} ${this.html(exercise)}`; }, html(exercise) { return `<div id="info"> <span id="label">${exercise.label}</span> <span id="delete">x</span> </div>`; }, css(exercise) { return `<style> :host { 1 display: inline-block; background: radial-gradient(circle, rgba(235,235,235,1) 0%, rgba(208,208,208,1) 100%); /*background-image: 2 url('${exercise.thumb}');*/ border-left-style: solid; border-left-width: 5px; } :host(.cardio) { 3 border-left-color: #28a7ff; } :host(.strength) { border-left-color: #75af01; } #info { font-size: small; display: flex; align-items: center; background-color: black; color: white; } :host { width: 200px; height: 200px; background-size: cover; } :host #info { padding: 5px; } </style>`; } }
With all of this now put together, our <wkout-exercise-lib> component renders all of the <wkout-exercise> components we have. Seen in figure 9.17, the first minor thing to notice is our component backgrounds:
background: radial-gradient(circle, rgba(235,235,235,1) 0%, rgba(208,208,208,1) 100%); /*background-image: url('${exercise.thumb}');*/
I’ve commented out the background image, but if you’ve searched online and found some great thumbnails for each exercise and added them to the data in the <wkout-exercise-lib> component, feel free to uncomment this line. If you didn’t, we’re simply showing a gradient gray background.
Notice as well how simple the HTML is. We’re showing a 200 × 200 box with a black label at the top. This is fine for the library view, but you might imagine that this could all be a little problematic to display as a list view in the exercise plan.
Again, we’re using some concepts we’ve covered before in this chapter. We’re identifying and selecting elements using the ID attribute as well as using the :host selector for our component’s shadow root context.
Note, however, that we have a small variation on the :host selector:
:host(.cardio) { border-left-color: #28a7ff; } :host(.strength) { border-left-color: #75af01; }
Back when rendering each of these components, we did put a class of strength or cardio on each component:
mkup += `<wkout-exercise class="${exercises[c].type}" ${Exercise.toAttributeString(exercises[c])}></wkout-exercise>`;
This variation on the :host selector allows us to consider any classes on the component’s tag itself and use that for more CSS specificity. To be clearer and more concise, :host(.cardio) enables us to style the element <wkout-exercise class= "cardio"> based on its cardio class. In practice, these differing border colors enable the user to differentiate between the two different types of exercises when browsing the library grid.
There are a few more CSS selectors you may have seen online that I didn’t get to here, but they lack support or are deprecated. We’ll finish up making the <wkout-exercise> component adaptable to different contexts in the next chapter, while talking about some Shadow DOM gotchas while we’re at it.
Before exploring the Shadow DOM gotchas and updating the Workout Creator app some more, we’ve learned enough to update the slider UI component we’ve been working on throughout this book. What’s nice is that not much needs to change!
First things first, let’s start using the Shadow DOM. Previously, the component initialization code was in the connectedCallback function, but we know now about the ability to use the constructor because of the Shadow DOM. The following listing shows this constructor; keep in mind, we’ve removed the connectedCallback altogether, moving the setup code to here.
constructor() { 1 super(); this.attachShadow({mode: 'open'}); 2 this.shadowRoot.innerHTML = Template.render(); 3 this.dom = Template.mapDOM(this.shadowRoot); document.addEventListener('mousemove', e => this.eventHandler(e)); document.addEventListener('mouseup', e => this.eventHandler(e)); this.addEventListener('mousedown', e => this.eventHandler(e)); }
Also, because the constructor fires prior to the attributeChangedCallback, the timing issue we faced before with connectedCallback doesn’t happen anymore. You’ll notice that we no longer have the following lines in our constructor’s setup code:
this.refreshSlider(this.getAttribute('value')); this.setColor(this.getAttribute('backgroundcolor'));
We also don’t need to check if the this.dom property exists anymore, like when we did this:
setColor(color) { if (this.dom) { . . .
Of course, this check doesn’t hurt. But with all of the initialization happening prior to the incoming attribute changes when the component starts, it’s just not needed.
The template.js module can change slightly as well. In addition to using the :host selector for the component root, we can use IDs now instead of classes for styling and selection. As I’ve mentioned, using IDs is a luxury we weren’t afforded before without an encapsulated DOM like we have now. The next listing shows the new template.js file for the slider.
export default { render() { return `${this.css()} ${this.html()}`; }, mapDOM(scope) { return { overlay: scope.getElementById( 1 'bg-overlay'), thumb: scope.getElementById('thumb'), } }, html() { return `<div id="bg-overlay"></div> 2 <div id="thumb"></div>`; }, css() { return `<style> :host { 3 display: inline-block; position: relative; border-radius: 3px; } #bg-overlay { 4 width: 100%; height: 100%; position: absolute; border-radius: 3px; } #thumb { margin-top: -1px; width: 5px; height: calc(100% - 5px); position: absolute; border-style: solid; border-width: 3px; border-color: white; border-radius: 3px; pointer-events: none; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } </style>`; } }
With the Shadow DOM now working in the slider component, we’ve done just about all we need to do on that particular component. We won’t abandon it yet, though! The slider will be an integral part of a bigger component that we’ll create in the last chapters of this book, where we’ll also explore testing, a build process, and running Web Components in IE11.
In this chapter, we learned
18.224.37.68