Tutorials on Web Components

Learn about Web Components from fellow newline community members!

  • React
  • Angular
  • Vue
  • Svelte
  • NextJS
  • Redux
  • Apollo
  • Storybook
  • D3
  • Testing Library
  • JavaScript
  • TypeScript
  • Node.js
  • Deno
  • Rust
  • Python
  • GraphQL
  • React
  • Angular
  • Vue
  • Svelte
  • NextJS
  • Redux
  • Apollo
  • Storybook
  • D3
  • Testing Library
  • JavaScript
  • TypeScript
  • Node.js
  • Deno
  • Rust
  • Python
  • GraphQL

Form-associated custom elements FTW!

Before Shadow DOM, you needed a framework to encapsulate component templates or styling. Shadow DOM was a game-changer because it allows you to code UI components without their logic clashing with other components using just the web platform. Shadow DOM poses challenges when HTML elements encapsulated by Shadow DOM need to participate in a form. In this post, I'll provide an overview of form-associated custom elements. Form-associated custom elements is a web specification that allows engineers to code custom form controls that report value and validity to HTMLFormElement , while also promoting a fully accessible user experience. With the encapsulation provided by Shadow DOM, engineers can code UI components where the CSS styling doesn't collide with other components. Shadow DOM provides a DOM tree for an element separated from the rest of the Document Object Model (DOM). The separation of concerns promoted by Shadow DOM is a boon for coding reusable UI components. While Shadow DOM has several benefits, there are some complications when elements embedded in Shadow DOM have to interact with HTMLFormElement . Suppose you wanted to code a custom checkbox component using Shadow DOM. Checkboxes usually require a significant amount of CSS styling that overrides the browser defaults to match a given mockup. You code an autonomous custom element and style the HTMLInputElement with type="checkbox" in the context of Shadow DOM so the styling doesn't conflict with other elements. You give the component a tag name of my-checkbox . Just when you think you're following best practices, you place an instance of the custom element as a child of HTMLFormElement . Upon inspection in Dev Tools, you may notice the HTMLInputElement cannot participate with the form. You can inspect this phenomenon in this CodeSandbox . HTMLInputElement by design can report value and validity back to HTMLFormElement , but only when HTMLInputElement is a direct descendent of HTMLFormElement . When coding reusable components it's a good idea to provide an interface for web engineers that's familiar. It's typical for HTMLInputElement that are direct descendants of HTMLFormElement to have access to the parent form directly on the element. You can inspect this behavior in the following CodeSandbox . Since the HTMLInputElement is found in an entirely different DOM tree (Shadow DOM), the HTMLFormElement doesn't recognize the HTMLInputElement . In short, HTMLInputElement embedded in Shadow DOM can't participate in the form. In 2019, a new specification was proposed that solves this issue. Form-associated custom elements allow web engineers to use the benefits of Shadow DOM while providing an API that enables custom elements to participate in HTMLFormElement . Form-associated custom elements have all the benefits of autonomous custom elements. They can implement Shadow DOM and use the typical custom lifecycle hooks because they inherit from HTMLElement . If you've coded autonomous custom elements, learning how to code form-associated custom elements is fairly similar. In the following examples, I'll demonstrate how a checkbox embedded in Shadow DOM can participate in an HTML form by implementing formAssociated and ElementInternals . Before I mentioned that form controls that are native to the browser like HTMLInputElement automatically participate in HTMLFormElement . The form control is added to an Array-like interface on HTMLFormElement , allowing web engineers to loop through the form controls to handle common tasks like validation. For the Checkbox component to participate in HTMLFormElement the same way, you simply need to set the value of a static property named formAssociated to true . the below example written with TypeScript does just that. If you wish to follow along, fork this CodeSandbox and start coding. Inversely, if you wish to reference HTMLFormElement on instances of Checkbox , similar to how HTMLInputElement behaves when it's a direct descendant of HTMLFormElement , a method inherited from HTMLElement called attachInternals can be called with provides the same interface, along with the Accessibility Object Model (AOM). By setting a property on Checkbox named _internals to what's returned by attachInternals , you effectively add the ElementInternals interface to Checkbox . Later in this post, I'll provide an example of how you can reference a method on the ElementInternals interface that aids with validation. Before that, we should resolve some discrepancies between Checkbox and a typical HTMLInputElement . If we expect engineers to reuse this component, it should behave similarly to HTMLInputElement , which has a well-known interface. To provide parity between HTMLInputElement and Checkbox , let's define some getters and setters on Checkbox . First, make a getter that returns a reference of the HTMLInputElement so you can easily reference the element with this.checkbox throughout the logic of the component. Next, define a getter and setter for the state of the checkbox. It's probably a good idea to make the HTMLInputElement the single source of truth here. Any getter and setter defined on Checkbox either returns the value or sets the value of checked on this.checkbox . We could introduce several more properties on Checkbox to provide parity between it and a typical HTMLInputElement , but we'll stop there for now. While coding a UI library filled with form-associated custom elements, I found a couple of challenges in making the components reusable. Suppose you wanted to add validation logic to Checkbox and have the class another method called onValidate , including all the logic there. In the below example, I call setValidity on the ElementInternals interface, which reports the validity of the input to HTMLFormElement . This is convenient, however placing this logic here doesn't give a web engineer the ability to configure validations per business logic in different scenarios. A higher-level validation pattern is required that would allow engineers to loop through form controls and validate an entire form. Another challenge had to do with making inline validation messages accessible. Getting screen readers to interpret validation messages as errors that should be read aloud at first seems tricky because of Shadow DOM, although is possible using WAI-ARIA attributes. Suppose this were the template instead of just the input. If the form control is invalid, custom logic could populate the <div class="message"> with relevant content. The WAI-ARIA attributes provide an immediate response for screen readers. Did you like what you read here? In the book Fullstack Web Components , you'll code a form-associated custom element, bringing all the features necessary to reuse that component in an enterprise-grade web application. You'll not just learn how to provide parity between the form control well-known elements like HTMLInputElement , but also discover a pattern for implementing reusable validations that validate an entire form. You'll also tackle challenges with making form-associated custom elements accessible. This is just in Chapter 3! Fullstack Web Components provides everything you need to know to code an entire UI library of custom elements. Are you looking to code Web Components now, but don't know where to get started? I wrote a book titled Fullstack Web Components:Complete Guide to Building UI Libraries with Web Components , a hands-on guide to coding UI libraries and web applications with custom elements. In Fullstack Web Components , you'll...

Thumbnail Image of Tutorial  Form-associated custom elements FTW!

Build Your Own JavaScript Micro-Library Using Web Components: Part 4 of 4

In this capstone tutorial, we're going to actually use the micro-library in app code so you can see how the micro-library makes things easier for developers in real world development. In the previous steps of this 4-part tutorial, this is what we accomplished: In this final tutorial, we will now refactor an example component to use the @Component decorator and the attachShadow function from our micro-library. We're refactoring a file, packages/component/src/card/Card.ts , which contains the CardComponent class. This is a regular Web Components custom element. To get it to use our micro-library, we first import Component and attachShadow from our micro-library. Next, we add the Component decorator to CardComponent . We remove the line at the bottom of the file that registers the component, noting the tag name in-card . Remove customElements.define('in-card', CardComponent); . The above code is now automated by our micro-library. We set the selector property to the ElementMeta passed into Component to in-card , the same string originally used to register the component. Next, we move the content of the style tag in the constructor to the new style property on ElementMeta . We do the same for the template of CardComponent . We migrate the HTML to the new template property until the ElementMeta is filled in. Next, we remove everything in the constructor and replace it with a call to our micro-library's attachShadow function, passing in this to the first argument. This automates Shadow DOM setup. To make sure everything is working properly, this is where we start up the development server and observe the changes in the browser. Nothing should have changed about the user interface. Everything should appear the same. Our CardComponent has now been successfully refactored to use the micro-library's utilities, eliminating boilerplate and making the actual component code easier to reason about. That completes this 4-part tutorial series on building a micro-library for developing with Web Components. Our micro-library supports autonomous and form-associated custom elements. It enables developers to automate custom element setup as well as Shadow DOM setup, so they can focus on the unique functionality of their components. In the long run, these efficiencies add up to a lot of saved time and cognitive effort. If you want to dive more into ways to build long-lived web apps that use Web Components and avoid lock-in into specific JavaScript frameworks, check out Fullstack Web Components: Complete Guide to Building UI Libraries with Web Components.

Thumbnail Image of Tutorial Build Your Own JavaScript Micro-Library Using Web Components: Part 4 of 4

I got a job offer, thanks in a big part to your teaching. They sent a test as part of the interview process, and this was a huge help to implement my own Node server.

This has been a really good investment!

Advance your career with newline Pro.

Only $30 per month for unlimited access to over 60+ books, guides and courses!

Learn More

Build Your Own JavaScript Micro-Library Using Web Components: Part 3 of 4

Here is Part 3/4 of our tutorial series on building a JavaScript micro-library for creating your apps with Web Components. As I pointed out in previous lessons, the micro-library eases the path to development with Web Components, automating a lot of the work so developers can build their apps faster. Here's what we covered so far: Now in this tutorial, Part 3, we will automate another piece of functionality for classes that use our decorator. In this case, we'll automatically attach a Shadow DOM to those classes so that the user of the library does not have to manually create a Shadow DOM for their custom elements. Now that we have ElementMeta stored on the prototype of any class using the Component decorator, our next step is to write a reusable function that'll be used in the constructor of the same class to instantiate the Shadow DOM. By abstracting this logic to a reusable function , we'll reduce several lines of code in each component implementation down to one line. Basically, we want to take something like this... ...and reduce it to one line. The first argument of attachShadow is the instance of the class which, in the constructor , you can reference as this . The second argument is the Object that configures the call to element.attachShadow . You can read more about element.attachShadow on MDN . To start development of this new function , make a new directory named template in packages/common/src and create a new file in that directory named index.ts . Create another file in the directory, named shadow.ts . In packages/common/src/template/shadow.ts , create a new function named attachShadow and export it. Declare two arguments for attachShadow : context and options . Make options optional with ? and type define context as any , and options as ShadowRootInit , a type definition exported from lib.dom.d.ts . Follow up in packages/common/src/template/index.ts and ensure attachShadow is exported for the main index.ts . Finally, in packages/common/index.ts , export the attachShadow function. Jumping back to packages/common/src/template/shadow.ts , fill in the algorithm for attachShadow . Make a const named shadowRoot , type defined as ShadowRoot , equal to context.attachShadow . On the next line, make a const named template , equal to document.createElement('template') . This line creates a new HTML template. Set the content of the HTML template using the ElementMeta stored on the prototype of whatever class will use this attachShadow function. Pass in context.elementMeta.style to a style tag and context.elementMeta.template afterward. Finally, append a clone of the HTML template to the ShadowRoot . When you are finished, the attachShadow function should look like this: With Component and attachShadow now supporting autonomous and form-associated custom elements, you can now use the new decorator pattern in actual components. Build the @in/common package again so files inside the @in/ui package can pick up the latest changes. We're almost done building this Web Components micro-library, though there's a lot more features you could add. In the final lesson in building our micro-library, we'll refactor some example components to use the micro-library so you can see how end developers actually use the library. For more about building UI Libraries using Web Components, check out our latest book Fullstack Web Components: Complete Guide to Building UI Libraries with Web Components.

Thumbnail Image of Tutorial Build Your Own JavaScript Micro-Library Using Web Components: Part 3 of 4

Build Your Own JavaScript Micro-Library Using Web Components: Part 2 of 4

As we covered in the previous tutorial , a micro-library helps developers implement common features more quickly than using the web APIs directly. Now in this tutorial, we will code our class decorator that allows our classes to instantly gain new features without writing the code for them every time. The class decorator eliminates boilerplate and allows users of the micro-library to focus on higher-level concepts while delegating common class setups to the micro-library. Implementing a class decorator is going to significantly improve the developer experience, allowing developers to code Web Components faster. We'll enhance the class-based implementation which custom elements rely on by cleaning up each constructor . Specifically, we move the declaration of an HTML template and CSS to a new function that decorates the class . This, combined with abstracting template and style generation to a reusable function , will have the positive effect of reducing the boilerplate for each component. Here's an example of what a typical component will look like when it uses the decorator in our micro-library. This ButtonComponent will now look something like the following. We'll use a mono-repo structure for this project. So, create a packages/common directory with one source file: index.ts . To keep this package organized, we should figure out a way to divide the code into logical chunks. Making a directory for all decorators seems prudent, then organizing the decorators by function. To start developing the component decorator, create a new directory in packages/common/src/decorator , then make a new file at packages/common/src/decorator/index.ts , and another file at packages/common/src/decorator/component.ts . Any file named index.ts manages exports for several files in the same directory. Other files in a directory contain library code. Open packages/common/src/decorator/component.ts in your IDE to begin. First, let's define the interface used by the component decorator. This keeps the first argument of the decorator function type safe. Declare a new interface named ElementMeta and ensure it is exported with the export keyword. There are three properties on this interface : selector , style , and template . All three properties are optional, denoted by the ? prior to : . TypeScript class decorators are factory functions, that is, they return a function that constructs an Object . The Object we're concerned with is the class definition of the component using this decorator. Based on the values of properties on the ElementMeta , we'll modify the prototype of the class so we can use those properties to register the component with the CustomElementRegistry and instantiate an HTML template with Shadow DOM. The basic structure of the function is as follows: Let's put a fail-safe mechanism in for users who haven't specified an ElementMeta in the first argument of Component . The above conditional checks if meta is undefined and when truthy, displays a console error for developers in Dev Tools, then aborts the rest of the algorithm that depends on ElementMeta . We have two objectives for the class decorator: register the component using the selector property, then add metadata on the class definition to later append an HTML template to Shadow DOM using the style and template properties on the ElementMeta . Let's start with registering the component via the decorator function. In the factory function, reference the selector and pass it and the class definition, here named target , to customElements.define . This allows the end-user to omit a call to customElements.define in their implementation. The user has the option to ignore selector entirely in the ElementMeta , so be sure to wrap the call to define in a conditional. Class decorators are a place where you can modify the prototype of a class , so that's exactly what we're going to do. We're going to store the ElementMeta passed through the Component function arguments on the prototype of the class , so we can later access the template and style in the constructor of the component class . Set a new property called elementMeta on the prototype to the meta that was passed into the Component function . To avoid the style and template properties from ever being undefined , here is a good place for some conditionals that set each of these properties to an empty string in case it is undefined . When you are finished with the Component decorator, the function should look like this: Development of the Component decorator is finished, but you still need to provide the decorator and the interface for use outside the @in/common package . To finalize development of the decorator, add the proper exports to packages/common/src/decorator/index.ts and packages/common/index.ts . In packages/common/src/decorator/index.ts add the following exports. Export the same from packages/common/index.ts . index.ts is the entry point of the @in/common package. When the @in/common package is built, other packages in the monorepo will be able to reference anything exported from the index.ts . Check the build successfully runs, which means your TypeScript is valid. If you get an error like zsh: command not found: lerna make sure lerna is installed globally and try the build command again. If you see lerna success output in the Terminal, you are ready to proceed. Otherwise, check your TypeScript is correct. Now that we have ElementMeta stored on the prototype of any class using the Component decorator, we can write a reusable function that could be used in the constructor of the same class to instantiate Shadow DOM. Later, we'll work with Shadow DOM and abstract its setup into the functionality of our micro-library, leaving library users with one less detail to worry about. For more about Web Components, check out our latest book Fullstack Web Components: Complete Guide to Building UI Libraries with Web Components

Thumbnail Image of Tutorial Build Your Own JavaScript Micro-Library Using Web Components: Part 2 of 4

Build Your Own JavaScript Micro-Library Using Web Components: Part 1

If you've ever wondered how libraries like React , Preact , or Svelte work under the hood, this is a great exploration of what you need to know. Using Web Components means that your own micro-library, which we build in this series, will work easily with any JavaScript codebase. This achieves greater levels of code reuse. Let's dive in. When building with Web Components, you will rely heavily on the set of specifications that make up Web Components: The main benefits Web Components bring to the table: reuse, interoperable, accessible, having a long lifespan, are due to their reliance on browser specifications. Had we adopted a library or framework, we might have lost some or all of these characteristics in the user interfaces built with it. UI components coded with some libraries aren't interoperable with other JavaScript libraries or frameworks, which puts a hard limit on reuse. Even though we gain these benefits from Web Components, we have lost any benefit JavaScript libraries have to offer. Frameworks and libraries like React, Vue, Angular, and Svelte provide an abstraction around browser specifications. React, for example, famously opted for a purely functional approach, giving engineers "hooks" to manage side effects in user interfaces. JavaScript libraries and frameworks provide architectural patterns that make life easier on the part of the web developer, offering features not available with browser specifications alone, like data binding and state management. What if we could retain the benefits of Web Components while also gaining the architectural design of a JavaScript framework? Indeed, there are many such Web Component libraries already. Now you get to build your own. In this 4-part tutorial series , we'll demystify the inner workings of Web Components libraries as you get to develop your own. Using TypeScript decorators, we'll develop a new interface that simplifies development but doesn't compromise on performance. The micro-library we'll code optimizes to less than 1Kb of minified JavaScript. Among its features is that it allows the components we develop to transform from something like this: ...to instead use a TypeScript decorator named Component , like this: Decorators are denoted by the @ symbol, followed by the name of the decorator function , in this case, Component . In the above example, the Component function is called with a single argument: an Object where the developer can declare the tag name, CSS style, and HTML template. These are the advantages of using class decorators to handle templates and styling: In addition to making a class decorator that allows you declare a tag name, styling, and template with a cleaner interface than before, you'll also code a method decorator that simplifies binding event listeners to custom elements. Instead of typing this.addEventListener('click', this.onClick) , what if you could decorate the onClick method and still provide the same functionality? It could look something like this: If all of this seems foreign, don't fret. Coding decorators is much like coding any JavaScript function . Providing these framework-like features to custom elements may be easier than you think. Think of a micro-library as a collection of prewritten code used for common development tasks that has a small footprint. Micro-libraries may have similar functionality as much larger libraries but with way less code that optimizes down to a few Kb or maybe even less than 1 Kb. That's why they are "micro". Micro-libraries have existed for a while. Famously, Preact is a ~3 Kb alternative to the ~45 Kb React. Micro-libraries exist, in part, to provide a more performant alternative to popular JavaScript libraries. In the context of custom elements, micro-libraries are an interesting solution because we can gain the functionality of a JavaScript library with little expense with regards to performance. In Parts 2-4 of this micro-library tutorial series, I'll show you how to identify reusable parts of Web Components code and abstract logic away from each component implementation in a functional manner. You'll code a class decorator that handles declaration of a component selector, styling and template. You'll learn how to use method decorators to attach event listeners to DOM elements. You'll have a basic Web Components micro-library you can expand upon and use in actual apps. For a deep dive into Web Components, check out our latest book -  Fullstack Web Components: Complete Guide to Building UI Libraries with Web Components . It covers how to build robust UI libraries and entire applications using Web Components. 

Thumbnail Image of Tutorial Build Your Own JavaScript Micro-Library Using Web Components: Part 1

Fullstack Web Components is now LIVE 🎉

Web Components are a standard JavaScript technology whose adoption has soared in recent years. Since it enables your components to work in any JavaScript code base , whether you are using frameworks/libraries like React, Angular, Vue, or vanilla JavaScript, you can use Web Components everywhere. Author Stephen Belovarich , Principal Software Development Engineer at Workday, unpacks practical ways to build real world apps using the latest evolution of the web spec. In Part 1 of the book , you learn the basics of Web Components and build some standard component building blocks. Part 2 walks you step by step through building a library for Web Components and leveraging the library in actual development. In Part 3 , you integrate Web Components into a full app with JavaScript on the front end as well as Node.js with Express on the backend. In the course of building these practical projects, these are some of the skills you will learn: Get hands-on experience coding UI with Web Components, but also learn how to test and maintain those components in the context of a distributed UI library in Fullstack Web Components .

Thumbnail Image of Tutorial Fullstack Web Components is now LIVE 🎉

Web Components in Server-Side Rendered (SSR) and Static-Site Generated (SSG) in Next.js Applications

Compared to other web development frameworks, Next.js has become very popular because of its support for a number of rendering strategies that result in highly performant frontend applications: By having the content readily available in the initial HTML document, less client-side rendering takes place. All the client has to do is hydrate the components and make them functional. The less dynamic content the client renders, the better the application's performance and SEO. Search engine bots like Googlebot can visit the application, immediately crawl (and understand) its content and rank it high on search engines. If you use Web Components to build your application's UI, then you don't need to worry about the breaking changes that third-party JavaScript frameworks/libraries notoriously cause (as a result of rapid development and release cycles). Your application performs equally as well as (or better than) applications that use these frameworks/libraries. Additionally, the low-level, native APIs of Web Components are based on an official W3 specification and let you develop encapsulated, modular and reusable components (represented as custom HTML elements). However, you will come across several limitations that can prevent the application from taking advantage of these rendering strategies: Custom web components are defined with a class that extends from HTMLElement and registered using the customElements.define method. Since browser APIs do not exist in the context of Node.js, Next.js's build tools cannot run code that uses these APIs. This also applies to the Shadow DOM . The Shadow DOM's imperative nature prevents custom web components from being rendered by either the server or build tools. The missing browser APIs can be "polyfilled" and "shimmed" for these environments before the custom web component is defined with the class keyword. This approach makes it possible to simulate the DOM and output an HTML string for the initial HTML document, but it lacks the ability to hydrate components. Fortunately, the Declarative Shadow DOM provides an alternative way for implementing web components that complements server-side rendering and static-site generation. Below, I'm going to show you how to pre-render custom web components in Next.js. By extracting the logic used to create the template that is attached to the component's innerHTML into a separate module, the server (or build tools) never has to run code that uses browser APIs like HTMLElement and customElements . For any custom web component that has a shadow root attached to it, we will use the Declarative Shadow DOM so that the shadow root is attached to it and readily available at the time of the component's instantiation. To get started, scaffold a new Next.js application with the TypeScript boilerplate template. Once the application has been scaffolded, change the current directory to the application's directory, like so: In this tutorial, we want to test that the custom web components are pre-rendered when the application is server-side rendered or static-site generated. Next.js provides several CLI commands for building and running the application: dev , build , start and lint . For server-side rendering, we want to build a production version of the application and spin up a Next.js production server that runs getServerSideProps on each incoming request and pre-renders this application with the returned props. Since next start starts the server, let's add a prestart npm script that runs the build npm script, which builds the application via the next build command. ( package.json ) For static-site generation, we want to export the application to static HTML. Let's add an export npm script that exports the application to static HTML via the next export command. Since the application must be built prior to being exported, let's also add a preexport npm script that runs the build npm script. ( package.json ) Within the pages/index.tsx file, remove the children elements under the <main /> element, and replace the text within the <footer /> element's child <a /> element with the text "Powered by Vercel." ( pages/index.tsx ) Now that there are no more <Image /> components in the application, let's remove the import Image from 'next/image' line from the pages/index.tsx file. This Next.js application will feature two custom web components: Since the <pinned-location-link /> component will not use a Shadow DOM, let's create this component such that its rendering logic (what's set to innerHTML ) is completely decoupled from its class declaration. First, create a directory named components , which will house these custom web components. Within this directory, create two subdirectories, pinned-location-link and info-tooltip , each of which will contain two files: Let's define and register the <pinned-location-link /> component. To keep things simple, components' props will only come from their attributes. ( components/pinned-location-link/component.ts ) Note #1 : When using these components in JSX/TSX code, attributes must be prefixed with data- (custom data attributes). Otherwise, TypeScript will raise the following error: Property '<attribute>' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>' . Note #2 : connectedCallback is a special lifecycle callback that gets called after the custom element is inserted into the DOM. Note #3 : To fix the TypeScript error Type 'NamedNodeMap' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators. on [...this.attributes] , set the downlevelIteration option to true under the compilerOptions in the tsconfig.json file. This allows you to iterate over iterators in TypeScript code. Attributes for any node are represented as a NamedNodeMap , not as an Array . The connectedCallback lifecycle callback... Since all JSX elements come with a dangerouslySetInnerHTML property, this approach allows Next.js to not have to run the component.ts file to set the component's innerHTML . You don't have to polyfill and shim browser APIs like HTMLElement and customElements for Next.js's build tools. Let's write the template function that generates the component's stringified HTML from the passed props. ( components/pinned-location-link/template.ts ) This template function supports several types of location search queries used in a Google Maps URL, each of which is optional: Based on the passed props, we can add a class to the <a /> element in the stringified HTML. We can also modify the Google Maps URL and the text displayed within its child <span /> element. Inside of the <main /> element of the <Home /> page component in the pages/index.tsx file, add the <pinned-location-link /> component, like so: ( pages/index.tsx ) Currently, Next.js doesn't know what the HTML content inside of the <pinned-location-link /> component is supposed to be. However, if we give it the dangerouslySetInnerHTML property and set the __html key to the result of the component's template function, then Next.js can render the component's HTML content without ever having to define or register it as a custom element. To demonstrate this, let's import the template function from the components/pinned-location-link/template.ts file and add the dangerouslySetInnerHTML property to the component, like so: ( pages/index.tsx ) Note : If any data comes from a third-party service, then you may want to pass the result of template to an HTML sanitizer like DOMPurify to mitigate cross-site scripting (XSS) attacks. Unfortunately, TypeScript raises the following error: Property 'pinned-location-link' does not exist on type 'JSX.IntrinsicElements' . Since the <pinned-location-link /> component is not a React component or standard HTML element, TypeScript does not recognize it as a valid JSX element. Therefore, we must create a type definition file ( types/index.d.ts ) that lets us add custom elements to the JSX.IntrinsicElements global interface: ( types/index.d.ts ) With the TypeScript error no longer popping up in VSCode, let's build the application and run it in production mode: Note : Add export {}; to the empty components/info-tooltip/component.ts and components/info-tooltip/template.ts files. Otherwise, the build will fail. Within a browser, visit localhost:3000 . Then, disable JavaScript and reload the page. When you inspect the elements on the page, you will see that the HTML content of the <pinned-location-link /> component still exists despite JavaScript being disabled. Now, let's define and register the <info-tooltip /> component. Unlike the <pinned-location-link /> component, this component will use the Shadow DOM. A hidden, separate DOM will be attached to the component (the custom element becomes a shadow host). All CSS rules defined within the Shadow DOM will be scoped to elements that are part of the shadow tree, and CSS rules outside of the Shadow DOM that specify the same selectors will have no effect on these elements. The Shadow DOM will be implemented declaratively, not imperatively. The Declarative Shadow DOM involves wrapping the component's content in a <template /> element with a shadowroot attribute. When this attribute is set to open , the shadow root's internal features can be accessed with JavaScript. On the other hand, imperatively setting up the shadow root involves calling the Element.attachShadow method to attach a shadow tree to the custom element. ( components/info-tooltip/component.ts ) Since both the <pinned-location-link /> and <info-tooltip /> components extract props from data attributes, we should refactor this logic into a function named extractPropsFromAttrs so that it can be shared by both components. ( shared/index.ts ) Don't forget to update the components/pinned-location-link/component.ts file so that the connectedCallback lifecycle callback calls this method. ( components/pinned-location-link/component.ts ) Once you have made that update, create the <info-tooltip /> component's template. The <info-tooltip /> component only accepts two props: ( components/info-tooltip/template.ts ) Unlike the <pinned-location-link /> component's template, the <info-tooltip /> component's template gets wrapped within a <template /> element by default during server-side rendering or static-site generation. If the Declarative Shadow Root does not exist when the page is initially loaded, then via client-side rendering, the shadow root gets set up imperatively with the Element.attachShadow method. The <template /> element is only for the Declarative Shadow DOM. For us to use it in the JSX code, add the <info-tooltip /> custom element to the JSX.IntrinsicElements global interface. ( types/index.d.ts ) With everything completed, let's render this component within the <Home /> page component. Import the template function from the components/info-tooltip/template.ts file and add the dangerouslySetInnerHTML property to the component, like so: Note : Since both files, components/info-tooltip/template.ts and components/pinned-location-link/template.ts , export a template function as the default export, both functions will need to be renamed to avoid conflicting function names. Let's rebuild the application and run it in production mode: Within a browser, revisit localhost:3000 . JavaScript should still be disabled from before. When you inspect the elements on the page, you will see that the HTML content of the <info-tooltip /> component lives within a Shadow DOM despite JavaScript being disabled. Suppose we re-enable JavaScript. If we add data attributes to the <pinned-location-link /> and <info-tooltip /> components rendered in the <Home /> page component, then the components should initially be rendered with the props passed to the template function for the dangerouslySetInnerHTML property. Once client-side rendering takes place, the components should be rendered with the props extracted from the data attributes. Let's add some data attributes to these components. ( pages/index.tsx ) Then, rebuild the application and run it in production mode: When you reload the page, you will see that nothing has changed. This is because we have not yet imported the component.ts files into the Next.js application. Since the server and Next.js build tools do not run the useEffect lifecycle hook, let's dynamically import these files in useEffect . After the initial render of the page, useEffect will run on the client-side, import these files and execute them. These custom elements will officially be registered by the browser, and the component will be updated and re-rendered using the data attributes. ( pages/index.tsx ) Then, let's temporarily modify the components/info-tooltip/component.ts file so that regardless of whether or not the Declarative Shadow Root exists as a result of SSR/SSG, the <info-tooltip /> component's content will be updated using the props extracted from the component's data attributes: ( components/info-tooltip/component.ts ) Rebuild the application once more and run it in production mode: When you reload the page, you will see that the content of the components is initially rendered via the dangerouslySetInnnerHTML property. As soon as the client fetches the component.ts files and executes them, the content of the components updates accordingly with the data attributes set on these components. If you disable JavaScript and reload the page again, then the client never fetches the component.ts files, and you fallback to the components in their initial render state once more. Afterwards, undo the changes made to the components/info-tooltip/component.ts file. Now let's test that the application works properly when static-site generated. Run the following command to export the application as static HTML: With the static HTML (e.g., index.html and 404.html files) exported to an out directory by default, serve the directory as a static site via serve : Within a browser, visit localhost:3000 and reload the page with JavaScript enabled/disabled to verify everything still works properly. Currently, only Google Chrome and Microsoft Edge natively support Declarative Shadow DOM. To provide support for this feature in other browsers, we must use the @webcomponents/template-shadowroot ponyfill . Unlike polyfills, which are found in HTML document's <head /> , ponyfills are found at the end of the HTML document's <body /> . Since it gets executed after the DOM is fully loaded, the ponyfill will be able to find all the <template /> elements that have a shadowroot attribute and convert them into shadow roots on the <template /> elements' parent elements (done via the ponyfill's hydrateShadowRoots method). In the Next.js application, we will dynamically import the ponyfill in the useEffect lifecycle hook after the <Home /> page component's initial render. Once the ponyfill has been successfully imported, we check if the client natively supports Declarative Shadow DOM. If not, then we call hydrateShadowRoots on the HTML document's <body /> . Afterwards, we proceed to dynamically import the two component.ts files like before. First, let's install the @webcomponents/template-shadowroot ponyfill: Then, dynamically import the ponyfill in the useEffect lifecycle hook, like so: ( pages/index.tsx ) If you find yourself stuck at any point while working through this tutorial, then feel free to visit the main branch of this GitHub repository here for the code. Try passing data to custom web components from props returned by getServerSideProps and getStaticProps . If you want to learn more advanced techniques with web components, then check out the Fullstack Web Components  book by Steve Belovarich, a software engineer who has many years of industry experience building out enterprise-grade web applications.

Thumbnail Image of Tutorial Web Components in Server-Side Rendered (SSR) and Static-Site Generated (SSG) in Next.js Applications

Creating Autonomous Web Components in Storybook

Ever since the introduction of open source libraries and frameworks like React and Vue , the most popular approach for building user interfaces has been the composition of small, reusable components to create larger, more complex components and views. An often glossed over alternative that follows a similar paradigm is Web Components . As a collection of browser standards maintained by the WHATWG and W3C, Web Components lets you develop custom components, and use them anywhere in your application like regular HTML elements, with native browser APIs. It consists of three primary specifications that are implemented by all major browsers. Together, these specifications make self-contained components, each one encapsulating its own set of functionalities and structure: Third-party libraries and frameworks are known to frequently release new versions and introduce breaking changes. However, the lengthy, rigorous process of reviewing and approving changes to official specifications by the WHATWG and W3C results in far less breaking changes, which means less wasted development time. Using Web Components, components can extend from native HTML elements to augment their capabilities (i.e. clicking a table cell pops open a tooltip) while also retaining their accessibility characteristics. Commonly, Web Components can be found in applications built with Lit (successor of the deprecated Polymer library) or AMP . If we have to share components across different Web Components-based applications, then we need to be sure that these components can work standalone regardless of their environment. By building them in isolation within a UI development sandbox like Storybook , we can design resilient components that behave deterministically and cover a greater variety of common and edge use cases without the influence of outside data or business logic. Storybook allows you to visually test your components. Additionally, it organizes your components in a single place and serves as documentation for them. Below, I'm going to show you how to create autonomous Web Components-based components in Storybook. To get started, clone this component library template from GitHub: This template has Rollup , ESLint and TypeScript automatically configured so that we can focus exclusively on building and designing the library's components. Within this new project, install Storybook via the Storybook CLI. When the installation prompts you to pick a Storybook project type, select the html option. For this project, we will write the components with plain HTML and JavaScript. No JSX. No syntax that requires a compilation step. Note : The web_components option is for projects running Lit. For this tutorial, we will be creating the custom elements from scratch with native browser APIs. Delete the stories directory to remove all example stories from the project. Create two new directories, src and src/components , to house the custom elements made with Web Components. To verify that Storybook has been properly installed and recognizes our library's components, let's create a simple <hello-world /> custom element. ( src/components/HelloWorld.ts ) Note : The names of custom elements must be kebab-cased and cannot be single words. A custom element named "helloworld" will not be registered since it is considered a single word, whereas "hello-world" will be registered since there is a dash between each word. Define a new story for this component in a new file: ( src/components/HelloWorld.stories.ts ) A story describes a state that is supported by a component. Depending on its state, a component renders accordingly and demonstrates how a state addresses a specific use case. Here, we define a story for the <HelloWorld /> component named "Default." It receives no arguments and simply demos the default use case of displaying the text "Hello World" within a <span /> tag. All stories rely on a template, which determines how a story is rendered based on the arguments passed to it. In this case, since there are no arguments involved, the template function just returns stringified markup of the custom element <hello-world /> being used. The bind() method lets us make a new copy of the template per story. This way, we can customize the template based on each story's unique set of arguments. If you look inside the .storybook/main.js file, you will notice that Storybook expects all files matching the glob patterns ../src/**/*.stories.mdx and ../src/**/*.stories.@(js|jsx|ts|tsx) to export ( export default ) a configuration object following the Component Story Format . With this open standard, the default export represents the component's metadata, and each named export represents a story. The only required property is component , which we set to the component itself. The optional title property is set to the name of the component displayed in the sidebar of the Storybook. Slashes in the title help to organize the stories into distinct groupings. Now, let's create a slightly more complex component. This component, an informational tooltip, can be attached to the end of a phrase or sentence, and its contents can be customized based on whatever further elaboration you wish to provide. Within the src/components directory, create a new file named InfoTooltip.ts . ( src/components/InfoTooltip.ts ) The HTML template contains a <slot /> element, which allows for dynamic content that can be injected in place of the <slot /> element. In the context of the Shadow DOM, slotted content is accessible through the Light DOM as long as mode is set to open . Find the slotted content by executing document.querySelector('[slot="content"]') . For example, to recreate the tooltip shown in the previous picture, we nest an element with the slot attribute (set to the name of the <slot /> element to replace, which in this case is content ) within the <info-tooltip /> custom element: Within the src/components directory, create another new file named InfoTooltip.stories.ts . Let's write three stories for this component: ( src/components/InfoTooltip.stories.ts ) The template accepts one argument: the text to display within the tooltip. For each story, we define the arguments needed to describe the use case it covers. When we load Storybook, notice how the tooltip fails to fit within the canvas when hovering over (or focusing) the trigger. Let's fix this by applying a global decorator that wraps all of our stories' templates in a <div /> element with top and left margining of 4rem . ( .storybook/preview.js ) Reload Storybook. Now the tooltip fits within the canvas! Install @storybook/addon-controls as a dev. dependency: Add the Controls addon to the list of registered addons: ( .storybook/main.js ) By integrating the Controls addon to the configuration object, we can freely modify the text argument within the Storybook UI and see the resulting render in the canvas. The properties on argTypes map to what Storybook displays as the argument name in the Controls panel. In this case, the text argument is given the control name text . To enforce the text type for the text control, set the type to text . Assigning a text type for a control yields a text field next to the control's name in the Controls panel. For a final version of this project, visit the GitHub repository here . If you started out with a component-based library/framework like React, Vue, Angular or Svelte, then try out Web Components. To supercharge Web Components with features like reactivity and directives, consider using Lit .

Thumbnail Image of Tutorial Creating Autonomous Web Components in Storybook