Tag: Components

Using Web Components With Next (or Any SSR Framework)

In my previous post we looked at Shoelace, which is a component library with a full suite of UX components that are beautiful, accessible, and — perhaps unexpectedly — built with Web Components. This means they can be used with any JavaScript framework. While React’s Web Component interoperability is, at present, less than ideal, there are workarounds.

But one serious shortcoming of Web Components is their current lack of support for server-side rendering (SSR). There is something called the Declarative Shadow DOM (DSD) in the works, but current support for it is pretty minimal, and it actually requires buy-in from your web server to emit special markup for the DSD. There’s currently work being done for Next.js that I look forward to seeing. But for this post, we’ll look at how to manage Web Components from any SSR framework, like Next.js, today.

We’ll wind up doing a non-trivial amount of manual work, and slightly hurting our page’s startup performance in the process. We’ll then look at how to minimize these performance costs. But make no mistake: this solution is not without tradeoffs, so don’t expect otherwise. Always measure and profile.

The problem

Before we dive in, let’s take a moment and actually explain the problem. Why don’t Web Components work well with server-side rendering?

Application frameworks like Next.js take React code and run it through an API to essentially “stringify” it, meaning it turns your components into plain HTML. So the React component tree will render on the server hosting the web app, and that HTML will be sent down with the rest of the web app’s HTML document to your user’s browser. Along with this HTML are some <script> tags that load React, along with the code for all your React components. When a browser processes these <script> tags, React will re-render the component tree, and match things up with the SSR’d HTML that was sent down. At this point, all of the effects will start running, the event handlers will wire up, and the state will actually… contain state. It’s at this point that the web app becomes interactive. The process of re-processing your component tree on the client, and wiring everything up is called hydration.

So, what does this have to do with Web Components? Well, when you render something, say the same Shoelace <sl-tab-group> component we visited last time:

<sl-tab-group ref="{tabsRef}">   <sl-tab slot="nav" panel="general"> General </sl-tab>   <sl-tab slot="nav" panel="custom"> Custom </sl-tab>   <sl-tab slot="nav" panel="advanced"> Advanced </sl-tab>   <sl-tab slot="nav" panel="disabled" disabled> Disabled </sl-tab>    <sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>   <sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>   <sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>   <sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel> </sl-tab-group>

…React (or honestly any JavaScript framework) will see those tags and simply pass them along. React (or Svelte, or Solid) are not responsible for turning those tags into nicely-formatted tabs. The code for that is tucked away inside of whatever code you have that defines those Web Components. In our case, that code is in the Shoelace library, but the code can be anywhere. What’s important is when the code runs.

Normally, the code registering these Web Components will be pulled into your application’s normal code via a JavaScript import. That means this code will wind up in your JavaScript bundle and execute during hydration which means that, between your user first seeing the SSR’d HTML and hydration happening, these tabs (or any Web Component for that matter) will not render the correct content. Then, when hydration happens, the proper content will display, likely causing the content around these Web Components to move around and fit the properly formatted content. This is known as a flash of unstyled content, or FOUC. In theory, you could stick markup in between all of those <sl-tab-xyz> tags to match the finished output, but this is all but impossible in practice, especially for a third-party component library like Shoelace.

Moving our Web Component registration code

So the problem is that the code to make Web Components do what they need to do won’t actually run until hydration occurs. For this post, we’ll look at running that code sooner; immediately, in fact. We’ll look at custom bundling our Web Component code, and manually adding a script directly to our document’s <head> so it runs immediately, and blocks the rest of the document until it does. This is normally a terrible thing to do. The whole point of server-side rendering is to not block our page from processing until our JavaScript has processed. But once done, it means that, as the document is initially rendering our HTML from the server, the Web Components will be registered and will both immediately and synchronously emit the right content.

In our case, we’re just looking to run our Web Component registration code in a blocking script. This code isn’t huge, and we’ll look to significantly lessen the performance hit by adding some cache headers to help with subsequent visits. This isn’t a perfect solution. The first time a user browses your page will always block while that script file is loaded. Subsequent visits will cache nicely, but this tradeoff might not be feasible for you — e-commerce, anyone? Anyway, profile, measure, and make the right decision for your app. Besides, in the future it’s entirely possible Next.js will fully support DSD and Web Components.

Getting started

All of the code we’ll be looking at is in this GitHub repo and deployed here with Vercel. The web app renders some Shoelace components along with text that changes color and content upon hydration. You should be able to see the text change to “Hydrated,” with the Shoelace components already rendering properly.

Custom bundling Web Component code

Our first step is to create a single JavaScript module that imports all of our Web Component definitions. For the Shoelace components I’m using, my code looks like this:

import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";  import "@shoelace-style/shoelace/dist/components/tab/tab.js"; import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";  import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";  setDefaultAnimation("dialog.show", {   keyframes: [     { opacity: 0, transform: "translate3d(0px, -20px, 0px)" },     { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },   ],   options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" }, }); setDefaultAnimation("dialog.hide", {   keyframes: [     { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },     { opacity: 0, transform: "translate3d(0px, 20px, 0px)" },   ],   options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" }, });

It loads the definitions for the <sl-tab-group> and <sl-dialog> components, and overrides some default animations for the dialog. Simple enough. But the interesting piece here is getting this code into our application. We cannot simply import this module. If we did that, it’d get bundled into our normal JavaScript bundles and run during hydration. This would cause the FOUC we’re trying to avoid.

While Next.js does have a number of webpack hooks to custom bundle things, I’ll use Vite instead. First, install it with npm i vite and then create a vite.config.js file. Mine looks like this:

import { defineConfig } from "vite"; import path from "path";  export default defineConfig({   build: {     outDir: path.join(__dirname, "./shoelace-dir"),     lib: {       name: "shoelace",       entry: "./src/shoelace-bundle.js",       formats: ["umd"],       fileName: () => "shoelace-bundle.js",     },     rollupOptions: {       output: {         entryFileNames: `[name]-[hash].js`,       },     },   }, });

This will build a bundle file with our Web Component definitions in the shoelace-dir folder. Let’s move it over to the public folder so that Next.js will serve it. And we should also keep track of the exact name of the file, with the hash on the end of it. Here’s a Node script that moves the file and writes a JavaScript module that exports a simple constant with the name of the bundle file (this will come in handy shortly):

const fs = require("fs"); const path = require("path");  const shoelaceOutputPath = path.join(process.cwd(), "shoelace-dir"); const publicShoelacePath = path.join(process.cwd(), "public", "shoelace");  const files = fs.readdirSync(shoelaceOutputPath);  const shoelaceBundleFile = files.find(name => /^shoelace-bundle/.test(name));  fs.rmSync(publicShoelacePath, { force: true, recursive: true });  fs.mkdirSync(publicShoelacePath, { recursive: true }); fs.renameSync(path.join(shoelaceOutputPath, shoelaceBundleFile), path.join(publicShoelacePath, shoelaceBundleFile)); fs.rmSync(shoelaceOutputPath, { force: true, recursive: true });  fs.writeFileSync(path.join(process.cwd(), "util", "shoelace-bundle-info.js"), `export const shoelacePath = "/shoelace/$ {shoelaceBundleFile}";`);

Here’s a companion npm script:

"bundle-shoelace": "vite build && node util/process-shoelace-bundle",

That should work. For me, util/shoelace-bundle-info.js now exists, and looks like this:

export const shoelacePath = "/shoelace/shoelace-bundle-a6f19317.js";

Loading the script

Let’s go into the Next.js _document.js file and pull in the name of our Web Component bundle file:

import { shoelacePath } from "../util/shoelace-bundle-info";

Then we manually render a <script> tag in the <head>. Here’s what my entire _document.js file looks like:

import { Html, Head, Main, NextScript } from "next/document"; import { shoelacePath } from "../util/shoelace-bundle-info";  export default function Document() {   return (     <Html>       <Head>         <script src={shoelacePath}></script>       </Head>       <body>         <Main />         <NextScript />       </body>     </Html>   ); }

And that should work! Our Shoelace registration will load in a blocking script and be available immediately as our page processes the initial HTML.

Improving performance

We could leave things as they are but let’s add caching for our Shoelace bundle. We’ll tell Next.js to make these Shoelace bundles cacheable by adding the following entry to our Next.js config file:

async headers() {   return [     {       source: "/shoelace/shoelace-bundle-:hash.js",       headers: [         {           key: "Cache-Control",           value: "public,max-age=31536000,immutable",         },       ],     },   ]; }

Now, on subsequent browses to our site, we see the Shoelace bundle caching nicely!

DevTools Sources panel open and showing the loaded Shoelace bundle.

If our Shoelace bundle ever changes, the file name will change (via the :hash portion from the source property above), the browser will find that it does not have that file cached, and will simply request it fresh from the network.

Wrapping up

This may have seemed like a lot of manual work; and it was. It’s unfortunate Web Components don’t offer better out-of-the-box support for server-side rendering.

But we shouldn’t forget the benefits they provide: it’s nice being able to use quality UX components that aren’t tied to a specific framework. It’s aldo nice being able to experiment with brand new frameworks, like Solid, without needing to find (or hack together) some sort of tab, modal, autocomplete, or whatever component.


Using Web Components With Next (or Any SSR Framework) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , ,

Building Interoperable Web Components That Even Work With React

Those of us who’ve been web developers more than a few years have probably written code using more than one JavaScript framework. With all the choices out there — React, Svelte, Vue, Angular, Solid — it’s all but inevitable. One of the more frustrating things we have to deal with when working across frameworks is re-creating all those low-level UI components: buttons, tabs, dropdowns, etc. What’s particularly frustrating is that we’ll typically have them defined in one framework, say React, but then need to rewrite them if we want to build something in Svelte. Or Vue. Or Solid. And so on.

Wouldn’t it be better if we could define these low-level UI components once, in a framework-agnostic way, and then re-use them between frameworks? Of course it would! And we can; web components are the way. This post will show you how.

As of now, the SSR story for web components is a bit lacking. Declarative shadow DOM (DSD) is how a web component is server-side rendered, but, as of this writing, it’s not integrated with your favorite application frameworks like Next, Remix or SvelteKit. If that’s a requirement for you, be sure to check the latest status of DSD. But otherwise, if SSR isn’t something you’re using, read on.

First, some context

Web Components are essentially HTML elements that you define yourself, like <yummy-pizza> or whatever, from the ground up. They’re covered all over here at CSS-Tricks (including an extensive series by Caleb Williams and one by John Rhea) but we’ll briefly walk through the process. Essentially, you define a JavaScript class, inherit it from HTMLElement, and then define whatever properties, attributes and styles the web component has and, of course, the markup it will ultimately render to your users.

Being able to define custom HTML elements that aren’t bound to any particular component is exciting. But this freedom is also a limitation. Existing independently of any JavaScript framework means you can’t really interact with those JavaScript frameworks. Think of a React component which fetches some data and then renders some other React component, passing along the data. This wouldn’t really work as a web component, since a web component doesn’t know how to render a React component.

Web components particularly excel as leaf components. Leaf components are the last thing to be rendered in a component tree. These are the components which receive some props, and render some UI. These are not the components sitting in the middle of your component tree, passing data along, setting context, etc. — just pure pieces of UI that will look the same, no matter which JavaScript framework is powering the rest of the app.

The web component we’re building

Rather than build something boring (and common), like a button, let’s build something a little bit different. In my last post we looked at using blurry image previews to prevent content reflow, and provide a decent UI for users while our images load. We looked at base64 encoding a blurry, degraded versions of our images, and showing that in our UI while the real image loaded. We also looked at generating incredibly compact, blurry previews using a tool called Blurhash.

That post showed you how to generate those previews and use them in a React project. This post will show you how to use those previews from a web component so they can be used by any JavaScript framework.

But we need to walk before we can run, so we’ll walk through something trivial and silly first to see exactly how web components work.

Everything in this post will build vanilla web components without any tooling. That means the code will have a bit of boilerplate, but should be relatively easy to follow. Tools like Lit or Stencil are designed for building web components and can be used to remove much of this boilerplate. I urge you to check them out! But for this post, I’ll prefer a little more boilerplate in exchange for not having to introduce and teach another dependency.

A simple counter component

Let’s build the classic “Hello World” of JavaScript components: a counter. We’ll render a value, and a button that increments that value. Simple and boring, but it’ll let us look at the simplest possible web component.

In order to build a web component, the first step is to make a JavaScript class, which inherits from HTMLElement:

class Counter extends HTMLElement {}

The last step is to register the web component, but only if we haven’t registered it already:

if (!customElements.get("counter-wc")) {   customElements.define("counter-wc", Counter); }

And, of course, render it:

<counter-wc></counter-wc>

And everything in between is us making the web component do whatever we want it to. One common lifecycle method is connectedCallback, which fires when our web component is added to the DOM. We could use that method to render whatever content we’d like. Remember, this is a JS class inheriting from HTMLElement, which means our this value is the web component element itself, with all the normal DOM manipulation methods you already know and love.

At it’s most simple, we could do this:

class Counter extends HTMLElement {   connectedCallback() {     this.innerHTML = "<div style='color: green'>Hey</div>";   } }  if (!customElements.get("counter-wc")) {   customElements.define("counter-wc", Counter); }

…which will work just fine.

The word "hey" in green.

Adding real content

Let’s add some useful, interactive content. We need a <span> to hold the current number value and a <button> to increment the counter. For now, we’ll create this content in our constructor and append it when the web component is actually in the DOM:

constructor() {   super();   const container = document.createElement('div');    this.valSpan = document.createElement('span');    const increment = document.createElement('button');   increment.innerText = 'Increment';   increment.addEventListener('click', () => {     this.#value = this.#currentValue + 1;   });    container.appendChild(this.valSpan);   container.appendChild(document.createElement('br'));   container.appendChild(increment);    this.container = container; }  connectedCallback() {   this.appendChild(this.container);   this.update(); }

If you’re really grossed out by the manual DOM creation, remember you can set innerHTML, or even create a template element once as a static property of your web component class, clone it, and insert the contents for new web component instances. There’s probably some other options I’m not thinking of, or you can always use a web component framework like Lit or Stencil. But for this post, we’ll continue to keep it simple.

Moving on, we need a settable JavaScript class property named value

#currentValue = 0;  set #value(val) {   this.#currentValue = val;   this.update(); }

It’s just a standard class property with a setter, along with a second property to hold the value. One fun twist is that I’m using the private JavaScript class property syntax for these values. That means nobody outside our web component can ever touch these values. This is standard JavaScript that’s supported in all modern browsers, so don’t be afraid to use it.

Or feel free to call it _value if you prefer. And, lastly, our update method:

update() {   this.valSpan.innerText = this.#currentValue; }

It works!

The counter web component.

Obviously this is not code you’d want to maintain at scale. Here’s a full working example if you’d like a closer look. As I’ve said, tools like Lit and Stencil are designed to make this simpler.

Adding some more functionality

This post is not a deep dive into web components. We won’t cover all the APIs and lifecycles; we won’t even cover shadow roots or slots. There’s endless content on those topics. My goal here is to provide a decent enough introduction to spark some interest, along with some useful guidance on actually using web components with the popular JavaScript frameworks you already know and love.

To that end, let’s enhance our counter web component a bit. Let’s have it accept a color attribute, to control the color of the value that’s displayed. And let’s also have it accept an increment property, so consumers of this web component can have it increment by 2, 3, 4 at a time. And to drive these state changes, let’s use our new counter in a Svelte sandbox — we’ll get to React in a bit.

We’ll start with the same web component as before and add a color attribute. To configure our web component to accept and respond to an attribute, we add a static observedAttributes property that returns the attributes that our web component listens for.

static observedAttributes = ["color"];

With that in place, we can add a attributeChangedCallback lifecycle method, which will run whenever any of the attributes listed in observedAttributes are set, or updated.

attributeChangedCallback(name, oldValue, newValue) {   if (name === "color") {     this.update();   } }

Now we update our update method to actually use it:

update() {   this.valSpan.innerText = this._currentValue;   this.valSpan.style.color = this.getAttribute("color") || "black"; }

Lastly, let’s add our increment property:

increment = 1;

Simple and humble.

Using the counter component in Svelte

Let’s use what we just made. We’ll go into our Svelte app component and add something like this:

<script>   let color = "red"; </script>  <style>   main {     text-align: center;   } </style>  <main>   <select bind:value={color}>     <option value="red">Red</option>     <option value="green">Green</option>     <option value="blue">Blue</option>   </select>    <counter-wc color={color}></counter-wc> </main>

And it works! Our counter renders, increments, and the dropdown updates the color. As you can see, we render the color attribute in our Svelte template and, when the value changes, Svelte handles the legwork of calling setAttribute on our underlying web component instance. There’s nothing special here: this is the same thing it already does for the attributes of any HTML element.

Things get a little bit interesting with the increment prop. This is not an attribute on our web component; it’s a prop on the web component’s class. That means it needs to be set on the web component’s instance. Bear with me, as things will wind up much simpler in a bit.

First, we’ll add some variables to our Svelte component:

let increment = 1; let wcInstance;

Our powerhouse of a counter component will let you increment by 1, or by 2:

<button on:click={() => increment = 1}>Increment 1</button> <button on:click={() => increment = 2}>Increment 2</button>

But, in theory, we need to get the actual instance of our web component. This is the same thing we always do anytime we add a ref with React. With Svelte, it’s a simple bind:this directive:

<counter-wc bind:this={wcInstance} color={color}></counter-wc>

Now, in our Svelte template, we listen for changes to our component’s increment variable and set the underlying web component property.

$ : {   if (wcInstance) {     wcInstance.increment = increment;   } }

You can test it out over at this live demo.

We obviously don’t want to do this for every web component or prop we need to manage. Wouldn’t it be nice if we could just set increment right on our web component, in markup, like we normally do for component props, and have it, you know, just work? In other words, it’d be nice if we could delete all usages of wcInstance and use this simpler code instead:

<counter-wc increment={increment} color={color}></counter-wc>

It turns out we can. This code works; Svelte handles all that legwork for us. Check it out in this demo. This is standard behavior for pretty much all JavaScript frameworks.

So why did I show you the manual way of setting the web component’s prop? Two reasons: it’s useful to understand how these things work and, a moment ago, I said this works for “pretty much” all JavaScript frameworks. But there’s one framework which, maddeningly, does not support web component prop setting like we just saw.

React is a different beast

React. The most popular JavaScript framework on the planet does not support basic interop with web components. This is a well-known problem that’s unique to React. Interestingly, this is actually fixed in React’s experimental branch, but for some reason wasn’t merged into version 18. That said, we can still track the progress of it. And you can try this yourself with a live demo.

The solution, of course, is to use a ref, grab the web component instance, and manually set increment when that value changes. It looks like this:

import React, { useState, useRef, useEffect } from 'react'; import './counter-wc';  export default function App() {   const [increment, setIncrement] = useState(1);   const [color, setColor] = useState('red');   const wcRef = useRef(null);    useEffect(() => {     wcRef.current.increment = increment;   }, [increment]);    return (     <div>       <div className="increment-container">         <button onClick={() => setIncrement(1)}>Increment by 1</button>         <button onClick={() => setIncrement(2)}>Increment by 2</button>       </div>        <select value={color} onChange={(e) => setColor(e.target.value)}>         <option value="red">Red</option>         <option value="green">Green</option>         <option value="blue">Blue</option>       </select>        <counter-wc ref={wcRef} increment={increment} color={color}></counter-wc>     </div>   ); }

As we discussed, coding this up manually for every web component property is simply not scalable. But all is not lost because we have a couple of options.

Option 1: Use attributes everywhere

We have attributes. If you clicked the React demo above, the increment prop wasn’t working, but the color correctly changed. Can’t we code everything with attributes? Sadly, no. Attribute values can only be strings. That’s good enough here, and we’d be able to get somewhat far with this approach. Numbers like increment can be converted to and from strings. We could even JSON stringify/parse objects. But eventually we’ll need to pass a function into a web component, and at that point we’d be out of options.

Option 2: Wrap it

There’s an old saying that you can solve any problem in computer science by adding a level of indirection (except the problem of too many levels of indirection). The code to set these props is pretty predictable and simple. What if we hide it in a library? The smart folks behind Lit have one solution. This library creates a new React component for you after you give it a web component, and list out the properties it needs. While clever, I’m not a fan of this approach.

Rather than have a one-to-one mapping of web components to manually-created React components, what I prefer is just one React component that we pass our web component tag name to (counter-wc in our case) — along with all the attributes and properties — and for this component to render our web component, add the ref, then figure out what is a prop and what is an attribute. That’s the ideal solution in my opinion. I don’t know of a library that does this, but it should be straightforward to create. Let’s give it a shot!

This is the usage we’re looking for:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

wcTag is the web component tag name; the rest are the properties and attributes we want passed along.

Here’s what my implementation looks like:

import React, { createElement, useRef, useLayoutEffect, memo } from 'react';  const _WcWrapper = (props) => {   const { wcTag, children, ...restProps } = props;   const wcRef = useRef(null);    useLayoutEffect(() => {     const wc = wcRef.current;      for (const [key, value] of Object.entries(restProps)) {       if (key in wc) {         if (wc[key] !== value) {           wc[key] = value;         }       } else {         if (wc.getAttribute(key) !== value) {           wc.setAttribute(key, value);         }       }     }   });    return createElement(wcTag, { ref: wcRef }); };  export const WcWrapper = memo(_WcWrapper);

The most interesting line is at the end:

return createElement(wcTag, { ref: wcRef });

This is how we create an element in React with a dynamic name. In fact, this is what React normally transpiles JSX into. All our divs are converted to createElement("div") calls. We don’t normally need to call this API directly but it’s there when we need it.

Beyond that, we want to run a layout effect and loop through every prop that we’ve passed to our component. We loop through all of them and check to see if it’s a property with an in check that checks the web component instance object as well as its prototype chain, which will catch any getters/setters that wind up on the class prototype. If no such property exists, it’s assumed to be an attribute. In either case, we only set it if the value has actually changed.

If you’re wondering why we use useLayoutEffect instead of useEffect, it’s because we want to immediately run these updates before our content is rendered. Also, note that we have no dependency array to our useLayoutEffect; this means we want to run this update on every render. This can be risky since React tends to re-render a lot. I ameliorate this by wrapping the whole thing in React.memo. This is essentially the modern version of React.PureComponent, which means the component will only re-render if any of its actual props have changed — and it checks whether that’s happened via a simple equality check.

The only risk here is that if you’re passing an object prop that you’re mutating directly without re-assigning, then you won’t see the updates. But this is highly discouraged, especially in the React community, so I wouldn’t worry about it.

Before moving on, I’d like to call out one last thing. You might not be happy with how the usage looks. Again, this component is used like this:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

Specifically, you might not like passing the web component tag name to the <WcWrapper> component and prefer instead the @lit-labs/react package above, which creates a new individual React component for each web component. That’s totally fair and I’d encourage you to use whatever you’re most comfortable with. But for me, one advantage with this approach is that it’s easy to delete. If by some miracle React merges proper web component handling from their experimental branch into main tomorrow, you’d be able to change the above code from this:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

…to this:

<counter-wc ref={wcRef} increment={increment} color={color} />

You could probably even write a single codemod to do that everywhere, and then delete <WcWrapper> altogether. Actually, scratch that: a global search and replace with a RegEx would probably work.

The implementation

I know, it seems like it took a journey to get here. If you recall, our original goal was to take the image preview code we looked at in my last post, and move it to a web component so it can be used in any JavaScript framework. React’s lack of proper interop added a lot of detail to the mix. But now that we have a decent handle on how to create a web component, and use it, the implementation will almost be anti-climactic.

I’ll drop the entire web component here and call out some of the interesting bits. If you’d like to see it in action, here’s a working demo. It’ll switch between my three favorite books on my three favorite programming languages. The URL for each book will be unique each time, so you can see the preview, though you’ll likely want to throttle things in your DevTools Network tab to really see things taking place.

View entire code
class BookCover extends HTMLElement {   static observedAttributes = ['url'];    attributeChangedCallback(name, oldValue, newValue) {     if (name === 'url') {       this.createMainImage(newValue);     }   }    set preview(val) {     this.previewEl = this.createPreview(val);     this.render();   }    createPreview(val) {     if (typeof val === 'string') {       return base64Preview(val);     } else {       return blurHashPreview(val);     }   }    createMainImage(url) {     this.loaded = false;     const img = document.createElement('img');     img.alt = 'Book cover';     img.addEventListener('load', () =&gt; {       if (img === this.imageEl) {         this.loaded = true;         this.render();       }     });     img.src = url;     this.imageEl = img;   }    connectedCallback() {     this.render();   }    render() {     const elementMaybe = this.loaded ? this.imageEl : this.previewEl;     syncSingleChild(this, elementMaybe);   } }

First, we register the attribute we’re interested in and react when it changes:

static observedAttributes = ['url'];  attributeChangedCallback(name, oldValue, newValue) {   if (name === 'url') {     this.createMainImage(newValue);   } }

This causes our image component to be created, which will show only when loaded:

createMainImage(url) {   this.loaded = false;   const img = document.createElement('img');   img.alt = 'Book cover';   img.addEventListener('load', () => {     if (img === this.imageEl) {       this.loaded = true;       this.render();     }   });   img.src = url;   this.imageEl = img; }

Next we have our preview property, which can either be our base64 preview string, or our blurhash packet:

set preview(val) {   this.previewEl = this.createPreview(val);   this.render(); }  createPreview(val) {   if (typeof val === 'string') {     return base64Preview(val);   } else {     return blurHashPreview(val);   } }

This defers to whichever helper function we need:

function base64Preview(val) {   const img = document.createElement('img');   img.src = val;   return img; }  function blurHashPreview(preview) {   const canvasEl = document.createElement('canvas');   const { w: width, h: height } = preview;    canvasEl.width = width;   canvasEl.height = height;    const pixels = decode(preview.blurhash, width, height);   const ctx = canvasEl.getContext('2d');   const imageData = ctx.createImageData(width, height);   imageData.data.set(pixels);   ctx.putImageData(imageData, 0, 0);    return canvasEl; }

And, lastly, our render method:

connectedCallback() {   this.render(); }  render() {   const elementMaybe = this.loaded ? this.imageEl : this.previewEl;   syncSingleChild(this, elementMaybe); }

And a few helpers methods to tie everything together:

export function syncSingleChild(container, child) {   const currentChild = container.firstElementChild;   if (currentChild !== child) {     clearContainer(container);     if (child) {       container.appendChild(child);     }   } }  export function clearContainer(el) {   let child;    while ((child = el.firstElementChild)) {     el.removeChild(child);   } }

It’s a little bit more boilerplate than we’d need if we build this in a framework, but the upside is that we can re-use this in any framework we’d like — although React will need a wrapper for now, as we discussed.

Odds and ends

I’ve already mentioned Lit’s React wrapper. But if you find yourself using Stencil, it actually supports a separate output pipeline just for React. And the good folks at Microsoft have also created something similar to Lit’s wrapper, attached to the Fast web component library.

As I mentioned, all frameworks not named React will handle setting web component properties for you. Just note that some have some special flavors of syntax. For example, with Solid.js, <your-wc value={12}> always assumes that value is a property, which you can override with an attr prefix, like <your-wc attr:value={12}>.

Wrapping up

Web components are an interesting, often underused part of the web development landscape. They can help reduce your dependence on any single JavaScript framework by managing your UI, or “leaf” components. While creating these as web components — as opposed to Svelte or React components — won’t be as ergonomic, the upside is that they’ll be widely reusable.


Building Interoperable Web Components That Even Work With React originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

Dialog Components: Go Native HTML or Roll Your Own?

As the author of a library called AgnosticUI, I’m always on the lookout for new components. And recently, I decided to dig in and start work on a new dialog (aka modal) component. That’s something many devs like to have in their toolset and my goal was to make the best one possible, with an extra special focus on making it inclusive and accessible.

My first thought was that I would avoid any dependencies and bite the bullet to build my own dialog component. As you may know, there’s a new <dialog> element making the rounds and I figured using it as a starting point would be the right thing, especially in the inclusiveness and accessibilities departments.

But, after doing some research, I instead elected to leverage a11y-dialog by Kitty Giraudel. I even wrote adapters so it integrates smoothly with Vue 3, Svelte, and Angular. Kitty has long offered a React adapter as well.

Why did I go that route? Let me take you through my thought process.

First question: Should I even use the native <dialog> element?

The native <dialog> element is being actively improved and will likely be the way forward. But, it still has some issues at the moment that Kitty pointed out quite well:

  1. Clicking the backdrop overlay does not close the dialog by default
  2. The alertdialog ARIA role used for alerts simply does not work with the native <dialog> element. We’re supposed to use that role when a dialog requires a user’s response and shouldn’t be closed by clicking the backdrop, or by pressing ESC.
  3. The <dialog> element comes with a ::backdrop pseudo-element but it is only available when a dialog is programmatically opened with dialog.showModal().

And as Kitty also points out, there are general issues with the element’s default styles, like the fact they are left to the browser and will require JavaScript. So, it’s sort of not 100% HTML anyway.

Here’s a pen demonstrating these points:

Now, some of these issues may not affect you or whatever project you’re working on specifically, and you may even be able to work around things. If you still would like to utilize the native dialog you should see Adam Argyle’s wonderful post on building a dialog component with native dialog.

OK, let’s discuss what actually are the requirements for an accessible dialog component…

What I’m looking for

I know there are lots of ideas about what a dialog component should or should not do. But as far as what I was personally going after for AgnosticUI hinged on what I believe make for an accessible dialog experience:

  1. The dialog should close when clicking outside the dialog (on the backdrop) or when pressing the ESC key.
  2. It should trap focus to prevent tabbing out of the component with a keyboard.
  3. It should allow forwarding tabbing with TAB and backward tabbing with SHIFT+TAB.
  4. It should return focus back to the previously focused element when closed.
  5. It should correctly apply aria-* attributes and toggles.
  6. It should provide Portals (only if we’re using it within a JavaScript framework).
  7. It should support the alertdialog ARIA role for alert situations.
  8. It should prevent the underlying body from scrolling, if needed.
  9. It would be great if our implementation could avoid the common pitfalls that come with the native <dialog> element.
  10. It would ideally provide a way to apply custom styling while also taking the prefers-reduced-motion user preference query as a further accessibility measure.

I’m not the only one with a wish list. You might want to see Scott O’Hara’s article on the topic as well as Kitty’s full write-up on creating an accessible dialog from scratch for more in-depth coverage.

It should be clear right about now why I nixed the native <dialog> element from my component library. I believe in the work going into it, of course, but my current needs simply outweigh the costs of it. That’s why I went with Kitty’s a11y-dialog as my starting point.

Auditing <dialog> accessibility

Before trusting any particular dialog implementation, it’s worth making sure it fits the bill as far as your requirements go. With my requirements so heavily leaning on accessibility, that meant auditing a11y-dialog.

Accessibility audits are a profession of their own. And even if it’s not my everyday primary focus, I know there are some things that are worth doing, like:

This is quite a lot of work, as you might imagine (or know from experience). It’s tempting to take a path of less resistance and try automating things but, in a study conducted by Deque Systems, automated tooling can only catch about 57% of accessibility issues. There’s no substitute for good ol’ fashioned hard work.

The auditing environment

The dialog component can be tested in lots of places, including Storybook, CodePen, CodeSandbox, or whatever. For this particular test, though, I prefer instead to make a skeleton page and test locally. This way I’m preventing myself from having to validate the validators, so to speak. Having to use, say, a Storybook-specific add-on for a11y verification is fine if you’re already using Storybook on your own components, but it adds another layer of complexity when testing the accessibility of an external component.

A skeleton page can verify the dialog with manual checks, existing a11y tooling, and screen readers. If you’re following along, you’ll want to run this page via a local server. There are many ways to do that; one is to use a tool called serve, and npm even provides a nice one-liner npx serve <DIRECTORY> command to fire things up.

Let’s do an example audit together!

I’m obviously bullish on a11y-dialog here, so let’s put it to the test and verify it using some of the the recommended approaches we’ve covered.

Again, all I’m doing here is starting with an HTML. You can use the same one I am (complete with styles and scripts baked right in).

View full code
<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <meta http-equiv="X-UA-Compatible" content="ie=edge">     <title>A11y Dialog Test</title>     <style>       .dialog-container {         display: flex;         position: fixed;         top: 0;         left: 0;         bottom: 0;         right: 0;         z-index: 2;       }              .dialog-container[aria-hidden='true'] {         display: none;       }              .dialog-overlay {         position: fixed;         top: 0;         left: 0;         bottom: 0;         right: 0;         background-color: rgb(43 46 56 / 0.9);         animation: fade-in 200ms both;       }              .dialog-content {         background-color: rgb(255, 255, 255);         margin: auto;         z-index: 2;         position: relative;         animation: fade-in 400ms 200ms both, slide-up 400ms 200ms both;         padding: 1em;         max-width: 90%;         width: 600px;         border-radius: 2px;       }              @media screen and (min-width: 700px) {         .dialog-content {           padding: 2em;         }       }              @keyframes fade-in {         from {           opacity: 0;         }       }              @keyframes slide-up {         from {           transform: translateY(10%);         }       }        /* Note, for brevity we haven't implemented prefers-reduced-motion */              .dialog h1 {         margin: 0;         font-size: 1.25em;       }              .dialog-close {         position: absolute;         top: 0.5em;         right: 0.5em;         border: 0;         padding: 0;         background-color: transparent;         font-weight: bold;         font-size: 1.25em;         width: 1.2em;         height: 1.2em;         text-align: center;         cursor: pointer;         transition: 0.15s;       }              @media screen and (min-width: 700px) {         .dialog-close {           top: 1em;           right: 1em;         }       }              * {         box-sizing: border-box;       }              body {         font: 125% / 1.5 -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;         padding: 2em 0;       }              h1 {         font-size: 1.6em;         line-height: 1.1;         font-family: 'ESPI Slab', sans-serif;         margin-bottom: 0;       }              main {         max-width: 700px;         margin: 0 auto;         padding: 0 1em;       }     </style>     <script defer src="https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min.js"></script>   </head>    <body>     <main>       <div class="dialog-container" id="my-dialog" aria-hidden="true" aria-labelledby="my-dialog-title" role="dialog">         <div class="dialog-overlay" data-a11y-dialog-hide></div>         <div class="dialog-content" role="document">           <button data-a11y-dialog-hide class="dialog-close" aria-label="Close this dialog window">             ×           </button>           <a href="https://www.yahoo.com/" target="_blank">Rando Yahoo Link</a>              <h1 id="my-dialog-title">My Title</h1>           <p id="my-dialog-description">             Some description of what's inside this dialog…           </p>         </div>       </div>       <button type="button" data-a11y-dialog-show="my-dialog">         Open the dialog       </button>     </main>     <script>       // We need to ensure our deferred A11yDialog has       // had a chance to do its thing ;-)       window.addEventListener('DOMContentLoaded', (event) => {         const dialogEl = document.getElementById('my-dialog')         const dialog = new A11yDialog(dialogEl)       });     </script>   </body>  </html>

I know, we’re ignoring a bunch of best practices (what, styles in the <head>?!) and combined all of the HTML, CSS, and JavaScript in one file. I won’t go into the details of the code as the focus here is testing for accessibility, but know that this test requires an internet connection as we are importing a11y-dialog from a CDN.

First, the manual checks

I served this one-pager locally and here are my manual check results:

Feature Result
It should close when clicking outside the dialog (on the backdrop) or when pressing the ESC key.
It ought to trap focus to prevent tabbing out of the component with a keyboard.
It should allow forwarding tabbing with TAB and backward tabbing with SHIFT+TAB.
It should return focus back to the previously focused element when closed.
It should correctly apply aria-* attributes and toggles.
I verified this one “by eye” after inspecting the elements in the DevTools Elements panel.
It should provide Portals. Not applicable.
This is only useful when implementing the element with React, Svelte, Vue, etc. We’ve statically placed it on the page with aria-hidden for this test.
It should support for the alertdialog ARIA role for alert situations.
You’ll need to do two things:

First, remove data-a11y-dialog-hide from the overlay in the HTML so that it is <div class="dialog-overlay"></div>. Replace the dialog role with alertdialog so that it becomes:

<div class="dialog-container" id="my-dialog" aria-hidden="true" aria-labelledby="my-dialog-title" aria-describedby="my-dialog-description" role="alertdialog">

Now, clicking on the overlay outside of the dialog box does not close the dialog, as expected.

It should prevent the underlying body from scrolling, if needed.
I didn’t manually test but this, but it is clearly available per the documentation.
It should avoid the common pitfalls that come with the native <dialog> element.
This component does not rely on the native <dialog> which means we’re good here.

Next, let’s use some a11y tooling

I used Lighthouse to test the component both on a desktop computer and a mobile device, in two different scenarios where the dialog is open by default, and closed by default.

a11y-dialog Lighthouse testing, score 100.

I’ve found that sometimes the tooling doesn’t account for DOM elements that are dynamically shown or hidden DOM elements, so this test ensures I’m getting full coverage of both scenarios.

I also tested with IBM Equal Access Accessibility Checker. Generally, this tool will give you a red violation error if there’s anything egregious wrong. It will also ask you to manually review certain items. As seen here, there a couple of items for manual review, but no red violations.

a11y-dialog — tested with IBM Equal Access Accessibility Checker

Moving on to screen readers

Between my manual and tooling checks, I’m already feeling relatively confident that a11y-dialog is an accessible option for my dialog of choice. However, we ought to do our due diligence and consult a screen reader.

VoiceOver is the most convenient screen reader for me since I work on a Mac at the moment, but JAWS and NVDA are big names to look at as well. Like checking for UI consistency across browsers, it’s probably a good idea to test on more than one screen reader if you can.

VoiceOver caption over the a11y-modal example.

Here’s how I conducted the screen reader part of the audit with VoiceOver. Basically, I mapped out what actions needed testing and confirmed each one, like a script:

Step Result
The dialog component’s trigger button is announced. “Entering A11y Dialog Test, web content.”
The dialog should open when pressing CTRL+ALT +Space should show the dialog. “Dialog. Some description of what’s inside this dialog. You are currently on a dialog, inside of web content.”
The dialog should TAB to and put focus on the component’s Close button. “Close this dialog button. You are currently on a button, inside of web content.”
Tab to the link element and confirm it is announced. “Link, Rando Yahoo Link”
Pressing the SPACE key while focused on the Close button should close the dialog component and return to the last item in focus.

Testing with people

If you’re thinking we’re about to move on to testing with real people, I was unfortunately unable to find someone. If I had done this, though, I would have used a similar set of steps for them to run through while I observe, take notes, and ask a few questions about the general experience.

As you can see, a satisfactory audit involves a good deal of time and thought.

Fine, but I want to use a framework’s dialog component

That’s cool! Many frameworks have their own dialog component solution, so there’s lots to choose from. I don’t have some amazing spreadsheet audit of all the frameworks and libraries in the wild, and will spare you the work of evaluating them all.

Instead, here are some resources that might be good starting points and considerations for using a dialog component in some of the most widely used frameworks.

Disclaimer: I have not tested these personally. This is all stuff I found while researching.

Angular dialog options

In 2020, Deque published an article that audits Angular component libraries and the TL;DR was that Material (and its Angular/CDK library) and ngx-bootstrap both appear to provide decent dialog accessibility.

React dialog options

Reakit offers a dialog component that they claim is compliant with WAI-ARIA dialog guidelines, and chakra-ui appears to pay attention to its accessibility. Of course, Material is also available for React, so that’s worth a look as well. I’ve also heard good things about reach/dialog and Adobe’s @react-aria/dialog.

Vue dialog options

I’m a fan of Vuetensils, which is Austin Gil’s naked (aka headless) components library, which just so happens to have a dialog component. There’s also Vuetify, which is a popular Material implementation with a dialog of its own. I’ve also crossed paths with PrimeVue, but was surprised that its dialog component failed to return focus to the original element.

Svelte dialog options

You might want to look at svelte-headlessui. Material has a port in svelterial that is also worth a look. It seems that many current SvelteKit users prefer to build their own component sets as SvelteKit’s packaging idiom makes it super simple to do. If this is you, I would definitely recommend considering svelte-a11y-dialog as a convenient means to build custom dialogs, drawers, bottom sheets, etc.

I’ll also point out that my AgnosticUI library wraps the React, Vue, Svelte and Angular a11y-dialog adapter implementations we’ve been talking about earlier.

Bootstrap, of course

Bootstrap is still something many folks reach for, and unsurprisingly, it offers a dialog component. It requires you to follow some steps in order to make the modal accessible.

If you have other inclusive and accessible library-based dialog components that merit consideration, I’d love to know about them in the comments!

But I’m creating a custom design system

If you’re creating a design system or considering some other roll-your-own dialog approach, you can see just how many things need to be tested and taken into consideration… all for one component! It’s certainly doable to roll your own, of course, but I’d say it’s also extremely prone to error. You might ask yourself whether the effort is worthwhile when there are already battle-tested options to choose from.

I’ll simply leave you with something Scott O’Hara — co-editor of ARIA in HTML and HTML AAM specifications in addition to just being super helpful with all things accessibility — points out:

You could put in the effort to add in those extensions, or you could use a robust plugin like a11y-dialog and ensure that your dialogs will have a pretty consistent experience across all browsers.

Back to my objective…

I need that dialog to support React, Vue, Svelte, and Angular implementations.

I mentioned earlier that a11y-dialog already has ports for Vue and React. But the Vue port hasn’t yet been updated for Vue 3. Well, I was quite happy to spend the time I would have spent creating what likely would have been a buggy hand-rolled dialog component toward helping update the Vue port. I also added a Svelte port and one for Angular too. These are both very new and I would consider them experimental beta software at time of writing. Feedback welcome, of course!

It can support other components, too!

I think it’s worth pointing out that a dialog uses the same underlying concept for hiding and showing that can be used for a drawer (aka off-canvas) component. For example, if we borrow the CSS we used in our dialog accessibility audit and add a few additional classes, then a11y-dialog can be transformed into a working and effective drawer component:

.drawer-start { right: initial; } .drawer-end { left: initial; } .drawer-top { bottom: initial; } .drawer-bottom { top: initial; }  .drawer-content {   margin: initial;   max-width: initial;   width: 25rem;   border-radius: initial; }  .drawer-top .drawer-content, .drawer-bottom .drawer-content {   width: 100%; }

These classes are used in an additive manner, essentially extending the base dialog component. This is exactly what I have started to do as I add my own drawer component to AgnosticUI. Saving time and reusing code FTW!

Wrapping up

Hopefully I’ve given you a good idea of the thinking process that goes into the making and maintenance of a component library. Could I have hand-rolled my own dialog component for the library? Absolutely! But I doubt it would have yielded better results than what a resource like Kitty’s a11y-dialog does, and the effort is daunting. There’s something cool about coming up with your own solution — and there may be good situations where you want to do that — but probably not at the cost of sacrificing something like accessibility.

Anyway, that’s how I arrived at my decision. I learned a lot about the native HTML <dialog> and its accessibility along the way, and I hope my journey gave you some of those nuggets too.


Dialog Components: Go Native HTML or Roll Your Own? originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , ,
[Top]

Avoiding the Pitfalls of Nested Components in a Design System

When creating a component-based, front-end infrastructure, one of the biggest pain points I’ve personally encountered is making components that are both reusable and responsive when there are nested components within components.

Take the following “call to action” (<CTA />) component, for example:

On smaller devices we want it to look like this:

This is simple enough with basic media queries. If we’re using flexbox, a media query can change the flex direction and makes the button go the full width. But we run into a problem when we start nesting other components in there. For example, say we’re using a component for the button and it already has a prop that makes it full-width. We are actually duplicating the button’s styling when applying a media query to the parent component. The nested button is already capable of handling it!

This is a small example and it wouldn’t be that bad of a problem, but for other scenarios it could cause a lot of duplicated code to replicate the styling. What if in the future we wanted to change something about how full-width buttons are styled? We’d need to go through and change it in all these different places. We should be able to change it in the button component and have that update everywhere.

Wouldn’t it be nice if we could move away from media queries and have more control of the styling? We should be using a component’s existing props and be able to pass different values based on the screen width.

Well, I have a way to do that and will show you how I did it.

I am aware that container queries can solve a lot of these issues, but it’s still in early days and doesn’t solve the issue with passing a variety of props based on screen width.

Tracking the window width

First, we need to track the current width of the page and set a breakpoint. This can be done with any front-end framework, but I’m going using a Vue composable here as to demonstrate the idea:

// composables/useBreakpoints.js  import { readonly, ref } from "vue";  const bps = ref({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }) const currentBreakpoint = ref(bps.xl);  export default () => {   const updateBreakpoint = () => {        const windowWidth = window.innerWidth;          if(windowWidth >= 1200) {       currentBreakpoint.value = bps.xl     } else if(windowWidth >= 992) {       currentBreakpoint.value = bps.lg     } else if(windowWidth >= 768) {       currentBreakpoint.value = bps.md     } else if(windowWidth >= 576) {       currentBreakpoint.value = bps.sm     } else {       currentBreakpoint.value = bps.xs     }   }    return {     currentBreakpoint: readonly(currentBreakpoint),     bps: readonly(bps),     updateBreakpoint,   }; };

The reason we are using numbers for the currentBreakpoint object will become clear later.

Now we can listen for window resize events and update the current breakpoint using the composable in the main App.vue file:

// App.vue  <script> import useBreakpoints from "@/composables/useBreakpoints"; import { onMounted, onUnmounted } from 'vue'  export default {   name: 'App',      setup() {     const { updateBreakpoint } = useBreakpoints()      onMounted(() => {       updateBreakpoint();       window.addEventListener('resize', updateBreakpoint)     })      onUnmounted(() => {       window.removeEventListener('resize', updateBreakpoint)     })   } } </script>

We probably want this to be debounced, but I’m keeping things simple for brevity.

Styling components

We can update the <CTA /> component to accept a new prop for how it should be styled:

// CTA.vue props: {   displayMode: {     type: String,     default: "default"   } }

The naming here is totally arbitrary. You can use whatever names you’d like for each of the component modes.

We can then use this prop to change the mode based on the current breakpoint:

<CTA :display-mode="currentBreakpoint > bps.md ? 'default' : 'compact'" />

You can see now why we’re using a number to represent the current breakpoint — it’s so the correct mode can be applied to all breakpoints below or above a certain number.

We can then use this in the CTA component to style according to the mode passed through:

// components/CTA.vue  <template>   <div class="cta" :class="displayMode">          <div class="cta-content">       <h5>title</h5>       <p>description</p>     </div>          <Btn :block="displayMode === 'compact'">Continue</Btn>        </div> </template>  <script> import Btn from "@/components/ui/Btn"; export default {   name: "CTA",   components: { Btn },   props: {     displayMode: {       type: String,       default: "default"     },   } } </script>  <style scoped lang="scss"> .cta {   display: flex;   align-items: center;      .cta-content {     margin-right: 2rem;   }    &.compact {     flex-direction: column;     .cta-content {       margin-right: 0;       margin-bottom: 2rem;     }   } } </style>

Already, we have removed the need for media queries! You can see this in action on a demo page I created.

Admittedly, this may seem like a lengthy process for something so simple. But when applied to multiple components, this approach can massively improve the consistency and stability of the UI while reducing the total amount of code we need to write. This way of using JavaScript and CSS classes to control the responsive styling also has another benefit…

Extensible functionality for nested components

There have been scenarios where I’ve needed to revert back to a previous breakpoint for a component. For example, if it takes up 50% of the screen, I want it displayed in the small mode. But at a certain screen size, it becomes full-width. In other words, the mode should change one way or the other when there’s a resize event.

Showing three versions of a call-to-action components with nested components within it.

I’ve also been in situations where the same component is used in different modes on different pages. This isn’t something that frameworks like Bootstrap and Tailwind can do, and using media queries to pull it off would be a nightmare. (You can still use those frameworks using this technique, just without the need for the responsive classes they provide.)

We could use a media query that only applies to middle sized screens, but this doesn’t solve the issue with varying props based on screen width. Thankfully, the approach we’re covering can solve that. We can modify the previous code to allow for a custom mode per breakpoint by passing it through an array, with the first item in the array being the smallest screen size.

<CTA :custom-mode="['compact', 'default', 'compact']" />

First, let’s update the props that the <CTA /> component can accept:

props: {   displayMode: {     type: String,     default: "default"   },   customMode: {     type: [Boolean, Array],     default: false   }, }

We can then add the following to generate to correct mode:

import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  // ...  setup(props) {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } },

This is taking the mode from the array based on the current breakpoint, and defaults to the displayMode if one isn’t found. Then we can use mode instead to style the component.

Extraction for reusability

Many of these methods can be extracted into additional composables and mixins that can be reuseD with other components.

Extracting computed mode

The logic for returning the correct mode can be extracted into a composable:

// composables/useResponsive.js  import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  export const useResponsive = (props) => {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } }

Extracting props

In Vue 2, we could repeat props was by using mixins, but there are noticeable drawbacks. Vue 3 allows us to merge these with other props using the same composable. There’s a small caveat with this, as IDEs seem unable to recognize props for autocompletion using this method. If this is too annoying, you can use a mixin instead.

Optionally, we can also pass custom validation to make sure we’re using the modes only available to each component, where the first value passed through to the validator is the default.

// composables/useResponsive.js  // ...  export const withResponsiveProps = (validation, props) => {   return {     displayMode: {       type: String,       default: validation[0],       validator: function (value) {         return validation.indexOf(value) !== -1       }     },     customMode: {       type: [Boolean, Array],       default: false,       validator: function (value) {         return value ? value.every(mode => validation.includes(mode)) : true       }     },     ...props   } }

Now let’s move the logic out and import these instead:

// components/CTA.vue  import Btn from "@/components/ui/Btn"; import { useResponsive, withResponsiveProps } from "@/composables/useResponsive";  export default {   name: "CTA",   components: { Btn },   props: withResponsiveProps(['default 'compact'], {     extraPropExample: {       type: String,     },   }),      setup(props) {     const { mode } = useResponsive(props)     return { mode }   } }

Conclusion

Creating a design system of reusable and responsive components is challenging and prone to inconsistencies. Plus, we saw how easy it is to wind up with a load of duplicated code. There’s a fine balance when it comes to creating components that not only work in many contexts, but play well with other components when they’re combined.

I’m sure you’ve come across this sort of situation in your own work. Using these methods can reduce the problem and hopefully make the UI more stable, reusable, maintainable, and easy to use.


Avoiding the Pitfalls of Nested Components in a Design System originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

Context-Aware Web Components Are Easier Than You Think

Another aspect of web components that we haven’t talked about yet is that a JavaScript function is called whenever a web component is added or removed from a page. These lifecycle callbacks can be used for many things, including making an element aware of its context.

Article series

The four lifecycle callbacks of web components

There are four lifecycle callbacks that can be used with web components:

  • connectedCallback: This callback fires when the custom element is attached to the element.
  • disconnectedCallback: This callback fires when the element is removed from the document.
  • adoptedCallback: This callback fires when the element is added to a new document.
  • attributeChangedCallback: This callback fires when an attribute is changed, added or removed, as long as that attribute is being observed.

Let’s look at each of these in action.

Our post-apocalyptic person component

Two renderings of the web component side-by-side, the left is a human, and the right is a zombie.

We’ll start by creating a web component called <postapocalyptic-person>. Every person after the apocalypse is either a human or a zombie and we’ll know which one based on a class — either .human or .zombie — that’s applied to the parent element of the <postapocalyptic-person> component. We won’t do anything fancy with it (yet), but we’ll add a shadowRoot we can use to attach a corresponding image based on that classification.

customElements.define(   "postapocalyptic-person",   class extends HTMLElement {     constructor() {       super();       const shadowRoot = this.attachShadow({ mode: "open" });     } }

Our HTML looks like this:

<div class="humans">   <postapocalyptic-person></postapocalyptic-person> </div> <div class="zombies">   <postapocalyptic-person></postapocalyptic-person> </div>

Inserting people with connectedCallback

When a <postapocalyptic-person> is loaded on the page, the connectedCallback() function is called.

connectedCallback() {   let image = document.createElement("img");   if (this.parentNode.classList.contains("humans")) {     image.src = "https://assets.codepen.io/1804713/lady.png";     this.shadowRoot.appendChild(image);   } else if (this.parentNode.classList.contains("zombies")) {     image.src = "https://assets.codepen.io/1804713/ladyz.png";     this.shadowRoot.appendChild(image);   } }

This makes sure that an image of a human is output when the <postapocalyptic-person> is a human, and a zombie image when the component is a zombie.

Be careful working with connectedCallback. It runs more often than you might realize, firing any time the element is moved and could (baffling-ly) even run after the node is no longer connected — which can be an expensive performance cost. You can use this.isConnected to know whether the element is connected or not.

Counting people with connectedCallback() when they are added

Let’s get a little more complex by adding a couple of buttons to the mix. One will add a <postapocalyptic-person>, using a “coin flip” approach to decide whether it’s a human or a zombie. The other button will do the opposite, removing a <postapocalyptic-person> at random. We’ll keep track of how many humans and zombies are in view while we’re at it.

<div class="btns">   <button id="addbtn">Add Person</button>   <button id="rmvbtn">Remove Person</button>    <span class="counts">     Humans: <span id="human-count">0</span>      Zombies: <span id="zombie-count">0</span>   </span> </div>

Here’s what our buttons will do:

let zombienest = document.querySelector(".zombies"),   humancamp = document.querySelector(".humans");  document.getElementById("addbtn").addEventListener("click", function () {   // Flips a "coin" and adds either a zombie or a human   if (Math.random() > 0.5) {     zombienest.appendChild(document.createElement("postapocalyptic-person"));   } else {     humancamp.appendChild(document.createElement("postapocalyptic-person"));   } }); document.getElementById("rmvbtn").addEventListener("click", function () {   // Flips a "coin" and removes either a zombie or a human   // A console message is logged if no more are available to remove.   if (Math.random() > 0.5) {     if (zombienest.lastElementChild) {       zombienest.lastElementChild.remove();     } else {       console.log("No more zombies to remove");     }   } else {     if (humancamp.lastElementChild) {       humancamp.lastElementChild.remove();     } else {       console.log("No more humans to remove");     }   } });

Here’s the code in connectedCallback() that counts the humans and zombies as they are added:

connectedCallback() {   let image = document.createElement("img");   if (this.parentNode.classList.contains("humans")) {     image.src = "https://assets.codepen.io/1804713/lady.png";     this.shadowRoot.appendChild(image);     // Get the existing human count.     let humancount = document.getElementById("human-count");     // Increment it     humancount.innerHTML = parseInt(humancount.textContent) + 1;   } else if (this.parentNode.classList.contains("zombies")) {     image.src = "https://assets.codepen.io/1804713/ladyz.png";     this.shadowRoot.appendChild(image);     // Get the existing zombie count.     let zombiecount = document.getElementById("zombie-count");     // Increment it     zombiecount.innerHTML = parseInt(zombiecount.textContent) + 1;   } }

Updating counts with disconnectedCallback

Next, we can use disconnectedCallback() to decrement the number as a humans and zombies are removed. However, we are unable to check the class of the parent element because the parent element with the corresponding class is already gone by the time disconnectedCallback is called. We could set an attribute on the element, or add a property to the object, but since the image’s src attribute is already determined by its parent element, we can use that as a proxy for knowing whether the web component being removed is a human or zombie.

disconnectedCallback() {   let image = this.shadowRoot.querySelector('img');   // Test for the human image   if (image.src == "https://assets.codepen.io/1804713/lady.png") {     let humancount = document.getElementById("human-count");     humancount.innerHTML = parseInt(humancount.textContent) - 1; // Decrement count   // Test for the zombie image   } else if (image.src == "https://assets.codepen.io/1804713/ladyz.png") {     let zombiecount = document.getElementById("zombie-count");     zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1; // Decrement count   } }

Beware of clowns!

Now (and I’m speaking from experience here, of course) the only thing scarier than a horde of zombies bearing down on your position is a clown — all it takes is one! So, even though we’re already dealing with frightening post-apocalyptic zombies, let’s add the possibility of a clown entering the scene for even more horror. In fact, we’ll do it in such a way that there’s a possibility any human or zombie on the screen is secretly a clown in disguise!

I take back what I said earlier: a single zombie clown is scarier than even a group of “normal” clowns. Let’s say that if any sort of clown is found — be it human or zombie — we separate them from the human and zombie populations by sending them to a whole different document — an <iframe> jail, if you will. (I hear that “clowning” may be even more contagious than zombie contagion.)

And when we move a suspected clown from the current document to an <iframe>, it doesn’t destroy and recreate the original node; rather it adopts and connects said node, first calling adoptedCallback then connectedCallback.

We don’t need anything in the <iframe> document except a body with a .clowns class. As long as this document is in the iframe of the main document — not viewed separately — we don’t even need the <postapocalyptic-person> instantiation code. We’ll include one space for humans, another space for zombies, and yes, the clowns’s jail… errr… <iframe> of… fun.

<div class="btns">   <button id="addbtn">Add Person</button>   <button id="jailbtn">Jail Potential Clown</button> </div> <div class="humans">   <postapocalyptic-person></postapocalyptic-person> </div> <div class="zombies">   <postapocalyptic-person></postapocalyptic-person> </div> <iframe class="clowniframeoffun” src="adoptedCallback-iframe.html"> </iframe>

Our “Add Person” button works the same as it did in the last example: it flips a digital coin to randomly insert either a human or a zombie. When we hit the “Jail Potential Clown” button another coin is flipped and takes either a zombie or a human, handing them over to <iframe> jail.

document.getElementById("jailbtn").addEventListener("click", function () {   if (Math.random() > 0.5) {     let human = humancamp.querySelector('postapocalyptic-person');     if (human) {       clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(human));     } else {       console.log("No more potential clowns at the human camp");     }   } else {     let zombie = zombienest.querySelector('postapocalyptic-person');     if (zombie) {       clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(zombie));     } else {       console.log("No more potential clowns at the zombie nest");     }   } });

Revealing clowns with adoptedCallback

In the adoptedCallback we’ll determine whether the clown is of the zombie human variety based on their corresponding image and then change the image accordingly. connectedCallback will be called after that, but we don’t have anything it needs to do, and what it does won’t interfere with our changes. So we can leave it as is.

adoptedCallback() {   let image = this.shadowRoot.querySelector("img");   if (this.parentNode.dataset.type == "clowns") {     if (image.src.indexOf("lady.png") != -1) {        // Sometimes, the full URL path including the domain is saved in `image.src`.       // Using `indexOf` allows us to skip the unnecessary bits.        image.src = "ladyc.png";       this.shadowRoot.appendChild(image);     } else if (image.src.indexOf("ladyz.png") != -1) {       image.src = "ladyzc.png";       this.shadowRoot.appendChild(image);     }   } }

Detecting hidden clowns with attributeChangedCallback

Finally, we have the attributeChangedCallback. Unlike the the other three lifecycle callbacks, we need to observe the attributes of our web component in order for the the callback to fire. We can do this by adding an observedAttributes() function to the custom element’s class and have that function return an array of attribute names.

static get observedAttributes() {   return [“attribute-name”]; }

Then, if that attribute changes — including being added or removed — the attributeChangedCallback fires.

Now, the thing you have to worry about with clowns is that some of the humans you know and love (or the ones that you knew and loved before they turned into zombies) could secretly be clowns in disguise. I’ve set up a clown detector that looks at a group of humans and zombies and, when you click the “Reveal Clowns” button, the detector will (through completely scientific and totally trustworthy means that are not based on random numbers choosing an index) apply data-clown="true" to the component. And when this attribute is applied, attributeChangedCallback fires and updates the component’s image to uncover their clownish colors.

I should also note that the attributeChangedCallback takes three parameters:

  • the name of the attribute
  • the previous value of the attribute
  • the new value of the attribute

Further, the callback lets you make changes based on how much the attribute has changed, or based on the transition between two states.

Here’s our attributeChangedCallback code:

attributeChangedCallback(name, oldValue, newValue) {   let image = this.shadowRoot.querySelector("img");   // Ensures that `data-clown` was the attribute that changed,   // that its value is true, and that it had an image in its `shadowRoot`   if (name="data-clown" && this.dataset.clown && image) {     // Setting and updating the counts of humans, zombies,     // and clowns on the page     let clowncount = document.getElementById("clown-count"),     humancount = document.getElementById("human-count"),     zombiecount = document.getElementById("zombie-count");     if (image.src.indexOf("lady.png") != -1) {       image.src = "https://assets.codepen.io/1804713/ladyc.png";       this.shadowRoot.appendChild(image);       // Update counts       clowncount.innerHTML = parseInt(clowncount.textContent) + 1;       humancount.innerHTML = parseInt(humancount.textContent) - 1;     } else if (image.src.indexOf("ladyz.png") != -1) {       image.src = "https://assets.codepen.io/1804713/ladyzc.png";       this.shadowRoot.appendChild(image);       // Update counts       clowncount.innerHTML = parseInt(clowncount.textContent) + 1;       zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1;     }   } }

And there you have it! Not only have we found out that web component callbacks and creating context-aware custom elements are easier than you may have thought, but detecting post-apocalyptic clowns, though terrifying, is also easier that you thought. What kind of devious, post-apocalyptic clowns can you detect with these web component callback functions?


Context-Aware Web Components Are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,
[Top]

Testing Vue Components With Cypress

Cypress is an automated test runner for browser-based applications and pages. I’ve used it for years to write end-to-end tests for web projects, and was happy to see recently that individual component testing had come to Cypress. I work on a large enterprise Vue application, and we already use Cypress for end-to-end tests. Most of our unit and component tests are written with Jest and Vue Test Utils.

Once component testing arrived in Cypress, my team was all in favor of upgrading and trying it out. You can learn a lot about how component testing works directly from the Cypress docs, so I’m going skip over some of the setup steps and focus on what it is like to work with component tests — what do they look like, how are we using them, and some Vue-specific gotchas and helpers we found.

Disclosure! At the time I wrote the first draft of this article, I was the front-end team lead at a large fleet management company where we used Cypress for testing. Since the time of writing, I’ve started working at Cypress, where I get to contribute to the open source test runner.

The Cypress component test runner is open, using Chrome to test a “Privacy Policy” component. Three columns are visible in the browser. The first contains a searchable list of component test spec files; the second shows the tests for the currently-spec; the last shows the component itself mounted in the browser. The middle column shows that two tests are passing.

All the examples mentioned here are valid at the time of writing using Cypress 8. It’s a new feature that’s still in alpha, and I wouldn’t be surprised if some of these details change in future updates.

If you already have a background in testing and component tests, you can skip right to our team’s experiences.

What a component test file looks like

For a simplified example, I’ve created a project that contains a “Privacy Policy” component. It has a title, body, and an acknowledgment button.

The Privacy Policy component has three areas. The title reads “Privacy Policy”; the body text reads “Information about privacy that you should read in detail,” and a blue button at the bottom reads “OK, I read it, sheesh.”

When the button is clicked, an event is emitted to let the parent component know that this has been acknowledged. Here it is deployed on Netlify.

Now here’s the general shape of a component test in Cypress that uses some of the feature’s we are going to talk about:

import { mount } from '@cypress/vue'; // import the vue-test-utils mount function import PrivacyPolicyNotice from './PrivacyPolicyNotice.vue'; // import the component to test  describe('PrivacyPolicyNotice', () => {    it('renders the title', () => {     // mount the component by itself in the browser 🏗     mount(PrivacyPolicyNotice);           // assert some text is present in the correct heading level 🕵️      cy.contains('h1', 'Privacy Policy').should('be.visible');    });    it('emits a "confirm" event once when confirm button is clicked', () => {     // mount the component by itself in the browser 🏗     mount(PrivacyPolicyNotice);      // this time let's chain some commands together     cy.contains('button', '/^OK/') // find a button element starting with text 'OK' 🕵️     .click() // click the button 🤞     .vue() // use a custom command to go get the vue-test-utils wrapper 🧐     .then((wrapper) => {       // verify the component emitted a confirm event after the click 🤯       expect(wrapper.emitted('confirm')).to.have.length(1)        // `emitted` is a helper from vue-test-utils to simplify accessing       // events that have been emitted     });   });  });

This test makes some assertions about the user interface, and some about the developer interface (shoutout to Alex Reviere for expressing this division in the way that clicked for me). For the UI, we are targeting specific elements with their expected text content. For developers, we are testing what events are emitted. We are also implicitly testing that the component is a correctly formed Vue component; otherwise it would not mount successfully and all the other steps would fail. And by asserting specific kinds of elements for specific purposes, we are testing the accessibility of the component — if that accessible button ever becomes a non-focusable div, we’ll know about it.

Here’s how our test looks when I swap out the button for a div. This helps us maintain the expected keyboard behavior and assistive technology hints that come for free with a button element by letting us know if we accidentally swap it out:

The Cypress component test runner shows that one test is passing and one is failing. The failure warning is titled 'Assertion Error' and reads 'Timed out retrying after 4000ms: Expected to find content: '/^OK/' within the selector: 'button' but never did.'

A little groundwork

Now that we’ve seen what a component test looks like, let’s back up a little bit and talk about how this fits in to our overall testing strategy. There are many definitions for these things, so real quick, for me, in our codebase:

  • Unit tests confirm single functions behave as expected when used by a developer.
  • Component tests mount single UI components in isolation and confirm they behave as expected when used by an end-user and a developer.
  • End-to-end tests visit the application and perform actions and confirm the app as whole behaves correctly when used by an end-user only.

Finally, integration testing is a little more of a squishy term for me and can happen at any level — a unit that imports other functions, a component that imports other components, or indeed, an “end-to-end” test that mocks API responses and doesn’t reach the database, might all be considered integration tests. They test more than one part of an application working together, but not the entire thing. I’m not sure about the real usefulness of that as a category, since it seems very broad, but different people and organizations use these terms in other ways, so I wanted to touch on it.

For a longer overview of the different kinds of testing and how they relate to front-end work, you can check out “Front-End Testing is For Everyone” by Evgeny Klimenchenko.

Component tests

In the definitions above, the different testing layers are defined by who will be using a piece of code and what the contract is with that person. So as a developer, a function that formats the time should always return the correct result when I provide it a valid Date object, and should throw clear errors if I provide it something different as well. These are things we can test by calling the function on its own and verifying it responds correctly to various conditions, independent of any UI. The “developer interface” (or API) of a function is all about code talking to other code.

Now, let’s zoom in on component tests. The “contract” of a component is really two contracts:

  • To the developer using a component, the component is behaving correctly if the expected events are emitted based on user input or other activity. It’s also fair to include things like prop types and validation rules in our idea of “correct developer-facing behavior,” though those things can also be tested at a unit level. What I really want from a component test as a developer is to know it mounts, and sends the signals it is supposed to based on interactions.
  • To the user interacting with a component, it is behaving correctly if the UI reflects the state of the component at all times. This includes more than just the visual aspect. The HTML generated by the component is the foundation for its accessibility tree, and the accessibility tree provides the API for tools like screen readers to announce the content correctly, so for me the component is not “behaving correctly” if it does not render the correct HTML for the contents.

At this point it’s clear that component testing requires two kinds of assertions — sometimes we check Vue-specific things, like “how many events got emitted of a certain type?”, and sometimes we check user-facing things, like “did a visible success message actually end up on the screen though?”

It also feels like component level tests are a powerful documentation tool. The tests should assert all the critical features of a component — the defined behaviors that are depended on — and ignore details that aren’t critical. This means we can look to the tests to understand (or remember, six months or a year from now!) what a component’s expected behavior is. And, all going well, we can change any feature that’s not explicitly asserted by the test without having to rewrite the test. Design changes, animation changes, improving the DOM, all should be possible, and if a test does fail, it will be for a reason you care about, not because an element got moved from one part of the screen to another.

This last part takes some care when designing tests, and most especially, when choosing selectors for elements to interact with, so we’ll return to this topic later.

How Vue component tests work with and without Cypress

At a high level, a combination of Jest and the Vue Test Utils library has becomes more or less the standard approach to running component tests that I’ve seen out there.

Vue Test Utils gives us helpers to mount a component, give it its options, and mock out various things a component might depend on to run properly. It also provides a wrapper object around the mounted component to make it a little easier to make assertions about what’s going on with the component.

Jest is a great test runner and will stand up the mounted component using jsdom to simulate a browser environment.

Cypress’ component test runner itself uses Vue Test Utils to mount Vue components, so the main difference between the two approaches is context. Cypress already runs end-to-end tests in a browser, and component tests work the same way. This means we can see our tests run, pause them mid-test, interact with the app or inspect things that happened earlier in the run, and know that browser APIs that our application depends on are genuine browser behavior rather than the jsdom mocked versions of those same features.

Once the component is mounted, all the usual Cypress things that we have been doing in end-to-end tests apply, and a few pain points around selecting elements go away. Mainly, Cypress is going to handle simulating all the user interactions, and making assertions about the application’s response to those interactions. This covers the user-facing part of the component’s contract completely, but what about the developer-facing stuff, like events, props, and everything else? This is where Vue Test Utils comes back in. Within Cypress, we can access the wrapper that Vue Test Utils creates around the mounted component, and make assertions about it.

What I like about this is that we end up with Cypress and Vue Test Utils both being used for what they are really good at. We can test the component’s behavior as a user with no framework-specific code at all, and only dig into Vue Test Utils for mounting the component and checking specific framework behavior when we choose to. We’ll never have to await a Vue-specific $ nextTick after doing some Vue-specific thing to update the state of a component. That was always the trickiest thing to explain to new developers on the team without Vue experience — when and why they would need to await things when writing a test for a Vue component.

Our experience of component testing

The advantages of component testing sounded great to us, but of course, in a large project very few things can be seamless out of the box, and as we got started with our tests, we ran into some issues. We run a large enterprise SPA built using Vue 2 and the Vuetify component library. Most of our work heavily uses Vuetify’s built-in components and styles. So, while the “test components by themselves” approach sounds nice, a big lesson learned was that we needed to set up some context for our components to be mounted in, and we needed to get Vuetify and some global styles happening as well, or nothing was going to work.

Cypress has a Discord where people can ask for help, and when I got stuck I asked questions there. Folks from the community —as well as Cypress team members — kindly directed me to example repos, code snippets, and ideas for solving our problems. Here’s a list of the little things we needed to understand in order to get our components to mount correctly, errors we encountered, and whatever else stands out as interesting or helpful:

Importing Vuetify

Through lurking in the Cypress Discord, I’d seen this example component test Vuetify repo by Bart Ledoux, so that was my starting point. That repo organizes the code into a fairly common pattern that includes a plugins folder, where a plugin exports an instance of Veutify. This is imported by the application itself, but it can also be imported by our test setup, and used when mounting the component being tested. In the repo a command is added to Cypress that will replace the default mount function with one that mounts a component with Vuetify.

Here is all the code needed to make that happen, assuming we did everything in commands.js and didn’t import anything from the plugins folder. We’re doing this with a custom command which means that instead of calling the Vue Test Utils mount function directly in our tests, we’ll actually call our own cy.mount command:

// the Cypress mount function, which wraps the vue-test-utils mount function import { mount } from "@cypress/vue";  import Vue from 'vue'; import Vuetify from 'vuetify/lib/framework';  Vue.use(Vuetify);  // add a new command with the name "mount" to run the Vue Test Utils  // mount and add Vuetify Cypress.Commands.add("mount", (MountedComponent, options) => {   return mount(MountedComponent, {     vuetify: new Vuetify({});, // the new Vuetify instance     ...options, // To override/add Vue options for specific tests   }); });

Now we will always have Vuetify along with our components when mounted, and we can still pass in all the other options we need to for that component itself. But we don’t need to manually add Veutify each time.

Adding attributes required by Vuetify

The only problem with the new mount command above is that, to work correctly, Vuetify components expect to be rendered in a certain DOM context. Apps using Vuetify wrap everything in a <v-app> component that represents the root element of the application. There are a couple of ways to handle this but the simplest is to add some setup to our command itself before it mounts a component.

Cypress.Commands.add("mount", (MountedComponent, options) => {   // get the element that our mounted component will be injected into   const root = document.getElementById("__cy_root");    // add the v-application class that allows Vuetify styles to work   if (!root.classList.contains("v-application")) {     root.classList.add("v-application");   }    // add the data-attribute — Vuetify selector used for popup elements to attach to the DOM   root.setAttribute('data-app', 'true');    return mount(MountedComponent, {     vuetify: new Vuetify({}),      ...options,   }); });

This takes advantage of the fact that Cypress itself has to create some root element to actually mount our component to. That root element is the parent of our component, and it has the ID __cy_root. This gives us a place to easily add the correct classes and attributes that Vuetify expects to find. Now components that use Vuetify components will look and behave correctly.

One other thing we noticed after some testing is that the required class of v-application has a display property of flex. This makes sense in a full app context using Vuetify’s container system, but had some unwanted visual side effects for us when mounting single components — so we added one more line to override that style before mounting the component:

root.setAttribute('style', 'display: block');

This cleared up the occasional layout issues and then we were truly done tweaking the surrounding context for mounting components.

Getting spec files where we want them

A lot of the examples out there show a cypress.json config file like this one for component testing:

{   "fixturesFolder": false,   "componentFolder": "src/components",   "testFiles": "**/*.spec.js" }

That is actually pretty close to what we want since the testFiles property accepts a glob pattern. This one says, Look in any folder for files ending in .spec.js. In our case, and probably many others, the project’s node_modules folder contained some irrelevant spec.js files that we excluded by prefixing !(node_modules) like this:

"testFiles": "!(node_modules)**/*.spec.js"

Before settling on this solution, when experimenting, we had set this to a specific folder where component tests would live, not a glob pattern that could match them anywhere. Our tests live right alongside our components, so that could have been fine, but we actually have two independent components folders as we package up and publish a small part of our app to be used in other projects at the company. Having made that change early, I admit I sure did forget it had been a glob to start with and was starting to get off course before popping into the Discord, where I got a reminder and figured it out. Having a place to quickly check if something is the right approach was helpful many times.

Command file conflict

Following the pattern outlined above to get Vuetify working with our component tests produced a problem. We had piled all this stuff together in the same commands.js file that we used for regular end-to-end tests. So while we got a couple of component tests running, our end-to-end tests didn’t even start. There was an early error from one of the imports that was only needed for component testing.

I was recommended a couple of solutions but on the day, I chose to just extract the mounting command and its dependencies into its own file, and imported it only where needed in the component tests themselves. Since this was the only source of any problem running both sets of tests, it was a clean way to take that out of the the end-to-end context, and it works just fine as a standalone function. If we have other issues, or next time we are doing cleanup, we would probably follow the main recommendation given, to have two separate command files and share the common pieces between them.

Accessing the Vue Test Utils wrapper

In the context of a component test, the Vue Test Utils wrapper is available under Cypress.vueWrapper. When accessing this to make assertions, it helps to use cy.wrap to make the result chain-able like other commands accessed via cy. Jessica Sachs adds a short command in her example repo to do this. So, once again inside commands,js, I added the following:

Cypress.Commands.add('vue', () => {   return cy.wrap(Cypress.vueWrapper); }); 

This can be used in a test, like this:

mount(SomeComponent)   .contains('button', 'Do the thing once')   .click()   .should('be.disabled')   .vue()   .then((wrapper) => {     // the Vue Test Utils `wrapper` has an API specifically setup for testing:      // https://vue-test-utils.vuejs.org/api/wrapper/#properties     expect(wrapper.emitted('the-thing')).to.have.length(1);   }); 

This starts to read very naturally to me and clearly splits up when we are working with the UI compared to when we are inspecting details revealed through the Vue Test Utils wrapper. It also emphasizes that, like lots of Cypress, to get the most out of it, it’s important to understand the tools it leverages, not just Cypress itself. Cypress wraps Mocha, Chai, and various other libraries. In this case, it’s useful to understand that Vue Test Utils is a third-party open source solution with its own entire set of documentation, and that inside the then callback above, we are in Vue Test Utils Land — not Cypress Land — so that we go to the right place for help and documentation.

Challenges

Since this has been a recent exploration, we have not added the Cypress component tests to our CI/CD pipelines yet. Failures will not block a pull request, and we haven’t looked at adding the reporting for these tests. I don’t expect any surprises there, but it’s worth mentioning that we haven’t completed integrating these into our whole workflow. I can’t speak to it specifically.

It’s also relatively early days for the component test runner and there are a few hiccups. At first, it seemed like every second test run would show a linter error and need to be manually refreshed. I didn’t get to the bottom of that, and then it fixed itself (or was fixed by a newer Cypress release). I’d expect a new tool to have potential issues like this.

One other stumbling block about component testing in general is that, depending on how your component works, it can be difficult to mount it without a lot of work mocking other parts of your system. If the component interacts with multiple Vuex modules or uses API calls to fetch its own data, you need to simulate all of that when you mount the component. Where end-to-end tests are almost absurdly easy to get up and running on any project that runs in the browser, component tests on existing components are a lot more sensitive to your component design.

This is true of anything that mounts components in isolation, like Storybook and Jest, which we’ve also used. It’s often when you attempt to mount components in isolation that you realize just how many dependencies your components actually have, and it can seem like a lot of effort is needed just to provide the right context for mounting them. This nudges us towards better component design in the long run, with components that are easier to test and while touching fewer parts of the codebase.

For this reason, I’d suggest if you haven’t already got component tests, and so aren’t sure what you need to mock in order to mount your component, choose your first component tests carefully, to limit the number of factors you have to get right before you can see the component in the test runner. Pick a small, presentational component that renders content provided through props or slots, to see it a component test in action before getting into the weeds on dependencies.

Benefits

The component test runner has worked out well for our team. We already have extensive end-to-end tests in Cypress, so the team is familiar with how to spin up new tests and write user interactions. And we have been using Vue Test Utils for individual component testing as well. So there was not actually too much new to learn here. The initial setup issues could have been frustrating, but there are plenty of friendly people out there who can help work through issues, so I’m glad I used the “asking for help” superpower.

I would say there are two main benefits that we’ve found. One is the consistent approach to the test code itself between levels of testing. This helps because there’s no longer a mental shift to think about subtle differences between Jest and Cypress interactions, browser DOM vs jsdom and similar issues.

The other is being able to develop components in isolation and getting visual feedback as we go. By setting up all the variations of a component for development purposes, we get the outline of the UI test ready, and maybe a few assertions too. It feels like we get more value out of the testing process up front, so it’s less like a bolted-on task at the end of a ticket.

This process is not quite test-driven development for us, though we can drift into that, but it’s often “demo-driven” in that we want to showcase the states of a new piece of UI, and Cypress is a pretty good way to do that, using cy.pause() to freeze a running test after specific interactions and talk about the state of the component. Developing with this in mind, knowing that we will use the tests to walk through the components features in a demo, helps organize the tests in a meaningful way and encourages us to cover all the scenarios we can think of at development time, rather than after.

Conclusion

The mental model for what exactly Cypress as whole does was tricky for me to when I first learned about it, because it wraps so many other open source tools in the testing ecosystem. You can get up and running quickly with Cypress without having a deep knowledge of what other tools are being leveraged under the hood.

This meant that when things went wrong, I remember not being sure which layer I should think about — was something not working because of a Mocha thing? A Chai issue? A bad jQuery selector in my test code? Incorrect use of a Sinon spy? At a certain point, I needed to step back and learn about those individual puzzle pieces and what exact roles they were playing in my tests.

This is still the case with component testing, and now there is an extra layer: framework-specific libraries to mount and test components. In some ways, this is more overhead and more to learn. On the other hand, Cypress integrates these tools in a coherent way and manages their setup so we can avoid a whole unrelated testing setup just for component tests. For us, we already wanted to mount components independently for testing with Jest, and for use in Storybook, so we figured out a lot of the necessary mocking ideas ahead of time, and tended to favor well-separated components with simple props/events based interfaces for that reason.

On balance, we like working with the test runner, and I feel like I’m seeing more tests (and more readable test code!) showing up in pull requests that I review, so to me that’s a sign that we’ve moved in a good direction.


The post Testing Vue Components With Cypress appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Jekyll doesn’t do components? Liar!

I like the pushback from Katie Kodes here. I’ve said in the past that I don’t think server-side languages haven’t quite nailed “building in components” as well as JavaScript has, but hey, this is a good point:

1. Any basic fragment-of-HTML “component” you can define with JSX in a file and then cross-reference as <MyComponent key="value" />, you can just as easily define with Liquid in a file and cross-reference in Jekyll as {% include MyComponent.html key=value %}.

2. Any basic fragment-of-HTML “layout” you can define with JSX in a file and then cross-reference as <MyLayout>Hello, world</MyLayout>, you can just as easily define with Liquid in a file and then cross-reference in the front matter of a Jekyll template as layout: MyLayout.

Any HTML preprocessor that can do partials with local variables is pretty close to replicating the best of stateless JavaScript components. The line gets even blurrier with stuff like Eleventy Serverless that can build individual pages on the fly by hitting the URL of a cloud function.

Direct Link to ArticlePermalink


The post Jekyll doesn’t do components? Liar! appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Developer Decisions For Building Flexible Components

Blog posts that get into the whole “how to think like a front-end developer” vibe are my favorite. Michelle Barker nails that in this post, and does it without sharing a line of code!

We simply can no longer design and develop only for “optimal” content or browsing conditions. Instead, we must embrace the inherent flexibility and unpredictability of the web, and build resilient components. Static mockups cannot cater to every scenario, so many design decisions fall to developers at build time. Like it or not, if you’re a UI developer, you are a designer — even if you don’t consider yourself one!

There are a lot of unknowns in front-end development. Much longer than my little list. Content of unknown size and length is certainly one of them. Then square the possibilities with every component variation while ensuring good accessibility and you’ve got, well, a heck of a job to do.

Direct Link to ArticlePermalink


The post Developer Decisions For Building Flexible Components appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Supercharging Built-In Elements With Web Components “is” Easier Than You Think

We’ve already discussed how creating web components is easier than you think, but there’s another aspect of the specification that we haven’t discussed yet and it’s a way to customize (nay, supercharge) a built-in element. It’s similar to creating fully custom or “autonomous” elements — like the <zombie-profile> element from the previous articles—but requires a few differences.

Customized built-in elements use an is attribute to tell the browser that this built-in element is no mild-mannered, glasses-wearing element from Kansas, but is, in fact, the faster than a speeding bullet, ready to save the world, element from planet Web Component. (No offense intended, Kansans. You’re super too.)

Supercharging a mild-mannered element not only gives us the benefits of the element’s formatting, syntax, and built-in features, but we also get an element that search engines and screen readers already know how to interpret. The screen reader has to guess what’s going on in a <my-func> element, but has some idea of what’s happening in a <nav is="my-func"> element. (If you have func, please, for the love of all that is good, don’t put it in an element. Think of the children.)

It’s important to note here that Safari (and a handful of more niche browsers) only support autonomous elements and not these customized built-in elements. We’ll discuss polyfills for that later.

Until we get the hang of this, let’s start by rewriting the <apocalyptic-warning> element we created back in our first article as a customized built-in element. (The code is also available in the CodePen demo.)

The changes are actually fairly simple. Instead of extending the generic HTMLElement, we’ll extend a specific element, in this case the <div> element which has the class HTMLDivElement. We’ll also add a third argument to the customElements.defines function: {extends: 'div'}.

customElements.define(   "apocalyptic-warning",   class ApocalypseWarning extends HTMLDivElement {     constructor() {       super();       let warning = document.getElementById("warningtemplate");       let mywarning = warning.content;        const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(         mywarning.cloneNode(true)       );     }   },   { extends: "div" } );

Lastly, we’ll update our HTML from <apocalyptic-warning> tags to <div> tags that include an is attribute set to “apocalyptic-warning” like this:

<div is="apocalyptic-warning">   <span slot="whats-coming">Undead</span> </div>

Reminder: If you’re looking at the below in Safari, you won’t see any beautiful web component goodness *shakes fist at Safari*

Only certain elements can have a shadow root attached to them. Some of this is because attaching a shadow root to, say, an <a> element or <form> element could have security implications. The list of available elements is mostly layout elements, such as <article>, <section>, <aside>, <main>, <header>, <div>, <nav>, and <footer>, plus text-related elements like <p>, <span>, <blockquote>, and <h1><h6>. Last but not least, we also get the body element and any valid autonomous custom element.

Adding a shadow root is not the only thing we can do to create a web component. At its base, a web component is a way to bake functionality into an element and we don’t need additional markup in the shadows to do that. Let’s create an image with a built-in light box feature to illustrate the point.

We’ll take a normal <img> element and add two attributes: first, the is attribute that signifies this <img> is a customized built-in element; and a data attribute that holds the path to the larger image that we’ll show in the light box. (Since I’m using an SVG, I just used the same URL, but you could easily have a smaller raster image embedded in the site and a larger version of it in the light box.)

<img is="light-box" src="https://assets.codepen.io/1804713/ninja2.svg" data-lbsrc="https://assets.codepen.io/1804713/ninja2.svg" alt="Silent but Undeadly Zombie Ninja" />

Since we can’t do a shadow DOM for this <img>, there’s no need for a <template> element, <slot> elements, or any of those other things. We also won’t have any encapsulated styles.

So, let’s skip straight to the JavaScript:

customElements.define(   "light-box",   class LightBox extends HTMLImageElement {     constructor() {       super();       // We’re creating a div element to use as the light box. We’ll eventually insert it just before the image in question.       let lb = document.createElement("div");       // Since we can’t use a shadow DOM, we can’t encapsulate our styles there. We could add these styles to the main CSS file, but they could bleed out if we do that, so I’m setting all styles for the light box div right here       lb.style.display = "none";       lb.style.position = "absolute";       lb.style.height = "100vh";       lb.style.width = "100vw";       lb.style.top = 0;       lb.style.left = 0;       lb.style.background =         "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";       lb.style.backgroundSize = "contain";        lb.addEventListener("click", function (evt) {         // We’ll close our light box by clicking on it         this.style.display = "none";       });       this.parentNode.insertBefore(lb, this); // This inserts the light box div right before the image       this.addEventListener("click", function (evt) {         // Opens the light box when the image is clicked.         lb.style.display = "block";       });     }   },   { extends: "img" } );

Now that we know how customized built-in elements work, we need to move toward ensuring they’ll work everywhere. Yes, Safari, this stink eye is for you.

WebComponents.org has a generalized polyfill that handles both customized built-in elements and autonomous elements, but because it can handle so much, it may be a lot more than you need, particularly if all you’re looking to do is support customized built-in elements in Safari.

Since Safari supports autonomous custom elements, we can swap out the <img> with an autonomous custom element such as <lightbox-polyfill>. “This will be like two lines of code!” the author naively said to himself. Thirty-seven hours of staring at a code editor, two mental breakdowns, and a serious reevaluation of his career path later, he realized that he’d need to start typing if he wanted to write those two lines of code. It also ended up being more like sixty lines of code (but you’re probably good enough to do it in like ten lines).

The original code for the light box can mostly stand as-is (although we’ll add a new autonomous custom element shortly), but it needs a few small adjustments. Outside the definition of the custom element, we need to set a Boolean.

let customBuiltInElementsSupported = false;

Then within the LightBox constructor, we set the Boolean to true. If customized built-in elements aren’t supported, the constructor won’t run and the Boolean won’t be set to true; thus we have a direct test for whether customized built-in elements are supported.

Before we use that test to replace our customized built-in element, we need to create an autonomous custom element to be used as a polyfill, namely <lightbox-polyfill>.

customElements.define(   "lightbox-polyfill", // We extend the general HTMLElement instead of a specific one   class LightBoxPoly extends HTMLElement {      constructor() {       super();        // This part is the same as the customized built-in element’s constructor       let lb = document.createElement("div");       lb.style.display = "none";       lb.style.position = "absolute";       lb.style.height = "100vh";       lb.style.width = "100vw";       lb.style.top = 0;       lb.style.left = 0;       lb.style.background =         "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";       lb.style.backgroundSize = "contain";        // Here’s where things start to diverge. We add a `shadowRoot` to the autonomous custom element because we can’t add child nodes directly to the custom element in the constructor. We could use an HTML template and slots for this, but since we only need two elements, it's easier to just create them in JavaScript.       const shadowRoot = this.attachShadow({ mode: "open" });        // We create an image element to display the image on the page       let lbpimg = document.createElement("img");        // Grab the `src` and `alt` attributes from the autonomous custom element and set them on the image       lbpimg.setAttribute("src", this.getAttribute("src"));       lbpimg.setAttribute("alt", this.getAttribute("alt"));        // Add the div and the image to the `shadowRoot`       shadowRoot.appendChild(lb);       shadowRoot.appendChild(lbpimg);        // Set the event listeners so that you show the div when the image is clicked, and hide the div when the div is clicked.       lb.addEventListener("click", function (evt) {         this.style.display = "none";       });       lbpimg.addEventListener("click", function (evt) {         lb.style.display = "block";       });     }   } );

Now that we have the autonomous element ready, we need some code to replace the customized <img> element when it’s unsupported in the browser.

if (!customBuiltInElementsSupported) {   // Select any image with the `is` attribute set to `light-box`   let lbimgs = document.querySelectorAll('img[is="light-box"]');   for (let i = 0; i < lbimgs.length; i++) { // Go through all light-box images     let replacement = document.createElement("lightbox-polyfill"); // Create an autonomous custom element      // Grab the image and div from the `shadowRoot` of the new lighbox-polyfill element and set the attributes to those originally on the customized image, and set the background on the div.     replacement.shadowRoot.querySelector("img").setAttribute("src", lbimgs[i].getAttribute("src"));     replacement.shadowRoot.querySelector("img").setAttribute("alt", lbimgs[i].getAttribute("alt"));     replacement.shadowRoot.querySelector("div").style.background =       "rgba(0,0,0, 0.7) url(" + lbimgs[i].dataset.lbsrc + ") no-repeat center";      // Stick the new lightbox-polyfill element into the DOM just before the image we’re replacing     lbimgs[i].parentNode.insertBefore(replacement, lbimgs[i]);     // Remove the customized built-in image     lbimgs[i].remove();   } }

So there you have it! We not only built autonomous custom elements, but customized built-in elements as well — including how to make them work in Safari. And we get all the benefits of structured, semantic HTML elements to boot including giving screen readers and search engines an idea of what these custom elements are.

Go forth and customize yon built-in elements with impunity!


The post Supercharging Built-In Elements With Web Components “is” Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,
[Top]

Using Web Components in WordPress is Easier Than You Think

Now that we’ve seen that web components and interactive web components are both easier than you think, let’s take a look at adding them to a content management system, namely WordPress.

There are three major ways we can add them. First, through manual input into the siteputting them directly into widgets or text blocks, basically anywhere we can place other HTML. Second, we can add them as the output of a theme in a theme file. And, finally, we can add them as the output of a custom block.

Loading the web component files

Now whichever way we end up adding web components, there’s a few things we have to ensure:

  1. our custom element’s template is available when we need it,
  2. any JavaScript we need is properly enqueued, and
  3. any un-encapsulated styles we need are enqueued.

We’ll be adding the <zombie-profile> web component from my previous article on interactive web components. Check out the code over at CodePen.

Let’s hit that first point. Once we have the template it’s easy enough to add that to the WordPress theme’s footer.php file, but rather than adding it directly in the theme, it’d be better to hook into wp_footer so that the component is loaded independent of the footer.php file and independent of the overall theme— assuming that the theme uses wp_footer, which most do. If the template doesn’t appear in your theme when you try it, double check that wp_footer is called in your theme’s footer.php template file.

<?php function diy_ezwebcomp_footer() { ?>   <!-- print/echo Zombie profile template code. -->   <!-- It's available at https://codepen.io/undeadinstitute/pen/KKNLGRg --> <?php }  add_action( 'wp_footer', 'diy_ezwebcomp_footer');

Next is to enqueue our component’s JavaScript. We can add the JavaScript via wp_footer as well, but enqueueing is the recommended way to link JavaScript to WordPress. So let’s put our JavaScript in a file called ezwebcomp.js (that name is totally arbitrary), stick that file in the theme’s JavaScript directory (if there is one), and enqueue it (in the functions.php file).

wp_enqueue_script( 'ezwebcomp_js', get_template_directory_uri() . '/js/ezwebcomp.js', '', '1.0', true ); 

We’ll want to make sure that last parameter is set to true , i.e. it loads the JavaScript before the closing body tag. If we load it in the head instead, it won’t find our HTML template and will get super cranky (throw a bunch of errors.)

If you can fully encapsulate your web component, then you can skip this next step. But if you (like me) are unable to do it, you’ll need to enqueue those un-encapsulated styles so that they’re available wherever the web component is used. (Similar to JavaScript, we could add this directly to the footer, but enqueuing the styles is the recommended way to do it). So we’ll enqueue our CSS file:

wp_enqueue_style( 'ezwebcomp_style', get_template_directory_uri() . '/ezwebcomp.css', '', '1.0', 'screen' ); 

That wasn’t too tough, right? And if you don’t plan to have any users other than Administrators use it, you should be all set for adding these wherever you want them. But that’s not always the case, so we’ll keep moving ahead!

Don’t filter out your web component

WordPress has a few different ways to both help users create valid HTML and prevent your Uncle Eddie from pasting that “hilarious” picture he got from Shady Al directly into the editor (complete with scripts to pwn every one of your visitors).

So when adding web-components directly into blocks or widgets, we’ll need to be careful about WordPress’s built-in code filtering . Disabling it all together would let Uncle Eddie (and, by extension, Shady Al) run wild, but we can modify it to let our awesome web component through the gate that (thankfully) keeps Uncle Eddie out.

First, we can use the wp_kses_allowed filter to add our web component to the list of elements not to filter out. It’s sort of like we’re whitelisting the component, and we do that by adding it to the the allowed tags array that’s passed to the filter function.

function add_diy_ezwebcomp_to_kses_allowed( $ the_allowed_tags ) {   $ the_allowed_tags['zombie-profile'] = array(); } add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

We’re adding an empty array to the <zombie-profile> component because WordPress filters out attributes in addition to elements—which brings us to another problem: the slot attribute (as well as part and any other web-component-ish attribute you might use) are not allowed by default. So, we have to explitcly allow them on every element on which you anticipate using them, and, by extension, any element your user might decide to add them to. (Wait, those element lists aren’t the same even though you went over it six times with each user… who knew?) Thus, below I have set slot to true on <span>, <img> and <ul>, the three elements I’m putting into slots in the <zombie-profile> component. (I also set part to true on span elements so that I could let that attribute through too.)

function add_diy_ezwebcomp_to_kses_allowed( $ the_allowed_tags ) {   $ the_allowed_tags['zombie-profile'] = array();   $ the_allowed_tags['span']['slot'] = true;   $ the_allowed_tags['span']['part'] = true;   $ the_allowed_tags['ul']['slot'] = true;   $ the_allowed_tags['img']['slot'] = true;   return $ the_allowed_tags; } add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

We could also enable the slot (and part) attribute in all allowed elements with something like this:

function add_diy_ezwebcomp_to_kses_allowed($ the_allowed_tags) {   $ the_allowed_tags['zombie-profile'] = array();   foreach ($ the_allowed_tags as &$ tag) {     $ tag['slot'] = true;     $ tag['part'] = true;   }   return $ the_allowed_tags; } add_filter('wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

Sadly, there is one more possible wrinkle with this. You may not run into this if all the elements you’re putting in your slots are inline/phrase elements, but if you have a block level element to put into your web component, you’ll probably get into a fistfight with the block parser in the Code Editor. You may be a better fist fighter than I am, but I always lost.

The code editor is an option that allows you to inspect and edit the markup for a block.

For reasons I can’t fully explain, the client-side parser assumes that the web component should only have inline elements within it, and if you put a <ul> or <div>, <h1> or some other block-level element in there, it’ll move the closing web component tag to just after the last inline/phrase element. Worse yet, according to a note in the WordPress Developer Handbook, it’s currently “not possible to replace the client-side parser.”

While this is frustrating and something you’ll have to train your web editors on, there is a workaround. If we put the web component in a Custom HTML block directly in the Block Editor, the client-side parser won’t leave us weeping on the sidewalk, rocking back and forth, and questioning our ability to code… Not that that’s ever happened to anyone… particularly not people who write articles…

Component up the theme

Outputting our fancy web component in our theme file is straightforward as long as it isn’t updated outside the HTML block. We add it the way we would add it in any other context, and, assuming we have the template, scripts and styles in place, things will just work.

But let’s say we want to output the contents of a WordPress post or custom post type in a web component. You know, write a post and that post is the content for the component. This allows us to use the WordPress editor to pump out an archive of <zombie-profile> elements. This is great because the WordPress editor already has most of the UI we need to enter the content for one of the <zombie-profile> components:

  • The post title can be the zombie’s name.
  • A regular paragraph block in the post content can be used for the zombie’s statement.
  • The featured image can be used for the zombie’s profile picture.

That’s most of it! But we’ll still need fields for the zombie’s age, infection date, and interests. We’ll create these with WordPress’s built in Custom Fields feature.

We’ll use the template part that handles printing each post, e.g. content.php, to output the web component. First, we’ll print out the opening <zombie-profile> tag followed by the post thumbnail (if it exists).

<zombie-profile>   <?php      // If the post featured image exists...     if (has_post_thumbnail()) {       $ src = wp_get_attachment_image_url(get_post_thumbnail_id()); ?>       <img src="<?php echo $ src; ?>" slot="profile-image">     <?php     }   ?>

Next we’ll print the title for the name

<?php   // If the post title field exits...   if (get_the_title()) { ?>   <span slot="zombie-name"><?php echo get_the_title(); ?></span>   <?php   } ?>

In my code, I have tested whether these fields exist before printing them for two reasons:

  1. It’s just good programming practice (in most cases) to hide the labels and elements around empty fields.
  2. If we end up outputting an empty <span> for the name (e.g. <span slot="zombie-name"></span>), then the field will show as empty in the final profile rather than use our web component’s built-in default text, image, etc. (If you want, for instance, the text fields to be empty if they have no content, you can either put in a space in the custom field or skip the if statement in the code).

Next, we will grab the custom fields and place them into the slots they belong to. Again, this goes into the theme template that outputs the post content.

<?php   // Zombie age   $ temp = get_post_meta(the_ID(), 'Age', true);   if ($ temp) { ?>     <span slot="z-age"><?php echo $ temp; ?></span>     <?php   }   // Zombie infection date   $ temp = get_post_meta(the_ID(), 'Infection Date', true);   if ($ temp) { ?>     <span slot="idate"><?php echo $ temp; ?></span>     <?php   }   // Zombie interests   $ temp = get_post_meta(the_ID(), 'Interests', true);   if ($ temp) { ?>     <ul slot="z-interests"><?php echo $ temp; ?></ul>     <?php   } ?>

One of the downsides of using the WordPress custom fields is that you can’t do any special formatting, A non-technical web editor who’s filling this out would need to write out the HTML for the list items (<li>) for each and every interest in the list. (You can probably get around this interface limitation by using a more robust custom field plugin, like Advanced Custom Fields, Pods, or similar.)

Lastly. we add the zombie’s statement and the closing <zombie-profile> tag.

<?php   $ temp = get_the_content();   if ($ temp) { ?>     <span slot="statement"><?php echo $ temp; ?></span>   <?php   } ?> </zombie-profile>

Because we’re using the body of the post for our statement, we’ll get a little extra code in the bargain, like paragraph tags around the content. Putting the profile statement in a custom field will mitigate this, but depending on your purposes, it may also be intended/desired behavior.

You can then add as many posts/zombie profiles as you need simply by publishing each one as a post!

Block party: web components in a custom block

Creating a custom block is a great way to add a web component. Your users will be able to fill out the required fields and get that web component magic without needing any code or technical knowledge. Plus, blocks are completely independent of themes, so really, we could use this block on one site and then install it on other WordPress sites—sort of like how we’d expect a web component to work!

There are the two main parts of a custom block: PHP and JavaScript. We’ll also add a little CSS to improve the editing experience.

First, the PHP:

function ez_webcomp_register_block() {   // Enqueues the JavaScript needed to build the custom block   wp_register_script(     'ez-webcomp',     plugins_url('block.js', __FILE__),     array('wp-blocks', 'wp-element', 'wp-editor'),     filemtime(plugin_dir_path(__FILE__) . 'block.js')   );    // Enqueues the component's CSS file   wp_register_style(     'ez-webcomp',     plugins_url('ezwebcomp-style.css', __FILE__),     array(),     filemtime(plugin_dir_path(__FILE__) . 'ezwebcomp-style.css')   );    // Registers the custom block within the ez-webcomp namespace   register_block_type('ez-webcomp/zombie-profile', array(     // We already have the external styles; these are only for when we are in the WordPress editor     'editor_style' =&gt; 'ez-webcomp',     'editor_script' =&gt; 'ez-webcomp',   )); } add_action('init', 'ez_webcomp_register_block'); 

The CSS isn’t necessary, it does help prevent the zombie’s profile image from overlapping the content in the WordPress editor.

/* Sets the width and height of the image.  * Your mileage will likely vary, so adjust as needed.  * "pic" is a class we'll add to the editor in block.js */ #editor .pic img {   width: 300px;   height: 300px; } /* This CSS ensures that the correct space is allocated for the image,  * while also preventing the button from resizing before an image is selected. */ #editor .pic button.components-button {    overflow: visible;   height: auto; } 

The JavaScript we need is a bit more involved. I’ve endeavored to simplify it as much as possible and make it as accessible as possible to everyone, so I’ve written it in ES5 to remove the need to compile anything.

Show code
(function (blocks, editor, element, components) {   // The function that creates elements   var el = element.createElement;   // Handles text input for block fields    var RichText = editor.RichText;   // Handles uploading images/media   var MediaUpload = editor.MediaUpload;        // Harkens back to register_block_type in the PHP   blocks.registerBlockType('ez-webcomp/zombie-profile', {     title: 'Zombie Profile', //User friendly name shown in the block selector     icon: 'id-alt', //the icon to usein the block selector     category: 'layout',     // The attributes are all the different fields we'll use.     // We're defining what they are and how the block editor grabs data from them.     attributes: {       name: {         // The content type         type: 'string',         // Where the info is available to grab         source: 'text',         // Selectors are how the block editor selects and grabs the content.         // These should be unique within an instance of a block.         // If you only have one img or one <ul> etc, you can use element selectors.         selector: '.zname',       },       mediaID: {         type: 'number',       },       mediaURL: {         type: 'string',         source: 'attribute',         selector: 'img',         attribute: 'src',       },       age: {         type: 'string',         source: 'text',         selector: '.age',       },       infectdate: {         type: 'date',         source: 'text',         selector: '.infection-date'       },       interests: {         type: 'array',         source: 'children',         selector: 'ul',       },       statement: {         type: 'array',         source: 'children',         selector: '.statement',       },   },   // The edit function handles how things are displayed in the block editor.   edit: function (props) {     var attributes = props.attributes;     var onSelectImage = function (media) {       return props.setAttributes({         mediaURL: media.url,         mediaID: media.id,       });     };     // The return statement is what will be shown in the editor.     // el() creates an element and sets the different attributes of it.     return el(       // Using a div here instead of the zombie-profile web component for simplicity.       'div', {         className: props.className       },       // The zombie's name       el(RichText, {         tagName: 'h2',         inline: true,         className: 'zname',         placeholder: 'Zombie Name…',         value: attributes.name,         onChange: function (value) {           props.setAttributes({             name: value           });         },       }),       el(         // Zombie profile picture         'div', {           className: 'pic'         },         el(MediaUpload, {           onSelect: onSelectImage,           allowedTypes: 'image',           value: attributes.mediaID,           render: function (obj) {             return el(               components.Button, {                 className: attributes.mediaID ?                   'image-button' : 'button button-large',                 onClick: obj.open,               },               !attributes.mediaID ?               'Upload Image' :               el('img', {                 src: attributes.mediaURL               })             );           },         })       ),       // We'll include a heading for the zombie's age in the block editor       el('h3', {}, 'Age'),       // The age field       el(RichText, {         tagName: 'div',         className: 'age',         placeholder: 'Zombie's Age…',         value: attributes.age,         onChange: function (value) {           props.setAttributes({             age: value           });         },       }),       // Infection date heading       el('h3', {}, 'Infection Date'),       // Infection date field       el(RichText, {         tagName: 'div',         className: 'infection-date',         placeholder: 'Zombie's Infection Date…',         value: attributes.infectdate,         onChange: function (value) {           props.setAttributes({             infectdate: value           });         },       }),       // Interests heading       el('h3', {}, 'Interests'),       // Interests field       el(RichText, {         tagName: 'ul',         // Creates a new <li> every time `Enter` is pressed         multiline: 'li',         placeholder: 'Write a list of interests…',         value: attributes.interests,         onChange: function (value) {           props.setAttributes({             interests: value           });         },         className: 'interests',       }),       // Zombie statement heading       el('h3', {}, 'Statement'),       // Zombie statement field       el(RichText, {         tagName: 'div',         className: "statement",         placeholder: 'Write statement…',         value: attributes.statement,         onChange: function (value) {           props.setAttributes({             statement: value           });         },       })     );   },    // Stores content in the database and what is shown on the front end.   // This is where we have to make sure the web component is used.   save: function (props) {     var attributes = props.attributes;     return el(       // The <zombie-profile web component       'zombie-profile',       // This is empty because the web component does not need any HTML attributes       {},       // Ensure a URL exists before it prints       attributes.mediaURL &&       // Print the image       el('img', {         src: attributes.mediaURL,         slot: 'profile-image'       }),       attributes.name &&       // Print the name       el(RichText.Content, {         tagName: 'span',         slot: 'zombie-name',         className: 'zname',         value: attributes.name,       }),       attributes.age &&       // Print the zombie's age       el(RichText.Content, {         tagName: 'span',         slot: 'z-age',         className: 'age',         value: attributes.age,     }),       attributes.infectdate &&       // Print the infection date       el(RichText.Content, {         tagName: 'span',         slot: 'idate',         className: 'infection-date',         value: attributes.infectdate,     }),       // Need to verify something is in the first element since the interests's type is array       attributes.interests[0] &&       // Pint the interests       el(RichText.Content, {         tagName: 'ul',         slot: 'z-interests',         value: attributes.interests,       }),       attributes.statement[0] &&       // Print the statement       el(RichText.Content, {         tagName: 'span',         slot: 'statement',         className: 'statement',         value: attributes.statement,     })     );     },   }); })(   //import the dependencies   window.wp.blocks,   window.wp.blockEditor,   window.wp.element,   window.wp.components );

Plugging in to web components

Now, wouldn’t it be great if some kind-hearted, article-writing, and totally-awesome person created a template that you could just plug your web component into and use on your site? Well that guy wasn’t available (he was off helping charity or something) so I did it. It’s up on github:

Do It Yourself – Easy Web Components for WordPress

The plugin is a coding template that registers your custom web component, enqueues the scripts and styles the component needs, provides examples of the custom block fields you might need, and even makes sure things are styled nicely in the editor. Put this in a new folder in /wp-content/plugins like you would manually install any other WordPress plugin, make sure to update it with your particular web component, then activate it in WordPress on the “Installed Plugins” screen.

Not that bad, right?

Even though it looks like a lot of code, we’re really doing a few pretty standard WordPress things to register and render a custom web component. And, since we packaged it up as a plugin, we can drop this into any WordPress site and start publishing zombie profiles to our heart’s content.

I’d say that the balancing act is trying to make the component work as nicely in the WordPress block editor as it does on the front end. We would have been able to knock this out with a lot less code without that consideration.

Still, we managed to get the exact same component we made in my previous articles into a CMS, which allows us to plop as many zombie profiles on the site. We combined our knowledge of web components with WordPress blocks to develop a reusable block for our reusable web component.

What sort of components will you build for your WordPress site? I imagine there are lots of possibilities here and I’m interested to see what you wind up making.

Article series

  1. Web Components Are Easier Than You Think
  2. Interactive Web Components Are Easier Than You Think
  3. Using Web Components in WordPress is Easier Than You Think

The post Using Web Components in WordPress is Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]