Skip to content

LitElement properties: @property vs @internalProperty

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

LitElement is an excellent alternative to build lightweight web applications since it’s based on the Web Components standard, and with the help of TypeScript, we can see more possibilities to build web components faster with a good developer experience.

In this article, I will explain the practical use of @property and @internalProperty decorators using a TypeScript implementation.

The Problem

Let's suppose you're building a small application that needs to display a list of Users. When you select any of them, other details should be displayed. In this case, it's the Address information.

LitElement properties: @property vs @internalproperty

Next, let's define the data model and a dataset before implementing the web components using LitElement.

Data Modeling and Dataset

The Data Model

Let's rely on TypeScript interfaces and static typing to have the model ready.

// model.ts
export interface User {
  id: number;
  name: string;
}

export interface Address {
  country: string;
  state: string;
  city: string;
  street: string;
  zipCode: number;
}

As you may note, the Address interface will display a set of attributes that represents a complete address.

The Source of Data

To make the testing easier, let's create a dataset of entities that matches the previous model.

// data.ts
import { Address } from "./model";
import { User } from "./model";

export const users: User[] = [
  {
    id: 0,
    name: "Donald Mayfield"
  },
  {
    id: 1,
    name: "Jill J. Fritz"
  },
  {
    id: 2,
    name: "Terry Buttram"
  }
];

export const address: Address[] = [
  {
    street: "2180 BELLFLOWER",
    country: "USA",
    state: "AL",
    city: "Madison",
    zipCode: 35064
  },
  {
    street: "845 ODOM ROAD, SUITE 200",
    country: "USA",
    state: "CA",
    city: "Los Angeles",
    zipCode: 90720
  },
  {
    street: "9025 QUEENS BLVD",
    country: "USA",
    state: "NY",
    city: "Queens",
    zipCode: 11355
  }
];

The relationship between the users and address is simple: The User id matches with the position of the address array.

Using LitElement Properties

LitElement provides different ways to manage the properties. As the documentation says:

LitElement manages your declared properties and their corresponding attributes.

This can be done using either a static properties field or using decorators. Let's take the TypeScript-way using the power of decorators.

We'll understand the "properties management" better in the practice through the next sections.

Creating the MainViewer Container

Let's create a class to contain our first component. In this case, we'll define a container component that will be in charge of displaying both the list of users and their details.

import {
  LitElement,
  html,
  customElement,
  css,
  internalProperty
} from "lit-element";
import { users } from "./data";
import { User } from "./model";

@customElement("main-viewer")
class MainViewer extends LitElement {
  static styles = css`
    :host {
      display: block;
    }
  `;

  users: User[] = [];
  userId?: number;

  constructor() {
    super();
  }

  render() {
    return html`
      <div>
        <h1>User Address Viewer</h1>
        <span>Select a User to see the Address:</span>
        <ul>
          ${this.users.map(
            user => html`
              <li>
                <a href="#" @click="${() => this.viewAddress(user.id)}"
                  >${user.name}</a
                >
              </li>
            `
          )}
        </ul>
      </div>
    `;
  }

  async connectedCallback() {
    super.connectedCallback();
    this.users = await this.getUsers();
  }

  private getUsers() {
    // in the real-world you'll get the data from a service, file, etc.
    return new Promise<User[]>((resolve, reject) => resolve(users));
  }

  private viewAddress(id: number) {
    this.userId = id;
  }
}

Let's explain what is happening in this component so far:

  • The static styles attribute defines the styles for the component using a tagged template literal (css).
  • It defines two class properties: users to "store" the set of Users to be displayed in the main list. Also, the userId will contain a reference of the selected User identifier (this value will be changed every time you select a different user).
  • The render method returns the HTML content through a template literal (html). This function will be called any time a component property changes.
  • The connectedCallback function makes a call to getUsers in order to get the initial data when the component is added to the document's DOM.
  • The getUsers function should perform an asynchronous call (in a real-world scenario) to retrieve the data.
  • The viewAddress function receives an id of the object selected once the user performs a click action over the link. Then, the class property userId will be updated.

The previous code is perfectly fine. However, it won't display any data yet! We'll only see the title and a blank section on the page and no list of users rendered.

Using the @internalProperty Decorator

In order to fix this rendering issue, we should make sure to trigger an update cycle for the web component.

Think for a while about when the update cycle should be performed: every time you change the list of users! However, this cannot be done every time you update any variable or attribute from your class. Instead, you should mention which attributes are "keys" to perform the rendering operation or keep your component updated. We'll call LitElement properties to these key attributes.

Once you have identified them, we'll need to declare these attributes as properties:

// main-viewer.ts

@customElement("main-viewer")
class MainViewer extends LitElement {
  // ...

  @internalProperty() users: User[] = [];
  @internalProperty() userId?: number;

  constructor() {
    super();
  }

  // ...
}

Why use @internalProperty in this case?

  • For this MainViewer component, we don't need to reference either users or userId from outside the component.
  • From the previous point, we can consider users and userId attributes as private or protected.

In other words, LitElement observes and "reacts" to these property changes so the template gets rendered/updated automatically.

Using the @property Decorator

Creating the AddressViewer Component

Before creating the child component, let's think about how we are going to use it from the container. In terms of web components, let's suppose we'll need the following element and attribute:

<address-viewer .userId=${userId}></address-viewer>

That means the new address-viewer component will need to "receive" the userId value to be able to retrieve the respective Address. The .userId=${userId} notation applies a one-way data binding to a property, meaning the AddressViewer component will have the userId as a public property.

@property vs @internalProperty decorators

Let's take the previous consideration into account and create a new file address-viewer.ts:

import {
  LitElement,
  html,
  property,
  customElement,
  css,
  internalProperty
} from "lit-element";
import { address } from "./data";
import { Address } from "./model";

@customElement("address-viewer")
class AddressViewer extends LitElement {
  static styles = css`
    :host {
      display: block;
    }

    table {
      border-collapse: collapse;
      width: 100%;
    }

    td,
    th {
      border: 1px solid gray;
      text-align: left;
      padding: 5px;
    }
  `;

  @property({ type: Number }) userId: number;
  @internalProperty() userAddress?: Address;

  constructor() {
    super();
  }

  render() {
    if (this.userAddress === undefined) {
      return html``;
    }

    return html`
      <table>
        <tr>
          <th>Country</th>
          <th>State</th>
          <th>City</th>
          <th>Street</th>
          <th>Zip Code</th>
        </tr>
        <tr>
          <td>${this.userAddress.country}</td>
          <td>${this.userAddress.state}</td>
          <td>${this.userAddress.city}</td>
          <td>${this.userAddress.street}</td>
          <td>${this.userAddress.zipCode}</td>
        </tr>
      </table>
    `;
  }

  update(changedProperties: Map<string, unknown>) {
    if (changedProperties.has("userId")) {
      const oldValue = changedProperties.get("userId") as number;
      console.log("userId updated, newVal", this.userId, "oldVal", oldValue);
      this.loadAddress(this.userId);
    }
    super.update(changedProperties);
  }

  private async loadAddress(id: number) {
    this.userAddress = await this.getAddress(id);
  }

  private getAddress(id: number) {
    return new Promise<Address>((resolve, reject) => resolve(address[id]));
  }
}

Again, let's take a closer look at the previous source code:

  • The userId attribute is defined as a public property for the component using @property decorator.
  • The userAddress property doesn't need to be public property for the component. Instead, it's defined using @internalProperty decorator to trigger the update cycle once it gets changed.
    • Initially, It's undefined
  • The render method returns the HTML content to be rendered every time a property is changed.
    • Note the function will return a meaningful template only once the userAddress contains the required object.
  • The update function reflects property values and calls the render function.
    • It receives a Map with the properties that have been changed.
    • It verifies if the userId property has been changed, and then performs a call to loadAddress.
    • Whenever you override this method, you'll need to call super.update() to render your template.
  • The loadAddress function takes the new value for userId and calls the utility function to retrieve the Address object.

The brand-new AddressViewer component is ready, and we'll need to use it as a child component in the main container. Let's update the render function as follows:

// main-viewer.ts

// Let's import the <address-viewer> definition
import "./address-viewer";

@customElement("main-viewer")
class MainViewer extends LitElement {
  //...

  render() {
    return html`
      <div>
        <h1>User Address Viewer</h1>
        <span>Select a User to see the Address:</span>
        <ul>
          ${this.users.map(
            user => html`
              <li>
                <a href="#" @click="${() => this.viewAddress(user.id)}"
                  >${user.name}</a
                >
              </li>
            `
          )}
        </ul>
        <address-viewer .userId=${this.userId}></address-viewer>
      </div>
    `;
  }

 // ... 
}

Again, pay attention to the one-way data binding .userId=${this.userId} that will trigger a rendering operation over the <address-viewer> component whenever the userId gets changed. This is magic, right?

Live Demo

Want to play around with this code? Open the Stackblitz editor:

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co