Skip to content

Web Components Integration using LitElement and TypeScript

Web Components Integration using LitElement and TypeScript

In my previous posts, I explained how to create a project based on LitElement and TypeScript from scratch. Also, I added the routing management and explained how to have more control over the navigation lifecycle.

Let's deep dive over the Web Components creation to understand how to reuse them, respond to any property change, and dispatch custom events.

Writing a Web Component

Let's define a base template to display an overview of every blog post in our web application.

<div class="blog-card">
  <div class="blog-description">
    <h1>Title</h1>
    <h2>Author</h2>
    <p>
      Brief Description
    </p>
    <p class="blog-footer">
      <a class="blog-link">Read More</a>
    </p>
  </div>
</div>

With this template, we should expect to draw a simple card to display the post content. It will define the internal DOM for our new component.

Let's create the Web Component using LitElement and the @customElement decorator:

// blog-card.ts

import { LitElement, html, customElement, css } from 'lit-element';
import { Post } from './post';

@customElement('blog-card')
export class BlogCard extends LitElement {
  static styles = css`
  .blog-card {
    margin: 20px;
    display: flex;
    flex-direction: column;
    margin-bottom: 15px;
    background: white;
    border-radius: 5px;
    overflow: hidden;
    border-radius: 10px;
  }
  .blog-description {
    padding: 20px;
    background: white;
  }
  .blog-footer {
    text-align: right;
  }
  .blog-link {
    color: #008cba;
  }
  h1 {
    margin: 0;
    font-size: 1.5rem;
  }
  h2 {
    font-size: 1rem;
    font-weight: 300;
    color: #5e5e5e;
    margin-top: 5px;
  }
  `;

  render() {
    return html`
      <div class="blog-card">
        <div class="blog-description">
          <h1>Title</h1>
          <h2>Author</h2>
          <p>
            Brief Description
          </p>
          <p class="blog-footer">
            <a class="blog-link">Read More</a>
          </p>
        </div>
      </div>
    `;
  }
}

This component is ready to be rendered. However, it cannot be reused since the content will remain always the same. We need a way to configure this component so that it can display different content for each instance of it(That should be ideal, right?).

Adding Properties

The TypeScript-way to declare the element's properties is as follows:

// blog-card.ts

@customElement('blog-card')
export class BlogCard extends LitElement {

  @property({ type: String }) postTitle?: string;
  @property({ type: String }) author?: string;
  @property({ type: String }) description?: string;
}

That looks enough to configure our new component and being able to display an overview of a blog post. However, what will happen if we decide to add other attributes? The properties list will be growing in number and maybe it's not the best way to handle this scenario.

The other option is define a Post model through a TypeScript Interface:

// post.ts
export interface Post {
  id: number;
  title: string;
  author: string;
  description: string;
}

Then, let's define a single property that expects a Post object:

// blog-card.ts

@customElement('blog-card')
export class BlogCard extends LitElement {

  @property({ type: Object }) post?: Post;
}

Binding Properties

It's time to improve the render function and create property bindings into the template.

// blog-card.ts

  render() {
    return html`
      <div class="blog-card">
        <div class="blog-description">
          <h1>${this.post?.title}</h1>
          <h2>${this.post?.author}</h2>
          <p>
            ${this.post?.description}
          </p>
          <p class="blog-footer">
            <a class="blog-link">Read More</a>
          </p>
        </div>
      </div>
    `;
  }

The @property declaration(defined before) will render the template every time the given property changes.

Adding Events

There are different ways to add event listeners for our Web Components. In this case, we can use a declarative event listener using @event notation:

  render() {
    return html`
      <div class="blog-card">
        <div class="blog-description">
          <h1>${this.post?.title}</h1>
          <h2>${this.post?.author}</h2>
          <p>
            ${this.post?.description}
          </p>
          <p class="blog-footer">
            <a class="blog-link" @click="${this.handleClick}">Read More</a>
          </p>
        </div>
      </div>
    `;
  }

  private handleClick() {
    this.dispatchEvent(
      new CustomEvent('readMore', { detail: this.post })
    );
  }

The event listener @click="${this.handleClick}" will be added once the template gets rendered in the browser. The click action will be handled by the handleClick function.

This function will fire an event from our Lit-based web component. The CustomEvent allows an object Post to be propagated along with it.

Parent-Child Component Communication

The blog-card component is ready to be integrated into the application.

parent-child

Let's think in the Blog Posts page as the parent and the Blog Card as the child component:

// blog-posts.ts

import { POSTS } from './data';
import { Post } from './post';

@customElement('lit-blog-posts')
export class BlogPosts extends LitElement {

  @property({ type: Array }) blogPosts?: Post[];

  constructor() {
    super();
  }

  render() {
    return html`
      <h2>Blog Posts</h2>
      ${this.blogPosts?.map(
        post => html`<blog-card .post="${post}"></blog-card>`
      )}
    `;
  }

  firstUpdated() {
    this.blogPosts = POSTS;
  }
}

The parent component defines a property to store a set of blog posts using @property({ type: Array }) blogPosts?: Post[];.

If you have the Web component template and want to bind properties, take into account the following rules:

  • Text content: <p>${...}</p>
  • Attribute: <p id="${...}"></p>
  • Boolean attribute: ?disabled="${...}"
  • Property: .value="${...}"
  • Event handler: @event="${...}"

The firstUpdated function will be called after the element's DOM has been updated the first time. In a real-world scenario, the app would need to perform an HTTP call to get the data.

For this example, let's load the data from data.ts file:

// data.ts

import { Post } from './post';

export const POSTS: Post[] = [
  {
    id: 0,
    title: 'Web Components Introduction',
    author: 'Luis Aviles',
    description:
      'Lorem ipsum dolor sit amet, consectetur adipiscing elit...',
  },
  {
    id: 1,
    title: 'LitElement with TypeScript',
    author: 'Luis Aviles',
    description:
      'Sed felis nisi, consectetur sed ipsum dignissim, semper porta risus...',
  },
  {
    id: 2,
    title: 'Navigation and Routing with Web Components',
    author: 'Luis Aviles',
    description:
      'Ut ipsum arcu, sodales aliquet nisi iaculis, faucibus varius mauris...',
  },
];

Listen to Child Events

The parent component is ready to display the blog posts. However, it would be great to listen every time the "Read More" link has been clicked and take the control from the Blog Posts page:

// blog-posts.ts

  firstUpdated() {
    this.blogPosts = POSTS;
    this.addEventListener('readMore', event => {
      const post = (event as CustomEvent).detail as Post; //event.detail has a the Post object
      Router.go(`/blog/posts/${post.id}`); // Get the Post id and redirect
    });
  }

The event listener will be executed after the first paint. This way of adding them can be useful when you're adding several listeners.

This should be the final result:

blog-posts-screenshot

Source Code Project

Find the complete project in this GitHub repository: https://github.com/luixaviles/litelement-website. Do not forget to give it a star ⭐️ and play around with the code.

You can follow me on Twitter and GitHub to see more about my work.