Chapter 9. Shadow CSS

This chapter covers

  • Keeping external style out of your Web Components
  • The Shadow DOM for CSS encapsulation
  • Shadow DOM CSS selectors
  • Rediscovering the ID attribute for Web Components

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.

9.1. Style creep

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.

Figure 9.1. A stylized stepper component comprising two buttons and a text span

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.

Listing 9.1. A stepper component without logic, just style
<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>

  • 1 Stepper decrement button
  • 2 Current stepper value
  • 3 Stepper increment button
  • 4 Component styles
  • 5 Component styles, continued
  • 6 Sample component on page

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.

Figure 9.2. Without using the Shadow DOM, style defined in your Web Component will apply to the entire page.

9.1.1. Style creep into component descendants

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.

9.1.2. Style creep into your component

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.

Figure 9.3. An example globally stylized button that could come from a design system

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.

Listing 9.2. A styled button coexisting on our page with a Web Component
<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>

  • 1 Non-component button styles
  • 2 Non-component button element

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.

Figure 9.4. How a global button style can negatively affect our stepper component

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.

Figure 9.5. More specificity and samenamed classes wreck the stepper component even more.

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!

9.2. Style creep solved with the Shadow DOM

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.

Listing 9.3. Using the Shadow DOM to protect our stepper component’s style
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>`;
   }
}

  • 1 Creates a shadow root to use the Shadow DOM
  • 2 With a smaller and more manageable DOM, CSS selectors don’t need to be so specific.

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.

Figure 9.6. Web Components using the Shadow DOM are unaffected by page-level CSS styling.

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.

Figure 9.7. The stepper component, almost fixed, and living side by side with a globally styled button

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.

Figure 9.8. CSS on the shadow host (or using the component’s tag as the selector) won’t penetrate into the shadow root or into the Shadow DOM.

9.2.1. When styles creep

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.

Listing 9.4. Text rules affecting inside 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>

  • 1 Some text styling on the outer page
  • 2 A span containing text inside our component’s Shadow DOM
  • 3 Applies the text styling to the entire page body

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!

Figure 9.9. The large, green, bold text indicates that outside style is affecting the contents of our Shadow DOM.

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.

Figure 9.10. When we place the class directly on the <span> tag, the Shadow DOM successfully blocks the style.

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.

Listing 9.5. Resetting the style in the Shadow DOM
<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>

  • 1 Applies initial styles to all elements in the shadow root

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!

9.3. Shadow DOM workout plan

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.

Figure 9.11. A demo app to browse exercises from a library and create a custom workout 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.

9.3.1. Application shell

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.

Figure 9.12. Basic file structure as we start our demo application

As with our other demos, our index.html will be extremely simple, as in the following listing.

Listing 9.6. The index.html for our demo application
<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>

  • 1 Component import
  • 2 Component declared in 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.

Listing 9.7. The main.css for our demo application
body {                            1
   margin: 0;
   padding: 0;
}

wkout-creator-app {               2
   height: calc(100vh - 20px);
   padding: 10px;
}

  • 1 Resets margin and padding on page
  • 2 Sizes the application to take up the entire page

For the <wkout-creator-app> itself, the component’s code, shown in the next listing, is also very simple.

Listing 9.8. The main application component for our demo application
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 );
}

  • 1 Uses the Shadow DOM in our component

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.

9.3.2. Host and ID selectors

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.

Listing 9.9. Application template module that defines our HTML and CSS
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>`;
   }
}

  • 1 Left container for the exercise library
  • 2 Divider line with an ID attribute
  • 3 Right container for workout plan list

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.

Figure 9.13. How our barebones application looks so far in a browser

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.

9.3.3. Grid and list containers

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.

Figure 9.14. Project file structure as our two container components are added for the exercise library and workout plan

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.

Listing 9.10. Workout plan component 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>`;
   },
}

  • 1 Assigns HTML/CSS to our component
  • 2 HTML to render
  • 3 CSS to render

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.

Listing 9.11. Exercise library component files
// 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>`;
    }
}

  • 1 Component definition module for the exercise library
  • 2 Template module for the exercise library, which holds our HTML and CSS
  • 3 Loops through exercises and renders them

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.

Figure 9.15. Filling in the components on the left and right sides of the application

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.

9.4. Adaptable components

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.

9.4.1. Creating the exercise component

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.

Figure 9.16. Final file structure for the application

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.

Listing 9.12. Component files for the exercise component
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);
}

  • 1 Getters/setters for each property
  • 2 Function to serialize all properties into an object
  • 3 Function to assemble an attribute string for a cloned exercise component

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>

9.4.2. Exercise component style

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.

Listing 9.13. First pass of the exercise component
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>`;
   }
}

  • 1 Styles the overall component
  • 2 Commented out thumbnail background
  • 3 Overall component style with a variation for a class on the component tag

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

Figure 9.17. Newly styled exercise components

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.

9.5. Updating the slider component

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.

Listing 9.14. Slider component constructor
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));
}

  • 1 Functionality in connectedCallback moved to constructor
  • 2 Attaches the Shadow DOM
  • 3 Uses the shadowRoot property instead of this for scope

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.

Listing 9.15. New slider template module
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>`;
    }
}

  • 1 Uses IDs to style instead of class
  • 2 References elements by ID instead of class
  • 3 Uses :host selector to style overall component
  • 4 Uses IDs to style instead of class

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.

Summary

In this chapter, we learned

  • How CSS styles can leak into and out of your Web Component just like anywhere else, if you’re not using the Shadow DOM
  • That the Shadow DOM completely protects your component’s DOM from outside CSS
  • That when using the Shadow DOM, we can be a lot less specific with our CSS selectors, taking full advantage of the separate DOM
  • How to use specific Shadow DOM CSS selectors to style your component, and style it differently in different contexts
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.37.68