The State of Web Components
Web components are a nice little web feature built on top of HTML and JavaScript. They were introduced as a standard back in 2017 by the Google team and in the past few years, the community is slowly shifting towards component-based solutions. The adoption rate does not look as great as one would expect, especially since complete frameworks such as Angular or Vue offer way too many features to be ignored. But there are efforts in all big libraries to ensure compatibility and a distribution of framework-specific components as web components if needed.
Even though they are not that popular yet, the web component space is pretty active as well with projects such as Stencil.js from the Ionic team or LitElement from the same team who worked on the Google's Polymer. In this video we'll first take a look at the basics of the web component standard by building a component from scratch in vanilla.js and then in the second part of the video we'll rewrite our component using LitElement, TypeScript, and Vite. Without further ado, let's jump into it.
Scaffolding a Vanilla JS Component
In an index.html file I'm adding a card input custom element in the HTML markup. We'll work on this implementation in just a second, but first I want to add some controls which will help us pass observable properties to our component and listen to custom events which might be emitted by it.
If you already have experience with UI libraries or frameworks you'll notice that the building blocks of a components are familiar. You'll have properties passed from the outside to change component behavior. You'll have internal state values, scope styling, templating, and other implementation details we'll get into during this video. This JS code is pretty straightforward. We can toggle on or of the compact mode of our custom component by clicking on a toggle button and we can attach an event listener on the card input element based on which a valid message might be displayed.
The Component Class and Shadow DOM
Let's jump into the index.js file now and work on some more interesting code. We'll define a card-input.js class which extends the HTMLElement class provided by the JavaScript runtime. If you are not familiar with object oriented programming, extending an existing class will allow you to inherit various state and behavior already implemented by the framework or the environment you are working in. In the constructor, we are attaching a shadow DOM tree which in short will provide the encapsulation needed to make our component standalone and will ensure that the structure, style, and behavior we define here will not have any effect outside the component scope.
Notice the connectedCallback and the attributeChangedCallback lifecycle methods. The naming here is important because they will be called at specific times while the app is running based on various events that might happen in the app. We'll register our custom component using the define method and then the element should pop up in the page.
Imperative Rendering and State
We'll do quite a lot of work in the renderer method. First of all, you'll see that this method is called from the lifecycle callbacks we defined earlier. Second of all, note that this symbol we are using all over the place. This is how we can define private class attributes or methods in JavaScript. In the renderer method, using template literals and string interpolation we'll define the DOM elements contained by our component. The actual HTML CSS rules can also be attached to the shadow root.
In our HTML I also added a couple of private state attributes to keep track of various internals and then as a best practice we can make sure that the render method is not called multiple times if this is not really necessary. Probably one of the annoying parts of writing vanilla web components is the templating part. Even though there is some support with the help of template strings, most of us are used with a declarative way of conditionally rendering content or attaching event listeners. We'll see a bit later that Lit improves the vanilla experience but for now we'll do things in a more imperative way. So we are adding two input fields in our component. The name field is conditionally rendered based on the compact attribute and the credit card number is always present.
Observing Attribute Changes
Remember that we defined an attributeChangedCallback a little bit earlier. However, we also need to register the reactive attributes in order for the runtime to know what changes to monitor. Note that the observedAttributes getter is static. We are getting back to object oriented programming principles here so know that whenever a method is declared a static, the property will be associated with the class itself and not with any object instantiated from the class. These are pretty basic OOP concepts but I find in the JS world, especially with devs that prefer functional approaches and rarely interact with classes, these details might not be that clear.
Managing Events and this Binding
Okay, so back in the render method, every time a render happens the DOM will be updated and we need to reselect and reattach event handlers to our elements. This code should look familiar if you ever spent time working with a DOM directly. If your background is mostly in a UI framework you rarely have to do such things. The code is straightforward though, you can select any element in your Shadow DOM using the querySelector method and register event callbacks using the addEventlistener method.
As a quick side note my callbacks are defined using the bind method. This is because of some caveats in the way that this keyword works in JavaScript. Again if you are lucky enough you didn't really cut the days when such code was done, or in short whenever an event is triggered that this keyword might point to the window object. To solve this we are forcefully making sure that the function passed as a callback is correctly bound to our component instance.
Back to the code, if the inputs are valid we'll trigger an event using the dispatchEvent method. The event will bubble up the DOM and any designated listeners registered on the component will be executed. Finally whenever the credit card number is updated I am conditionally rendering an appropriate card icon based on the card type.
Introducing Lit and TypeScript
So before moving to the Lit implementation, let's recap the main concepts we've looked at. We said that encapsulation can be achieved with the help of the Shadow DOM and that the outside world can pass information to the component via observable attributes. In return, components can send information to the outside world via events. We've looked at template literals and conditional rendering and I think that we can all agree that the whole coding experience felt a bit of boilerplatey.
As a better alternative to all these, we can achieve the same results with the Lit library. Despite being fairly specialized on a topic that's not that popular, Lit manages to raise some interest. In short, it provides a list of directives, decorators, and helper functions in a reactive context which will remove some of the boilerplate needed to create web components. For this part, I'm going to use TypeScript as well since decorators are not yet an official standard and are not available in all browsers. On top of that, the type safety TypeScript offers will be extremely useful when building standalone components that could be used by other developers. I'm a big fan of Vite so I'm using the Vite Lit starter to get things going.
In the index.html file I'm writing pretty much the same code I did earlier. Note however that the compact flag is now either passed or completely removed as an attribute. By having an attribute present or not in the component tag we can convert the information in a boolean flag on the component side. You'll see all this in just a second.
Defining Properties and State with Lit
In the card-input.ts file we'll start by defining a class extending LitElement. We'll use the custom element decorator to automatically register this as a custom component. Then we'll define a compact property and we'll mark its type as a boolean. Using the state decorator and defining state values for the image, credit card name, and number. Remember that these values are reactive, so whenever any of them will change, the render function will be executed again and the UI will update.
As another quick side note, I'm using the underscore to mark private values. In TypeScript using the private keyword is of course enough but some developers prefer to denote the visibility of variables by naming conventions as well. I'm not really a big fan of this but I'm following it here since this is what the Lit documentation suggests.
Declarative Templating and Styling in Lit
In the render method, the html backtick notation might have caught your eye. This is a tag template and it is a JavaScript feature that allows you to use this more friendlier syntax. Also note that we can register event handlers in a manner somewhat more familiar. We have a more friendlier solution for the CSS rules as well and you don't have to worry about adding them to the template.
Simplified Logic and Final Thoughts
Back to the event handlers, we'll simply fetch the input value and assign it to the internal state. The logic is pretty much the same we had in the vanilla version, but with the state values and this event handling capabilities, things feel a little bit more natural and declarative. We'll do the same validation and image rendering logic based on the credit card type and the implementation is almost complete.
All in all the code base is a bit thinner and easier to maintain, but we only scratch the surface of all the capabilities Lit has to offer. For instance, the library comes with a lot of built-in directives to use inside your HTML templates and to simplify that whole process. So if you have an hour to spare I suggest you spend a bit of time on their documentation page.
The final thing I want to reiterate here is the importance of TypeScript and providing good documentation and implementation details while developing web components. The core idea behind web components is code reusability so you should always make sure your code is not only modular and reusable but also easy to understand.
If you've made it this far and you found this video useful please consider liking it and subscribing to the channel. Thank you for watching.