Getting Started with Web Components & Lit | Part 3

David Bethune
Level Up Coding
Published in
12 min readJul 3, 2022

--

Part 3 | Build a Single Page App with Lit

Lit, from Google, is a simple way to start working with web components. In Part 1 of this series, I explain why I call Lit the anti-framework for web applications. In Part 2, we created a working web component with Lit and the Vite development server. In this post, we’ll create a single page app from scratch that lets us navigate to different panels of web content… without a framework!

Quick Index
Part 1: Introducing Web Components
Part 2: Setting Up Lit and Vite
Part 3: Building a Single Page App

Jurassic Design: Individual Pages

Back in the Jurassic era of web development, we used to make separate pages for each function of a website’s interface, like welcoming them with an index.html page but showing a shopping cart with cart.asp. Each phase of a user’s progress was controlled by whatever code was on that page on the server — and whatever page the user had navigated to with the URL. You might remember seeing warnings from this era, such as, “Do not use your browser’s back button during this process!”

Another artifact from this kind of software development is pages with extraordinarily long URLs with enormous unreadable codes and multiple parameters which, if not copied or entered 100% correctly, simply result in an error page. You’ve been sent these kind of horrible URLs in emails. They often only work for you, or only once, or only under some other circumstance.

This prehistoric method of navigating also left dino-sized droppings for us to debug. When every aspect of a user’s experience is controlled by separate URLs and pages of code, it’s difficult for us as humans to follow how the interface is being created — or to fix it when it’s wrong. What might be worse is that these kind of interfaces are also extraordinarily difficult to add features to because the pages and URLs don’t know anything about each other. Every step is an independent animal.

The business website for my games company is made from a fancy version of what I’ll teach you here. All of the buttons and panels are custom web components with Lit.

A Moment for MVC

To get away from the zoo, we need a single way to organize and present page content. This method is called MVC or Model-View-Controller. This method goes by many names has many fancy (and irrelevant) flavors like MVVM and others. But the bottom line is that we need a model to hold data. All data should be in the model and nowhere else. We need a view which comprises everything the user can see and touch. And we need controller software that moves stuff in-and-out of the model and view.

Enter the SPA

Single page applications replace the idea of individual pages of code and UI with a single page, usually index.html. That page acts as the controller in the MVC design pattern. Whatever views that page loads (inside its HTML <body>) will appear to the user to be the one single page they are viewing. Whatever data is available to any modules that index loads is the model part of MVC. If index can get to the data, it’s part of our model.

This arrangement is fabulous for debugging because there is no question about who or what is creating any output you see. The answer is always the same. The output is coming from the HTML being drawn by the web components that you caused index to load. By carefully regulating that content, you create a smooth user experience without unnecessary long URLs or complicated passing of data between your components.

Many of the world’s most popular applications today are SPAs. They’re especially popular in consumer-facing apps like Spotify because, with a good MVC design, single page apps can be robust and consistent while providing a high quality, custom user interface.

Spotify is a good example of a single page app that’s also cross-platform and built entirely from web components. Clicking on buttons and album covers triggers functions in the controller that update the view. The model is two parts, a model for your data and an orthogonal one for Spotify’s music library.

Simulating Navigation with Routing

Since the URL in an SPA doesn’t need to have any references to any real code (like filenames or parameter values), we can design our URLs to look however we want. We connect our designer URL to a visible view with an idea called routing. Originally, this meant simulating navigation by routing the user to another page. But since we’re not inside anyone else’s framework with Lit, we can make our routes mean anything we want to.

Creating a Multiple Page Viewer

Let’s create the web components for a page viewer app that acts like a simple website. We’ll store all our page data in a JSON model and write two custom web components, one for the “home” (a container for the page) and one for the page itself. Finally, we’ll add routing so viewers could “visit” various pages.

One of the W3C rules for web components is that your custom components must have a two part name, separated by a dash. We can use this together with another good programming practice called namespacing to separate our components from anyone else’s by using the same starting letters for all of our parts and modules. For this example, I’ll start all my web components with dta. So we’ll be making <dta-home> and <dta-page>.

The actual app we’re building is a simplified version of The DTA Games Company website, which you can visit to see some more complex layouts built with these same ideas.

Organizing Your App

Let’s setup a dta folder (or your chosen namespace name), then create a components folder and a global folder underneath it. You’ll also want a modules folder for future expansion. I use this pattern to adhere to a strict rule that anything in the view must live in the components folder. Other Typescript functions that aren’t dependent on a particular view become their own modules and live in that folder. I put stuff that “touches everything,” like startup stuff, loaders, fonts, and styles into my global folder.

To create a new folder, on the left side of VSCode, right-click in an empty area and choose New Folder. To create a new file inside a folder, right-click the folder and choose New File.

Creating Your First Component

Create a new dta-home.ts file (or another name with your namespace at the start) in your components folder. Here’s a bare minimum Lit component with a custom CSS style and plain text output.

Our custom element needs only 3 parts: the import statement, a exported class, and a render() function.

This first stab at <dta-home> needs only two parts in addition to its Lit imports. First is a @customElement decorator which is a shortcut in Lit to wire the HTML element <dta-home> to your code. You must use the exact name that appears in your HTML, separated with a dash, shown here on line 6 as dta-home. In addition, it’s good practice to use a camelCase version of the same name for your class, shown here on line 7 as dtaHome.

I apologize for the ugly warnings and squiggles in these screenshots. The unnecessary warnings can be removed with some settings tweaks, but that’s for another article. This is all correct code, I promise!

Making Components Accessible

I mentioned in Part 2 that the modularity of Lit comes from importing and exporting components and Typescript modules. To prevent “import spaghetti,” I use a module loader technique. The loader exports all the modules you’ve written. Then, then only the module loader itself needs to be loaded or imported to make your full component library available everywhere.

Using a global component loader means you only have to import one file to have access to all your components in the HTML you output.

Add a component-loader.ts file under your global folder. For now, it only needs to export the single component we’ve written (line 9).

Adding the Component to The Page

The minimal index.html page contains our new component, <dta-home>, and a component loader.

Our page viewer’s index.html file is not that different from the sample component that we saw from Vite in Part 2. But instead of a <script> load in the <head> for a particular component, it has a <script> load in the <body> for our universal component loader.

To add future components to our app, we only need to export them from the component loader, not fiddle with them here in index.html.

Viewing Our Output

We used the Shadow DOM, mentioned in Part 2, to create this ugly, big orange text.

Vite should have been keeping up in the background while we’ve been writing. Let’s look at https://localhost:3000 and check out our page.

That Shadow DOM Again

Two things to notice here. Where the styles came from, and where the content came from. This big ugly text is an example of the shadow DOM I mentioned earlier. It only lives in this component (thankfully) and can’t spread its ugliness to anyone else. Here’s the code that created it:

The Shadow DOM takes an argument that starts with a css decorator.

A static variable in Lit is one that isn’t passed from the outside (like an attribute or value). The css decorator here, written up against backticks, is a shortcut way of saying “this thing is CSS.” The :host syntax means it’s the CSS for this web component, <dta-home>. Finally, the em size for text means “times normal.” So this font is 2x the user’s normal text size. There’s four cool tricks in one small example. Take note, fearless coder!

Wherefore Art Thou, Content?

The greeting text itself is coming from the render() function. Let’s look at it:

The render() function should return html or svg using a decorator and backtick notation.

As I mentioned in Part 2, the render function runs every time the component is updated. In this case, that’s going to be whenever someone visits or refreshes index.html because our <dta-home> component is a child of index.

Notice the use of the html decorator here along with backtick notation (called tagged template literals) in which you can bury variables and functions. We’ll look at that in a moment in our page component.

This HTML is so simple it’s just plain text, not even a <div>. And I show you this for a reason. You can use this simplicity to compose anything more complicated that you like, from custom SVG components that output graphics and animation, or prebuilt components like video players that already have HTML written that you want to “light up” with Lit.

Adding the Page Component

We can design the API or syntax for our component by just writing an example use case, then coding for it.

Instead of this greeting, let’s have our <dta-home> component return a <dta-page> component showing the page we want. We can scaffold the kind of component that we want first, then write the behavior. Since we don’t have any routing yet, we’ll hard-code a page name to start with it, then evolve it to use something from the URL.

Writing the Page Component

Let’s add a new dta-page.ts file with some content we can look at. I just pasted and edited the contents of the dta-home.ts file, and you can, too. Here’s mine:

For my <dta-page> component, I just changed the style a bit and added some hard-coded output.

The default behavior for custom web components is to do nothing. This allows you to design a new component without wiring it up first, simply by writing an example of the new component where you want to use it. Your un-wired component won’t cause any errors, but it won’t do anything either. If you’re not seeing any output from a custom component, check that the class names (line 6 and 7 above) correspond to the name of the element you wrote, and that the component definition file has been loaded.

Add the new component to the loader to give your page access to it.

Don’t forget to add the new component to your component-loader.ts, then check out your browser to see the new <dta-page> component rendering inside <dta-home>.

The <dta-page> component is also static, but not for long!

Adding the Magic!

So far, all this content is very static and very unexciting. Let’s create a data model with two pages that we can access by key. Each page will, itself, be HTML, giving as all the flexibility we need. Then, we’ll serve up the content from the model by looking up the value of the component’s page= property.

The page component can now respect a parameter or property that it is passed, and draw content from the model data using the ${expression} syntax.

For the sake of convenience, we’ll keep the model data definition here in the component. It really should be in a Typescript module in the modules folder, and we can refactor this code later to move it there. Our model is a JSON object with a key and a TemplateResult value, meaning HTML or other Lit custom component output.

Our model is simply a JSON constant in the same file with keys and HTML values.

Accessing Data in the Model

To access the value of the page attribute that we wrote into the <dta-page> element, we need a @property() with a matching name. Here we set a default value that can serve as a home page.

The @property() decorator defines the attributes that can be passed on our <dta-page> element. In this case, it’s the key we want from the Pages object. Attributes that are not defined as properties are ignored.

Finally, in the render() function, we use the ${expression} syntax to access the value of the variable by key. In Lit, the keyword this always refers to “this component,” so this.page is this component’s page property.

Template literals (inside the backticks) can run any function or grab any data from the model by writing an expression inside ${}. Here, we access the Page object in the model with the passed key from the component’s page property.

Although this is the simplest, worst kind of data model (because it’s married to the component itself), the takeaway here is that whatever data is available in the context of the component is part of your model. The best designs will call a remote function (hopefully in the modules folder), pass the key they want, and get back the page content to display or the value they need to compute that page content. Think about how you can design your app to use a remote data source like Firebase, or the results of a remote API call, and then provide that data (ideally, only the part you need) to your component.

It’s important to note that variables defined with the html decorator are really HTML so you can write the full language there, as in this example where I modified the definition of the welcome page to bold one word and change the color of another.

HTML values in Lit are real HTML and contain the full power of the language, plus all your custom components.
The output with inline styling of HTML in the variable definition.

Adding Routing

We said we wanted our page viewer to react to the URL. If we hard-code the key for the second page, we can verify that the underlying concept works.

Back in <dta-home>, hard-coding for the second page.
Hard-coding a view for the second page.

Since all we need is this key to display the correct page, we can just add it to our URL after a slash, then strip it off. The MDN doc says there’s a document.location.pathname that will give us a list of the “paths” or slash-values in the URL. If we take the first one and pass it as our page= value, we should have what we want!

We assign the first path to a variable, then use it as the page property, trapping for null with a default value.

Here, I added a trap for an empty pageKey to substitute “welcome” on line 25. Even though we have a default value on that property in its component definition, that default only kicks in if the property isn’t provided (if we hadn’t written page= on the element), but not if you provide a null or empty value.

Show Your Moves!

Your routing code should show the second page, based on the URL in the address bar.
A URL without a slash component should display our default page, specified in <dta-home>.

And There You Have It!

Et voilá… You’ve built a single page app from custom components that uses some best practice guidelines and can be extended into any kind of app or UI that you want. My Hexxed game, shown here, has Lit web components that output HTML, fancy programmatic CSS (even sharing CSS across components), and SVG with animations.

My online game, Hexxed, is built on exactly what I’ve shown you here, proving you can make all kinds of apps with Lit and web components, not just “websites.”

As always, thank you for joining me! Perhaps if there’s interest, I’ll look at refactoring and extending this example in a future post.

Be well!

— D

--

--

I'm a 35 year veteran of the software industry and the founder of DTA Games. Visit us at https://dtagames.io.