Advanced Tooling for Web Components

Over the course of the last four articles in this five-part series, we’ve taken a broad look at the technologies that make up the Web Components standards. First, we looked at how to create HTML templates that could be consumed at a later time. Second, we dove into creating our own custom element. After that, we encapsulated our element’s styles and selectors into the shadow DOM, so that our element is entirely self-contained.

We’ve explored how powerful these tools can be by creating our own custom modal dialog, an element that can be used in most modern application contexts regardless of the underlying framework or library. In this article, we will look at how to consume our element in the various frameworks and look at some advanced tooling to really ramp up your Web Component skills.

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
  5. Advanced Tooling for Web Components (This post)

Framework agnostic

Our dialog component works great in almost any framework or even without one. (Granted, if JavaScript is disabled, the whole thing is for naught.) Angular and Vue treat Web Components as first-class citizens: the frameworks have been designed with web standards in mind. React is slightly more opinionated, but not impossible to integrate.

Angular

First, let’s take a look at how Angular handles custom elements. By default, Angular will throw a template error whenever it encounters an element it doesn’t recognize (i.e. the default browser elements or any of the components defined by Angular). This behavior can be changed by including the CUSTOM_ELEMENTS_SCHEMA.

…allows an NgModule to contain the following:

  • Non-Angular elements named with dash case (-).
  • Element properties named with dash case (-). Dash case is the naming convention for custom elements.

Angular Documentation

Consuming this schema is as simple as adding it to a module:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @NgModule({ /** Omitted */ schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}

That’s it. After this, Angular will allow us to use our custom element wherever we want with the standard property and event bindings:

<one-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)"> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div>
</one-dialog>

Vue

Vue’s compatibility with Web Components is even better than Angular’s as it doesn’t require any special configuration. Once an element is registered, it can be used with Vue’s default templating syntax:

<one-dialog v-bind:open="isDialogOpen" v-on:dialog-closed="dialogClosed"> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div>
</one-dialog>

One caveat with Angular and Vue, however, is their default form controls. If we wish to use something like reactive forms or [(ng-model)] in Angular or v-model in Vue on a custom element with a form control, we will need to set up that plumbing for which is beyond the scope of this article.

React

React is slightly more complicated than Angular. React’s virtual DOM effectively takes a JSX tree and renders it as a large object. So, instead of directly modifying attributes on HTML elements like Angular or Vue, React uses an object syntax to track changes that need to be made to the DOM and updates them in bulk. This works just fine in most cases. Our dialog’s open attribute is bound to its property and will respond perfectly well to changing props.

The catch comes when we start to look at the CustomEvent dispatched when our dialog closes. React implements a series of native event listeners for us with their synthetic event system. Unfortunately, that means that controls like onDialogClosed won’t actually attach event listeners to our component, so we have to find some other way.

The most obvious means of adding custom event listeners in React is by using DOM refs. In this model, we can reference our HTML node directly. The syntax is a bit verbose, but works great:

import React, { Component, createRef } from 'react'; export default class MyComponent extends Component { constructor(props) { super(props); // Create the ref this.dialog = createRef(); // Bind our method to the instance this.onDialogClosed = this.onDialogClosed.bind(this); this.state = { open: false }; } componentDidMount() { // Once the component mounds, add the event listener this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed); } componentWillUnmount() { // When the component unmounts, remove the listener this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed); } onDialogClosed(event) { /** Omitted **/ } render() { return <div> <one-dialog open={this.state.open} ref={this.dialog}> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div> </one-dialog> </div> }
}

Or, we can use stateless functional components and hooks:

import React, { useState, useEffect, useRef } from 'react'; export default function MyComponent(props) { const [ dialogOpen, setDialogOpen ] = useState(false); const oneDialog = useRef(null); const onDialogClosed = event => console.log(event); useEffect(() => { oneDialog.current.addEventListener('dialog-closed', onDialogClosed); return () => oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) }); return <div> <button onClick={() => setDialogOpen(true)}>Open dialog</button> <one-dialog ref={oneDialog} open={dialogOpen}> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div> </one-dialog> </div>
}

That’s not bad, but you can see how reusing this component could quickly become cumbersome. Luckily, we can export a default React component that wraps our custom element using the same tools.

import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types'; export default class OneDialog extends Component { constructor(props) { super(props); // Create the ref this.dialog = createRef(); // Bind our method to the instance this.onDialogClosed = this.onDialogClosed.bind(this); } componentDidMount() { // Once the component mounds, add the event listener this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed); } componentWillUnmount() { // When the component unmounts, remove the listener this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed); } onDialogClosed(event) { // Check to make sure the prop is present before calling it if (this.props.onDialogClosed) { this.props.onDialogClosed(event); } } render() { const { children, onDialogClosed, ...props } = this.props; return <one-dialog {...props} ref={this.dialog}> {children} </one-dialog> }
} OneDialog.propTypes = { children: children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node ]).isRequired, onDialogClosed: PropTypes.func
};

…or again as a stateless, functional component:

import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types'; export default function OneDialog(props) { const { children, onDialogClosed, ...restProps } = props; const oneDialog = useRef(null); useEffect(() => { onDialogClosed ? oneDialog.current.addEventListener('dialog-closed', onDialogClosed) : null; return () => { onDialogClosed ? oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null; }; }); return <one-dialog ref={oneDialog} {...restProps}>{children}</one-dialog>
}

Now we can use our dialog natively in React, but still keep the same API across all our applications (and still drop classes, if that’s your thing).

import React, { useState } from 'react';
import OneDialog from './OneDialog'; export default function MyComponent(props) { const [open, setOpen] = useState(false); return <div> <button onClick={() => setOpen(true)}>Open dialog</button> <OneDialog open={open} onDialogClosed={() => setOpen(false)}> <span slot="heading">Heading text</span> <div> <p>Body copy</p> </div> </OneDialog> </div>
}

Advanced tooling

There are a number of great tools for authoring your own custom elements. Searching through npm reveals a multitude of tools for creating highly-reactive custom elements (including my own pet project), but the most popular today by far is lit-html from the Polymer team and, more specifically for Web Components, LitElement.

LitElement is a custom elements base class that provides a series of APIs for doing all of the things we’ve walked through so far. It can be run in a browser without a build step, but if you enjoy using future-facing tools like decorators, there are utilities for that as well.

Before diving into how to use lit or LitElement, take a minute to familiarize yourself with tagged template literals, which are a special kind of function called on template literal strings in JavaScript. These functions take in an array of strings and a collection of interpolated values and can return anything you might want.

function tag(strings, ...values) { console.log({ strings, values }); return true;
}
const who = 'world'; tag`hello ${who}`; /** would log out { strings: ['hello ', ''], values: ['world'] } and return true **/

What LitElement gives us is live, dynamic updating of anything passed to that values array, so as a property updates, the element’s render function would be called and the resulting DOM would be re-rendered

import { LitElement, html } from 'lit-element'; class SomeComponent { static get properties() { return { now: { type: String } }; } connectedCallback() { // Be sure to call the super super.connectedCallback(); this.interval = window.setInterval(() => { this.now = Date.now(); }); } disconnectedCallback() { super.disconnectedCallback(); window.clearInterval(this.interval); } render() { return html`<h1>It is ${this.now}</h1>`; }
} customElements.define('some-component', SomeComponent);

See the Pen
LitElement now example
by Caleb Williams (@calebdwilliams)
on CodePen.

What you will notice is that we have to define any property we want LitElement to watch using the static properties getter. Using that API tells the base class to call render whenever a change is made to the component’s properties. render, in turn, will update only the nodes that need to change.

So, for our dialog example, it would look like this using LitElement:

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

There are several variants of lit-html available, including Haunted, a React hooks-style library for Web Components that can also make use of virtual components using lit-html as a base.

At the end of the day, most of the modern Web Components tools are various flavors of what LitElement is: a base class that abstracts common logic away from our components. Among the other flavors are Stencil, SkateJS, Angular Elements and Polymer.

What’s next

Web Components standards are continuing to evolve and new features are being discussed and added to browsers on an ongoing basis. Soon, Web Component authors will have APIs for interacting with web forms at a high level (including other element internals that are beyond the scope of these introductory articles), like native HTML and CSS module imports, native template instantiation and updating controls, and many more which can be tracked on the W3C/web components issues board on GitHub.

These standards are ready to adopt into our projects today with the appropriate polyfills for legacy browsers and Edge. And while they may not replace your framework of choice, they can be used alongside them to augment you and your organization’s workflows.

The post Advanced Tooling for Web Components appeared first on CSS-Tricks.

Web Standards Meet User-Land: Using CSS-in-JS to Style Custom Elements

The popularity of CSS-in-JS has mostly come from the React community, and indeed many CSS-in-JS libraries are React-specific. However, Emotion, the most popular library in terms of npm downloads, is framework agnostic.

Using the shadow DOM is common when creating custom elements, but there’s no requirement to do so. Not all use cases require that level of encapsulation. While it’s also possible to style custom elements with CSS in a regular stylesheet, we’re going to look at using Emotion.

We start with an install:

npm i emotion

Emotion offers the css function:

import {css} from 'emotion';

css is a tagged template literal. It accepts standard CSS syntax but adds support for Sass-style nesting.

const buttonStyles = css` color: white; font-size: 16px; background-color: blue; &:hover { background-color: purple; }
`

Once some styles have been defined, they need to be applied. Working with custom elements can be somewhat cumbersome. Libraries — like Stencil and LitElement — compile to web components, but offer a friendlier API than what we’d get right out of the box.

So, we’re going to define styles with Emotion and take advantage of both Stencil and LitElement to make working with web components a little easier.

Applying styles for Stencil

Stencil makes use of the bleeding-edge JavaScript decorators feature. An @Component decorator is used to provide metadata about the component. By default, Stencil won’t use shadow DOM, but I like to be explicit by setting shadow: false inside the @Component decorator:

@Component({ tag: 'fancy-button', shadow: false
})

Stencil uses JSX, so the styles are applied with a curly bracket ({}) syntax:

export class Button { render() { return <div><button class={buttonStyles}><slot/></button></div> }
}

Here’s how a simple example component would look in Stencil:

import { css, injectGlobal } from 'emotion';
import {Component} from '@stencil/core'; const buttonStyles = css` color: white; font-size: 16px; background-color: blue; &:hover { background-color: purple; }
`
@Component({ tag: 'fancy-button', shadow: false
})
export class Button { render() { return <div><button class={buttonStyles}><slot/></button></div> }
}

Applying styles for LitElement

LitElement, on the other hand, use shadow DOM by default. When creating a custom element with LitElement, the LitElement class is extended. LitElement has a createRenderRoot() method, which creates and opens a shadow DOM:

createRenderRoot() { return this.attachShadow({mode: 'open'});
}

Don’t want to make use of shadow DOM? That requires re-implementing this method inside the component class:

class Button extends LitElement { createRenderRoot() { return this; }
}

Inside the render function, we can reference the styles we defined using a template literal:

render() { return html`<button class=${buttonStyles}>hello world!</button>`
}

It’s worth noting that when using LitElement, we can only use a slot element when also using shadow DOM (Stencil does not have this problem).

Put together, we end up with:

import {LitElement, html} from 'lit-element';
import {css, injectGlobal} from 'emotion';
const buttonStyles = css` color: white; font-size: 16px; background-color: blue; &:hover { background-color: purple; }
` class Button extends LitElement { createRenderRoot() { return this; } render() { return html`<button class=${buttonStyles}>hello world!</button>` }
} customElements.define('fancy-button', Button);

Understanding Emotion

We don’t have to stress over naming our button — a random class name will be generated by Emotion.

We could make use of CSS nesting and attach a class only to a parent element. Alternatively, we can define styles as separate tagged template literals:

const styles = { heading: css` font-size: 24px; `, para: css` color: pink; `
} 

And then apply them separately to different HTML elements (this example uses JSX):

render() { return <div> <h2 class={styles.heading}>lorem ipsum</h2> <p class={styles.para}>lorem ipsum</p> </div>
}

Styling the container

So far, we’ve styled the inner contents of the custom element. To style the container itself, we need another import from Emotion.

import {css, injectGlobal} from 'emotion';

injectGlobal injects styles into the “global scope” (like writing regular CSS in a traditional stylesheet — rather than generating a random class name). Custom elements are display: inline by default (a somewhat odd decision from spec authors). In almost all cases, I change this default with a style applied to all instances of the component. Below are the buttonStyles which is how we can change that up, making use of injectGlobal:

injectGlobal`
fancy-button { display: block;
}
`

Why not just use shadow DOM?

If a component could end up in any codebase, then shadow DOM may well be a good option. It’s ideal for third party widgets — any CSS that’s applied to the page simply won’t break the the component, thanks to the isolated nature of shadow DOM. That’s why it’s used by Twitter embeds, to take one example. However, the vast majority of us make components for for a particular site or app and nowhere else. In that situation, shadow DOM can arguably add complexity with limited benefit.

The post Web Standards Meet User-Land: Using CSS-in-JS to Style Custom Elements appeared first on 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.