Javascript

A Tale of Form Autofill, LitElement and the Shadow DOM

Jamie Kuppens
6 min read
A Tale of Form Autofill, LitElement and the Shadow DOM
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Many web applications utilize forms in places be it for logging in, making payments, or editing a user profile. As a user of web applications, you have probably noticed that the browser is able to autofill in certain fields when a form appears so that you don't have to do it yourself. If you've ever written an application in Lit though, you may have noticed that this doesn't always work as expected.

The Problem

I was working on a frontend project utilizing Lit and had to implement a login form. In essence these aren’t very complicated on the frontend side of life. You just need to define a form, put some input elements inside of it with the correct type attributes assigned to it, then you hook the form up to your backend, API, or whatever you need to call to authenticate by adding a submit handler.

However, there was an issue. The autocomplete doesn’t appear to be working as expected. Only the username field was being filled, but not the password. When this happened, I made sure to check documentation sites such as MDN and looked at examples. But I couldn’t find any differences between theirs and mine. At some point, I prepared a minimal reproducible example without Lit, and I was able to get the form working fine, so it had to do something with my usage of Lit.

After doing a little bit of research and some testing, I found out this happened because Lit relies very heavily on something known as the Shadow DOM. I don’t believe the Shadow DOM is necessarily supposed to break this functionality. But for most major browsers, it doesn’t play nice with autocomplete for the time being. I experienced slightly different behavior in all browsers, and the autocomplete even worked under Shadow DOM with Firefox in the Lit app I was working on.

The solution I ended up settling on was ensuring the form was contained inside of the Light DOM instead of the Shadow DOM, whilst also allowing the Shadow DOM to continue to be used in places where autofillable forms are not present. In this article I will show you how to implement this solution, and how to deal with any problems that might arise from it.

Shadow DOM vs. Light DOM

The Shadow DOM is a feature that provides a way to encapsulate your components and prevent unrelated code and components from affecting them in undesired ways. Specifically, it allows for a way to prevent outside CSS from affecting your components and vice versa by scoping them to a specific shadow root.

When it comes to the Light DOM, even if you’ve never heard of the term, you’ve probably used it. If you’ve ever worked on any website before, and interacted with the standard DOM tree, that is the Light DOM. The Light DOM, and any Shadow DOMs under it for that matter, can contain Shadow DOMs inside of them attached to elements. When you add a Lit component to a page, a shadow root will get attached to it that will contain its subelements, and prevent CSS from outside of that DOM from affecting it.

Using Light DOM with Certain Web Components

By default, Lit attaches a shadow root to all custom elements that extend from LitElement. However, web components don’t actually require a shadow root to function. We can do away with the shadow root by overriding the createRenderRoot method, and returning the web component itself:

createRenderRoot(): ShadowRoot | this {
  return this;
}

Although we can just put this method in any element we want exposed into the Light DOM. We can also make a new component called LightElement that overrides this method that we can extend from instead of LitElement on our own components. This will be useful later when we tackle another problem.

Uh oh, where did my CSS styling and slots go?

The issue with not using a shadow root is Lit has no way to encapsulate your component stylesheets anymore. As a result, your light components will now inherit styles from the root that they are contained in. For example, if your components are directly in the body of the page, then they will inherit all global styles on the page. Similarly when your light components are inside of a shadow root, they will inherit any styles attached to that shadow root.

To resolve this issue, one could simply add style tags to the HTML template returned in the render() method, and accept that other stylesheets in the same root could affect your components. You can use naming conventions such as BEM for your CSS classes to mitigate this for the most part. Although this does work and is a very pragmatic solution, this solution does pollute the DOM with multiple duplicate stylesheets if more than one instance of your component is added to the DOM.

Now, with the CSS problem solved, you can now have a functional Lit web component with form autofill for passwords and other autofillable data! You can view an example using this solution here.

Screenshot 2023-05-23 155514

A Better Approach using Adopted Stylesheets

For a login page where only one instance of the component is in the DOM tree at any given point, the aforementioned solution is not a problem at all. However, this can become a problem if whatever element you need to use the Light DOM with is used in lots of places or repeated many times on a page. An example of this would be a custom input element in a table that contains hundreds of rows. This can potentially cause performance issues, and also pollute the CSS inspector in your devtools resulting in a suboptimal experience both for users and yourself.

The better, though still imperfect, way to work around this problem is to use the adopted stylesheets feature to attach stylesheets related to the web component to the root it is connected in, and reuse that same stylesheet across all instances of the node.

Below is a function that tracks stylesheets using an id and injects them in the root node of the passed in element. Do note that, with this approach, it is still possible for your component’s styles to leak to other components within the same root. And like I advised earlier, you will need to take that into consideration when writing your styles.

export function injectSharedStylesheet(
  element: Element,
  id: string,
  content: string
) {
  const root = element.getRootNode() as DocumentOrShadowRoot;

  if (root.adoptedStyleSheets != null) {
    evictDisconnectedRoots();

    const rootNodes = documentStylesheets[id] ?? [];
    if (rootNodes.find(value => value === root)) {
      return;
    }

    let sharedStylesheet = sharedStylesheets[id];
    if (sharedStylesheet == null) {
      sharedStylesheet = new CSSStyleSheet();
      sharedStylesheet.replaceSync(content);
      sharedStylesheets[id] = sharedStylesheet;
    }

    root.adoptedStyleSheets.push(sharedStylesheet);
    if (documentStylesheets[id] != null) {
      documentStylesheets[id].push(root);
    } else {
      documentStylesheets[id] = [root];
    }
  } else {
    // FALLBACK: Inject <style> manually into the document if adoptedStyleSheets
    // is not supported.

    const target = root === document ? document.head : root;
    if (target?.querySelector(`#${id}`)) {
      return;
    }

    const styleElement = document.createElement('style');
    styleElement.id = id;
    styleElement.appendChild(document.createTextNode(content));

    target.appendChild(styleElement);
  }
}

This solution works for most browsers, and a fallback is included for Safari as it doesn’t support adoptedStylesheets at the time of writing this article. For Safari we inject de-duplicated style elements at the root. This accomplishes the same result effectively.

Let’s go over the evictDisconnectedRoots function that was called inside of the injection function. We need to ensure we clean up global state since the injection function relies on it to keep duplication to a minimum. Our global state holds references to document nodes and shadow roots that may no longer exist in the DOM. We want these to get cleaned up so as to not leak memory. Thankfully, this is easy to iterate through and check because of the isConnected property on nodes.

function evictDisconnectedRoots() {
  Object.entries(documentStylesheets).forEach(([id, roots]) => {
    documentStylesheets[id] = roots.filter(root => root.isConnected);
  });
}

Now we need to get our Lit component to use our new style injection function. This can be done by modifying our LightElement component, and having it iterate over its statically defined stylesheets and inject them. Since our injection function contains the de-duplication logic itself, we don’t need to concern ourselves with that here.

import { CSSResult, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { injectSharedStylesheet } from './style-injector.js';

export interface SharedStylesheet {
  id: string;
  content: CSSResult;
}

@customElement('light-element')
export class LightElement extends LitElement {
  static sharedStyles: SharedStylesheet[] = [];

  connectedCallback() {
    const { sharedStyles } = this.constructor as any;
    if (sharedStyles) {
      sharedStyles.forEach((stylesheet: SharedStylesheet) => {
        injectSharedStylesheet(
          this,
          stylesheet.id,
          stylesheet.content.toString()
        );
      });
    }

    super.connectedCallback();
  }

  createRenderRoot(): ShadowRoot | this {
    return this;
  }
}

With all that you should be able to get an autocompletable form just like the previous example. The full example using the adopted stylesheets approach can be found here.

Conclusion

I hope this article was helpful for helping you figure out how to implement autofillable forms in Lit. Both examples can be viewed in our blog demos repository. The example using basic style tags can be found here, and the one using adopted stylesheets can be found here.

About the author

Jamie Kuppens

Jamie Kuppens

Senior Software Engineer

I’m a software engineer with an interest in web development and some more esoteric things like emulator development.

Keep reading

View all posts →

Building a Lightweight Component with Lit

Component based apps make it much easier to build independent components and share them across projects. With broken down units as small as a button or user interactions, project iterations tend to become faster and more developer friendly. For this article, we will use Lit to build web components for our front end apps. What is Lit? According to the docs, Lit is a simple library for building fast, lightweight web components. Using Lit, you can build highly reusable components with the following features: Native component for the browser aka Web Component Framework agnostic, sharable and usable across multiple web frameworks Lightweight, not requiring too much JavaScript code to implement Flexible and can easily be plugged into any future project and still work as expected. Prerequisites To follow along with this article, I recommend you have: Basic knowledge of JavaScript and a frontend framework (React, Angular or Vue) Code Editor For this, we will be using the Lit JavaScript starter kit. Clone the repo to your local machine And change directory Install dependencies. Open the project with a code editor, and navigate to the file list element.js: Our web component ListElememt is a class that extends to the LitElement base class with the styles() and property() methods. The styles() method returns the CSS for the component that uses the css method imported from Lit to define component scoped CSS in a template literal. The properties() method returns the properties which expose the component's reactive variables. We declare an internal state of todoList to hold the list of todos. Then, we assign default values to properties in the constructor method. todoList is an empty array. The render() method returns the view using the html imported from Lit to define the html elements in a template literal. In the rendered html, we added an input element with id of fullname and a button with a click event triggering the custom method pushTodo to push new Todo to the todoList array. Then, we map through the list of todos with map directives imported from Lit and attach a button to each item with an event to remove the item when clicked, calling a custom method removeTodo that filters through the array, and returns items that satisfy our condition. The getter method input() returns the input element we created to give access to the input value so we can pass it to the pushTodo method. Now, to render the component, we will modify the index.html head section. We will also need to modify the body element of our index.html file. Now, run the app to see our modification, run: From the browser, navigate to localhost:8000, and you should see the element rendered below. Benefits of Lit Components Now we have a running app with a reusable component. Let's explore the Lit benefits of this component, and what differentiates it from other tools for building components. Native : Lit is built on top of the Web Components standards and you are able to extend just what you need for productivity: reactivity, declarative templates, and a handful of thoughtful features to reduce boilerplate and make your job easier. In our ListElement code, we defined the custom HTML element with window.customElements.define which defines and registers a new custom element to the browser DOM and associates it with an element name list element, making our component native. Every Lit component is built with web platform evolution in mind. Fast and Small : Lit has a 5 kb compressed size and a minified bundle which allows your application to load faster without the need to rebuild a virtual tree (No virtual DOM). Lit batches updates to maximize performance and efficiency. Setting multiple properties at once triggers only one update, performed asynchronously at microtask timing. Interoperable & future ready : Every Lit component is a native web component with the superpower of interoperability. Web components work anywhere you use HTML with any framework, or none at all. This makes Lit ideal for building shareable components, design systems, or maintainable, future ready sites and apps. Here is how you can quickly make our ListElement component usable and shareable across any project built with any framework or without one. For the dev/index.html head, we simply imported the JavaScript module of list element.js and quickly add the custom HTML element to the body: Reactivity with Lit : Lit components receive input and store their state as JavaScript class fields or properties. Reactive properties are properties that can trigger the reactive update cycle when changed, re rendering the component, and optionally be read or written to attributes. All JavaScript properties in Lit are defined inside this properties object, and then we used the constructor method in our App component class to set an initial value for the properties we created. Events : In addition to the standard addEventListener API, Lit introduces a declarative way to add event listeners. You will notice we used @ expressions in our template to add event listeners to elements in our component's template. We added the @click event and bind it into the template. Declarative event listeners are added when the template is rendered. Lifecycle Lit components use the standard custom element lifecycle methods. In addition, Lit introduces a reactive update cycle that renders changes to DOM when reactive properties change. Lit components are standard custom elements and inherit the custom element lifecycle methods. In our ListElement class, we have the constructor() method which is called when an element is created. Also, it’s invoked when an existing element is upgraded, which happens when the definition for a custom element is loaded after the element is already in the DOM. You will notice in our pushTodo() method we initiated a Lifecycle update by calling requestUpdate() method to perform an update immediately. Lit saves any properties already set on the element. This ensures values set before the upgrade are maintained and correctly override defaults set by the component. This is so we can push new items to the array and immediately re render the component. Conclusion Congratulations! You`ve learned how to build lightweight components with Lit.dev, and explore some of the benefits of using native web components which can be used across frameworks and non frameworks apps. What is the exciting thing you will be building with Lit? Let us know on Twitter!...

Hassan Sani5 mins
LitJavaScript

How to integrate Web Components using Lit in Angular

In previous posts, I explained how to create projects in Lit and TypeScript from scratch. Also, I covered different topics about Web Components, and Single Page Applications, using them. In this tutorial, I'll explain the needed steps to integrate these Web Components in Angular. Spoiler: You can find the source code of a demo project at the end. Project Setup Prerequisites You'll need to have installed the following tools in your local environment: Node.js . Preferably the latest LTS version. A package manager . You can use either NPM or Yarn. This tutorial will use NPM. Creating the Angular Project Let's create a project from scratch using the Angular CLI tool. This command will initialize a base project using some configuration options: routing. It will create a routing module. prefix corp. It defines a prefix to be applied to the selectors for created components(corp in this case). The default value is app. style css. The file extension for the styling files. skip tests. It avoids the generations of the .spec.ts files, which are used for testing. Install Lit Lit is a simple library for building fast, lightweight web components. Lit is available via npm, let's install it as a new dependency for the current project. Find more information about Lit here. Install Web Components Polyfills There are several ways to install the Web Components polyfills. In this case, we'll install it using npm. Then you can use the webcomponents loader.js, which allows loading a minimum polyfill bundle. Update the Angular Configuration You can load the polyfills using a tag into the index.html file. However, the Angular way to do it is by adding a new asset and script configurations into your angular.json file as follows: Both input and output properties must be relative to the project's root path. The same applies to the script imported. Creating your Web Components using Lit The project setup has been completed, and now it's time to create our first web component using Lit. Let's create an src/web components/card user folder, and then create two files: card user.ts and user.ts. The user.ts file will define a common model used by the Web Component implementation, and the Angular project: Next, let's define the TypeScript code for our Web component into card user.ts file: The previous class defines a new custom element as a brand new widget for our project. The @customElement decorator allows the component definition using a name for it: card user. It is applied at the class level. The static styles attribute defines the styles for the component using a tagged template literal (css). The @property decorator, which allows declaring properties for the custom element in a readable way. The render method returns the HTML content through a template literal (html). This function will be called any time the user property changes. You can read more about how to define a component using Lit here. Using Web Components in Angular Import the Web Component Before using any Web Component in an Angular component, we'll need to make sure to import its definition as follows. Use the Custom Element The next step is to use the brand new custom element as part of any template. In this case, let's use it in our app.component.html file: Once you save these changes, you may find an error in your command line interface (the place where you're running ng serve). Fortunately, the previous error message is significant enough to understand that we'll need to apply a change in the app.module.ts file, which is the main module of our application. The important part here is to import the CUSTOM ELEMENTS SCHEMA from the @angular/core package, and then use it in the schemas property. You can find more information about it here. Save your changes again, and the magic will happen : ) Using the Property Binding It is convenient to know that we can not only import external components in our application but also make use of the Property binding. To do that, let's define a user property in the app.component.ts file: Then, we can update the related template, and use the brackets notation to bind the property: Even better, you can create an array of users to be able to render them using other nice features from our favorite framework, such as the structural directive ngFor. Using the Event Binding It's time to listen and respond to user actions through the Event binding with Angular. As you saw before, the corp user component is able to dispatch an edit event once the "Edit" button is clicked. Let's use the parentheses notation this time to bind a method: Finally, let's create the edit method in our component. The edit method receives a generic Event. However, the Custom Element has sent a User object through the CustomEvent detail. Then, we can use the as syntax operator from TypeScript and get access to it using the detail property. The next screenshot shows the results in the browser's console. Source Code of the Project Find the complete project in this GitHub repository: angular lit web components. Do not forget to give it a star ⭐️ and play around with the code. Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work....

Luis Aviles5 mins
AngularLitTypeScriptWeb Development

Double Click: Try Out the New Releases for jQuery UI & Lit

Welcome to the Double Click! This is the weekly blog series that shines a spotlight on emerging technologies, technological concepts, and community projects that enrich the JavaScript Ecosystem! This week, I’m diving into some updates from two web development technologies that have recently seen updated releases: jQuery UI & Lit. jQuery 1.13.0 rc.2 jQuery UI is a popular JavaScript library that sees wide use across the web despite the technology not previously receiving an update in about five years. Omg! That’s why I’m thrilled to see jQuery UI receive some much needed love in the form of this 1.13.0 rc.2 release, which contains no breaking changes, but sort of polishes off some of its deprecated features to support wider use in modern user interfaces. The jQuery UI team has noted that it has struggled over the years to find reliable contributors, which may explain why it has taken five years to see any updated releases. However, the team notes that they presently don’t intend to significantly expand the framework, and that it will now enter a maintenance phase to simply ensure that it is compatible with future jQuery releases, and secure. Lit 2.0 Lit has gone through a number of changes with this most recent release which officially launched last week. They have a new logo, a new name (having previously been called "Lit Element”), a new npm, and a ton of new features. If you aren’t familiar with Lit, it is a library that simplifies and supercharges the process of building and implementing web components from Google. With this new release, the Lit team has slashed the size of the library to 6KB, just a fraction of the size of your average JavaScript framework, which increases its speed and performance. But this isn’t the only significant improvement to the technology as the Lit team has also introduced new features like a class based API for directive authors, templating enhancements, and reactive controllers. But what most caught my attention was the significant improvements to the Lit site and documentation. Aside from undergoing some major UI transformations, the new Lit website also features detailed documentation that includes a sandbox where users can test out Lit 2.0’s newest features in the browser! What are you excited about lately? Make sure to share with me on twitter @ladyleet....

Tracy Lee2 mins
GeneralJavaScriptLitRelease

Getting Started with Here Maps and Web Components using Lit

In the past months, I have been actively working on the development of geolocation oriented solutions in web applications. My experience has been good so far, taking into account the availability of modern tools and libraries, with a wide range of features that even enable the use of interactive maps and location services. In this article, I'll explain how to integrate Here Maps through web components, using Lit and TypeScript, in just a few steps. Introduction Let's start by explaining the technologies that we will be integrating into this post, step by step. Lit Lit is the next generation library for creating simple, fast, and lightweight web components. One of the benefits of using Lit is the power of interoperability, which means that your web components can be integrated within any framework, or can even be used on their own. This is possible since we're talking about native web components that can be rendered in any modern web browser. If you're interested in learning more about this technology, you can take a look at the LitElement Series that has been published in this blog. Here Maps Let's say your application requires real time navigation, the creation of custom maps, or the ability to visualize datasets over a map. HERE Maps meet these requirements without too much effort. In fact, there is a Maps API for JavaScript, which can be used to build web applications using either JavaScript or TypeScript. Note Although this guide provides an API key for testing purposes only, you may need to register your app, and get your API Key to move forward with your applications. Creating the Project Project Scaffolding An easy way to initialize an application is through the project generator from the Open Web Components initiative. They provide a set of tools and guides to create a web components application, adding the testing, linting, and build support in the beginning. Let's create the project using a command line interface Pay attention to the project structure that is generated. Also, take a look at your package.json file to review the dependencies and scripts that have been added. Update to Lit In case you generated a project based on LitElement, it shouldn't be complicated to replace dependencies with lit: Install the last version of Lit Uninstall LitElement and lit html library Also, you can make sure to update the imports as follows to make it work: Next, make sure the preview of the application is working fine using the following command: Creating a Map Component Installing Here Maps Before creating a web component that will render a map, you are required to load the API code libraries in our index.html file: You can see HERE Maps API for JavaScript Modules for more information about each imported script. Since the initial project is based on TypeScript, we may need to install the type definitions for the HERE Maps API. Now we're ready to go and code the web component using TypeScript. The Map Component Implementation Let's create a src/map/map.ts file with the following content: The above code snippet allows the definition of a new custom element through the @customElement decorator. Also, it defines the location value as a component property so that it can be configured later as: Also, the render method is ready to return a TemplateResult object which contains a div element that will be rendered in all the available space. The element is the HTML container in which the map will be rendered later. To finish the component implementation, we'll need to write the firstUpdated method. Before creating the map object, it's required to initialize the communication with the back end services provided by HERE maps. In that way, an H.service.Platform object is created using the value you got in previous steps. However, most of the code in the previous block has to do with the initialization of the map: An instance of H.Map class is created specifying the container element, the map type to use, the geographic coordinates to be used to center the map, and a zoom level. In case you need to make your map interactive, an H.mapevents.Behavior object can be created to enable the event system through a MapEvents instance. On other hand, the default UI components for the map can be added using the H.ui.UI.createDefault method. The resize listener can be used to make sure the map is rendered in the entire container. Using the Map Component As a final step, we'll need to update the spa heremaps lit.ts file: Since this class defines a new custom element as a container for the map component, it's required to import the previous component definition using import './map/map'. Then, inside the class definition we can find: The static styles attribute defines the styles for the component using a tagged template literal(css) The render method returns the HTML content through a template literal(html). The HTML content uses the previous custom element we created: The screenshot shows the map rendered in the main component of the application. Source Code of the Project Find the latest version of this project in this GitHub repository: spa heremaps lit. If you want to see the final code for this article, take a look at 01 setup lit heremaps tag. Do not forget to give it a star ⭐️ and play around with the code. Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work....

Luis Aviles5 mins
LitTypeScript