Tag: Shadow

Encapsulating Style and Structure with Shadow DOM

This is part four of a five-part series discussing the Web Components specifications. In part one, we took a 10,000-foot view of the specifications and what they do. In part two, we set out to build a custom modal dialog and created the HTML template for what would evolve into our very own custom HTML element in part three.

Article Series:

  1. An Introduction to Web Components
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch
  4. Encapsulating Style and Structure with Shadow DOM (This post)
  5. Advanced Tooling for Web Components (Coming soon!)

If you haven’t read those articles, you would be advised to do so now before proceeding in this article as this will continue to build upon the work we’ve done there.

When we last looked at our dialog component, it had a specific shape, structure and behaviors, however it relied heavily on the outside DOM and required that the consumers of our element would need to understand it’s general shape and structure, not to mention authoring all of their own styles (which would eventually modify the document’s global styles). And because our dialog relied on the contents of a template element with an id of “one-dialog”, each document could only have one instance of our modal.

The current limitations of our dialog component aren’t necessarily bad. Consumers who have an intimate knowledge of the dialog’s inner workings can easily consume and use the dialog by creating their own <template> element and defining the content and styles they wish to use (even relying on global styles defined elsewhere). However, we might want to provide more specific design and structural constraints on our element to accommodate best practices, so in this article, we will be incorporating the shadow DOM to our element.

What is the shadow DOM?

In our introduction article, we said that the shadow DOM was “capable of isolating CSS and JavaScript, almost like an <iframe>.” Like an <iframe>, selectors and styles inside of a shadow DOM node don’t leak outside of the shadow root and styles from outside the shadow root don’t leak in. There are a few exceptions that inherit from the parent document, like font family and document font sizes (e.g. rem) that can be overridden internally.

Unlike an <iframe>, however, all shadow roots still exist in the same document so that all code can be written inside a given context but not worry about conflicts with other styles or selectors.

Adding the shadow DOM to our dialog

To add a shadow root (the base node/document fragment of the shadow tree), we need to call our element’s attachShadow method:

class OneDialog extends HTMLElement {   constructor() {     super();     this.attachShadow({ mode: 'open' });     this.close = this.close.bind(this);   } }

By calling attachShadow with mode: 'open', we are telling our element to save a reference to the shadow root on the element.shadowRoot property. attachShadow always returns a reference to the shadow root, but here we don’t need to do anything with that.

If we had called the method with mode: 'closed', no reference would have been stored on the element and we would have to create our own means of storage and retrieval using a WeakMap or Object, setting the node itself as the key and the shadow root as the value.

const shadowRoots = new WeakMap();  class ClosedRoot extends HTMLElement {   constructor() {     super();     const shadowRoot = this.attachShadow({ mode: 'closed' });     shadowRoots.set(this, shadowRoot);   }    connectedCallback() {     const shadowRoot = shadowRoots.get(this);     shadowRoot.innerHTML = `<h1>Hello from a closed shadow root!</h1>`;   } }

We could also save a reference to the shadow root on our element itself, using a Symbol or other key to try to make the shadow root private.

In general, the closed mode for shadow roots exists for native elements that use the shadow DOM in their implementation (like <audio> or <video>). Further, for unit testing our elements, we might not have access to the shadowRoots object, making it unable for us to target changes inside our element depending on how our library is architected.

There might be some legitimate use cases for user-land closed shadow roots, but they are few and far between, so we’ll stick with the open shadow root for our dialog.

After implementing the new open shadow root, you might notice now that our element is completely broken when we try to run it:

See the Pen
Dialog example using template with shadow root
by Caleb Williams (@calebdwilliams)
on CodePen.

This is because all of the content we had before was added to and manipulated in the traditional DOM (what we’ll call the light DOM). Now that our element has a shadow DOM attached, there is no outlet for the light DOM to render. Let’s start fixing these issues by moving our content to the shadow DOM:

class OneDialog extends HTMLElement {   constructor() {     super();     this.attachShadow({ mode: 'open' });     this.close = this.close.bind(this);   }      connectedCallback() {     const { shadowRoot } = this;     const template = document.getElementById('one-dialog');     const node = document.importNode(template.content, true);     shadowRoot.appendChild(node);          shadowRoot.querySelector('button').addEventListener('click', this.close);     shadowRoot.querySelector('.overlay').addEventListener('click', this.close);     this.open = this.open;   }    disconnectedCallback() {     this.shadowRoot.querySelector('button').removeEventListener('click', this.close);     this.shadowRoot.querySelector('.overlay').removeEventListener('click', this.close);   }      set open(isOpen) {     const { shadowRoot } = this;     shadowRoot.querySelector('.wrapper').classList.toggle('open', isOpen);     shadowRoot.querySelector('.wrapper').setAttribute('aria-hidden', !isOpen);     if (isOpen) {       this._wasFocused = document.activeElement;       this.setAttribute('open', '');       document.addEventListener('keydown', this._watchEscape);       this.focus();       shadowRoot.querySelector('button').focus();     } else {       this._wasFocused && this._wasFocused.focus && this._wasFocused.focus();       this.removeAttribute('open');       document.removeEventListener('keydown', this._watchEscape);     }   }      close() {     this.open = false;   }      _watchEscape(event) {     if (event.key === 'Escape') {         this.close();        }   } }  customElements.define('one-dialog', OneDialog);

The major changes to our dialog so far are actually relatively minimal, but they carry a lot of impact. For starters, all our our selectors (including our style definitions) are internally scoped. For example, our dialog template only has one button internally, so our CSS only targets button { ... }, and those styles don’t bleed out to the light DOM.

We are, however, still reliant on the template that is external to our element. Let’s change that by removing the markup from our template and dropping it into our shadow root’s innerHTML.

See the Pen
Dialog example using only shadow root
by Caleb Williams (@calebdwilliams)
on CodePen.

Including content from the light DOM

The shadow DOM specification includes a means for allowing content from outside the shadow root to be rendered inside of our custom element. For those of you who remember AngularJS, this is a similar concept to ng-transclude or using props.children in React. With Web Components, this is done using the <slot> element.

A simple example would look like this:

<div>   <span>world <!-- this would be inserted into the slot element below --></span>   <#shadow-root><!-- pseudo code -->     <p>Hello <slot></slot></p>   </#shadow-root> </div>

A given shadow root can have any number of slot elements, which can be distinguished with a name attribute. The first slot inside of the shadow root without a name, will be the default slot and all content not otherwise assigned will flow inside that node. Our dialog really needs two slots: a heading and some content (which we’ll make default).

See the Pen
Dialog example using shadow root and slots
by Caleb Williams (@calebdwilliams)
on CodePen.

Go ahead and change the HTML portion of our dialog and see the result. Any content inside of the light DOM is inserted into the slot to which it is assigned. Slotted content remains inside the light DOM although it is rendered as if it were inside the shadow DOM. This means that these elements are still fully style-able by a consumer who might want to control the look and feel of their content.

A shadow root’s author can style content inside the light DOM to a limited extent using the CSS ::slotted() pseudo-selector; however, the DOM tree inside slotted is collapsed, so only simple selectors will work. In other words, we wouldn’t be able to style a <strong> element inside a <p> element within the flattened DOM tree in our previous example.

The best of both worlds

Our dialog is in a good state now: it has encapsulated, semantic markup, styles and behavior; however, some consumers of our dialog might still want to define their own template. Fortunately, by combining two techniques we’ve already learned, we can allow authors to optionally define an external template.

To do this, we will allow each instance of our component to reference an optional template ID. To start, we need to define a getter and setter for our component’s template.

get template() {   return this.getAttribute('template'); }  set template(template) {   if (template) {     this.setAttribute('template', template);   } else {     this.removeAttribute('template');   }   this.render(); }

Here we’re doing much the same thing that we did with our open property by tying it directly to its corresponding attribute. But at the bottom, we’re introducing a new method to our component: render. We are going to use our render method to insert our shadow DOM’s content and remove that behavior from the connectedCallback; instead, we will call render when our element is connected:

connectedCallback() {   this.render(); }  render() {   const { shadowRoot, template } = this;   const templateNode = document.getElementById(template);   shadowRoot.innerHTML = '';   if (templateNode) {     const content = document.importNode(templateNode.content, true);     shadowRoot.appendChild(content);   } else {     shadowRoot.innerHTML = `<!-- template text -->`;   }   shadowRoot.querySelector('button').addEventListener('click', this.close);   shadowRoot.querySelector('.overlay').addEventListener('click', this.close);   this.open = this.open; }

Our dialog now has some really basic default stylings, but also gives consumers the ability to define a new template for each instance. If we wanted, we could even use attributeChangedCallback to make this component update based on the template it’s currently pointing to:

static get observedAttributes() { return 'open', 'template']; }  attributeChangedCallback(attrName, oldValue, newValue) {   if (newValue !== oldValue) {     switch (attrName) {       /** Boolean attributes */       case 'open':         this[attrName] = this.hasAttribute(attrName);         break;       /** Value attributes */       case 'template':         this[attrName] = newValue;         break;     }   } }

See the Pen
Dialog example using shadow root, slots and template
by Caleb Williams (@calebdwilliams)
on CodePen.

In the demo above, changing the template attribute on our <one-dialog> element will alter which design is being used when the element is rendered.

Strategies for styling the shadow DOM

Currently, the only reliable way to style a shadow DOM node is by adding a <style> element to the shadow root’s inner HTML. This works fine in almost every case as browsers will de-duplicate stylesheets across these components, where possible. This does tend to add a bit of memory overhead, but generally not enough to notice.

Inside of these style tags, we can use CSS custom properties to provide an API for styling our components. Custom properties can pierce the shadow boundary and effect content inside a shadow node.

“Can we use a <link> element inside of a shadow root?” you might ask. And, in fact, we can. The trouble comes when trying to reuse this component across multiple applications as the CSS file might not be saved in a consistent location throughout all apps. However, if we are certain as to the element’s stylesheet location, then using <link> is an option. The same holds true for including an @import rule in a style tag.

CSS custom properties

One of the benefits of using CSS custom properties — also called CSS variables — is that they bleed through the shadow DOM. This is by design, giving component authors a surface for allowing theming and styling of their components from the outside. It is important to note, however, that since CSS cascades, changes to custom properties made inside a shadow root do not bleed back up.

See the Pen
CSS custom properties and shadow DOM
by Caleb Williams (@calebdwilliams)
on CodePen.

Go ahead and comment out or remove the variables set in the CSS panel of the demo above and see how this impacts the rendered content. Afterward, you can take a look at the styles in the shadow DOM’s innerHTML, you’ll see how the shadow DOM can define its own property that won’t affect the light DOM.

Constructible stylesheets

At the time of this writing, there is a proposed web feature that will allow for more modular styling of shadow DOM and light DOM elements using constructible stylesheets that has already landed in Chrome 73 and received positive signaling from Mozilla.

This feature would allow authors to define stylesheets in their JavaScript files similar to how they would write normal CSS and share those styles across multiple nodes. So, a single stylesheet could be appended to multiple shadow roots and potentially the document as well.

const everythingTomato = new CSSStyleSheet(); everythingTomato.replace('* { color: tomato; }');  document.adoptedStyleSheets = [everythingTomato];  class SomeCompoent extends HTMLElement {   constructor() {     super();     this.adoptedStyleSheets = [everythingTomato];   }      connectedCallback() {     this.shadowRoot.innerHTML = `<h1>CSS colors are fun</h1>`;   } }

In the above example, the everythingTomato stylesheet would be simultaneously applied to the shadow root and to the document’s body. This feature would be very useful for teams creating design systems and components that are intended to be shared across multiple applications and frameworks.

In the next demo, we can see a really basic example of how this can be utilized and the power that constructble stylesheets offer.

See the Pen
Construct style sheets demo
by Caleb Williams (@calebdwilliams)
on CodePen.

In this demo, we construct two stylesheets and append them to the document and to the custom element. After three seconds, we remove one stylesheet from our shadow root. For those three seconds, however, the document and the shadow DOM share the same stylesheet. Using the polyfill included in that demo, there are actually two style elements present, but Chrome Canary runs this natively.

That demo also includes a form for showing how a sheet’s rules can easily and effectively changed asynchronously as needed. This addition to the web platform can be a powerful ally for those creating design systems that span multiple frameworks or site authors who want to provide themes for their websites.

There is also a proposal for CSS Modules that could eventually be used with the adoptedStyleSheets feature. If implemented in its current form, this proposal would allow importing CSS as a module much like ECMAScript modules:

import styles './styles.css';  class SomeCompoent extends HTMLElement {   constructor() {     super();     this.adoptedStyleSheets = [styles];   } }

Part and theme

Another feature that is in the works for styling Web Components are the ::part() and ::theme() pseudo-selectors. The ::part() specification will allow authors to define parts of their custom elements that have a surface for styling:

class SomeOtherComponent extends HTMLElement {   connectedCallback() {     this.attachShadow({ mode: 'open' });     this.shadowRoot.innerHTML = `       <style>h1 { color: rebeccapurple; }</style>       <h1>Web components are <span part="description">AWESOME</span></h1>     `;   } }      customElements.define('other-component', SomeOtherComponent);

In our global CSS, we could target any element that has a part called description by invoking the CSS ::part() selector.

other-component::part(description) {   color: tomato; }

In the above example, the primary message of the <h1> tag would be in a different color than the description part, giving custom element authors the ability to expose styling APIs for their components and maintain control over the pieces they want to maintain control over.

The difference between ::part() and ::theme() is that ::part() must be specifically selected whereas ::theme() can be nested at any level. The following would have the same effect as the above CSS, but would also work for any other element that included a part="description" in the entire document tree.

:root::theme(description) {   color: tomato; }

Like constructible stylesheets, ::part() has landed in Chrome 73.

Wrapping up

Our dialog component is now complete, more-or-less. It includes its own markup, styles (without any outside dependencies) and behaviors. This component can now be included in projects that use any current or future frameworks because they are built against the browser specifications instead of third-party APIs.

Some of the core controls are a little verbose and do rely on at least a moderate knowledge of how the DOM works. In our final article, we will discuss higher-level tooling and how to incorporate with popular frameworks.

Article Series:

  1. An Introduction to Web Components
  2. Crafting Reusable HTML Templates
  3. Creating a Custom Element from Scratch
  4. Encapsulating Style and Structure with Shadow DOM (This post)
  5. Advanced Tooling for Web Components (Coming soon!)

The post Encapsulating Style and Structure with Shadow DOM appeared first on CSS-Tricks.

CSS-Tricks

, , ,

Extracting Text from Content Using HTML Slot, HTML Template and Shadow DOM

Chapter names in books, quotes from a speech, keywords in an article, stats on a report — these are all types of content that could be helpful to isolate and turn into a high-level summary of what’s important.

For example, have you seen the way Business Insider provides an article’s key points before getting into the content?

That’s the sort of thing we’re going to do, but try to extract the high points directly from the article using HTML Slot, HTML Template and Shadow DOM.

These three titular specifications are typically used as part of Web Components — fully functioning custom element modules meant to be reused in webpages.

Now, what we aim to do, i.e. text extraction, doesn’t need custom elements, but it can make use of those three technologies.

There is a more rudimentary approach to do this. For example, we could extract text and show the extracted text on a page with some basic script without utilizing slot and template. So why use them if we can go with something more familiar?

The reason is that using these technologies permits us a preset markup code (also optionally, style or script) for our extracted text in HTML. We’ll see that as we proceed with this article.

Now, as a very watered-down definition of the technologies we’ll be using, I’d say:

  • A template is a set of markup that can be reused in a page.
  • A slot is a placeholder spot for a designated element from the page.
  • A shadow DOM is a DOM tree that doesn’t really exist on the page till we add it using script.

We’ll see them in a little more depth once we get into coding. For now, what we’re going to make is an article that follows with a list of key points from the text. And, you probably guessed it, those key points are extracted from the article text and compiled into the key points section.

See the Pen
Text Extraction with HTML Slot and HTML Template
by Preethi Sam (@rpsthecoder)
on CodePen.

The key points are displayed as a list with a design in between the points. So, let’s first create a template for that list and designate a place for the list to go.

<article><!-- Article content --></article>  <!-- Section where the extracted keypoints will be displayed --> <section id='keyPointsSection'>   <h2>Key Points:</h2>   <ul><!-- Extracted key points will go in here --></ul> </section>  <!-- Template for the key points list --> <template id='keyPointsTemplate'>   <li><slot name='keyPoints'></slot></li>   <li style="text-align: center;">&#x2919;&mdash;&#x291a;</li> </template>

What we’ve got is a semantic <section> with a <ul> where the list of key points will go. Then we have a <template> for the list items that has two <li> elements: one with a <slot> placeholder for the key points from the article and another with a centered design.

The layout is arbitrary. What’s important is placing a <slot> where the extracted key points will go. Whatever’s inside the <template> will not be rendered on the page until we add it to the page using script.

Further, the markup inside <template> can be styled using inline styles, or CSS enclosed by <style>:

<template id='keyPointsTemplate'>     <li><slot name='keyPoints'></slot></li>     <li style="text-align: center;">&#x2919;&mdash;&#x291a;</li>     <style>         li{/* Some style */}     </style> </template>

The fun part! Let’s pick the key points from the article. Notice the value of the name attribute for the <slot> inside the <template> (keyPoints) because we’ll need that.

<article>   <h1>Bears</h1>   <p>Bears are carnivoran mammals of the family Ursidae. <span><span slot='keyPoints'>They are classified as caniforms, or doglike carnivorans</span></span>. Although only eight species of bears <!-- more content --> and partially in the Southern Hemisphere. <span><span slot='keyPoints'>Bears are found on the continents of North America, South America, Europe, and Asia</span></span>.<!-- more content --></p>   <p>While the polar bear is mostly carnivorous, <!-- more content -->. Bears use shelters, such as caves and logs, as their dens; <span><span slot='keyPoints'>Most species occupy their dens during the winter for a long period of hibernation</span></span>, up to 100 days.</p>   <!-- More paragraphs -->  </article>

The key points are wrapped in a <span> carrying a slot attribute value (“keyPoints“) matching the name of the <slot> placeholder inside the <template>.

Notice, too, that I’ve added another outer <span> wrapping the key points.

The reason is that slot names are usually unique and are not repeated, because one <slot> matches one element using one slot name. If there’re more than one element with the same slot name, the <slot> placeholder will be replaced by all those elements consecutively, ending in the last element being the final content at the placeholder.

So, if we matched that one single <slot> inside the <template> against all of the <span> elements with the same slot attribute value (our key points) in a paragraph or the whole article, we’d end up with only the last key point present in the paragraph or the article in place of the <slot>.

That’s not what we need. We need to show all the key points. So, we’re wrapping the key points with an outer <span> to match each of those individual key points separately with the <slot>. This is much more obvious by looking at the script, so let’s do that.

const keyPointsTemplate = document.querySelector('#keyPointsTemplate').content; const keyPointsSection = document.querySelector('#keyPointsSection > ul'); /* Loop through elements with 'slot' attribute */ document.querySelectorAll('[slot]').forEach((slot)=>{   let span = slot.parentNode.cloneNode(true);   span.attachShadow({  mode: 'closed' }).appendChild(keyPointsTemplate.cloneNode(true));   keyPointsSection.appendChild(span); });

First, we loop through every <span> with a slot attribute and get a copy of its parent (the outer <span>). Note that we could also loop through the outer <span> directly if we’d like, by giving them a common class value.

The outer <span> copy is then attached with a shadow tree (span.attachShadow) made up of a clone of the template’s content (keyPointsTemplate.cloneNode(true)).

This “attachment” causes the <slot> inside the template’s list item in the shadow tree to absorb the inner <span> carrying its matching slot name, i.e. our key point.

The slotted key point is then added to the key points section at the end of the page (keyPointsSection.appendChild(span)).

This happens with all the key points in the course of the loop.

That’s really about it. We’ve snagged all of the key points in the article, made copies of them, then dropped the copies into the list template so that all of the key points are grouped together providing a nice little CliffsNotes-like summary of the article.

Here’s that demo once again:

See the Pen
Text Extraction with HTML Slot and HTML Template
by Preethi Sam (@rpsthecoder)
on CodePen.

What do you think of this technique? Is it something that would be useful in long-form content, like blog posts, news articles, or even Wikipedia entries? What other use cases can you think of?

The post Extracting Text from Content Using HTML Slot, HTML Template and Shadow DOM appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , , ,
[Top]