Back to the Front-end: Exploring the Future of the Umbraco UI (Part 8 - Lit)

5 min read

When HQ announced that a new backoffice UI was on the cards, by far the biggest question on everyones mind was “what framework will it be built in?”.

Well we can finally answer that question as the framework of choice is Lit. Now in comparison to things like React or Vue, Lit might seem rather lacking, but I think they key thing HQ have learnt from the Angular UI is to not put all your eggs in one basket, and to stay as lightweight as possible, and so Lit is a perfect fit for that methodology.

About Lit

Lit is a simple library to help you build fast and lightweight web components.

At Lit’s core is a web component base class that provides reactive state, scoped styles, and a declarative template system.

Fundamentally, it hides away all the boilerplate code that comes with Web Component creation, and provides some key abilities that most modern frameworks today are based around (ie, reactivity and templating) saving you a ton of code.

For everything else you can either leverage vanilla JS functionality (which is suprisingly capable these days) or import and use other libraries for those specific functionalities.

Where Angular attempted to take care of everything, with Lit, things are much more modular.

Getting Started

Defining

As a basic example we’ll convert our Web Component that we originally defined in our Web Component post to be a Lit based Web Component. The initial structure would look something like this:

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('message-box')
export class MessageBox extends LitElement {
  ...
}

declare global {
  interface HTMLElementTagNameMap {
    "message-box": MessageBox;
  }
}

The key elements here are that we first import the classes and functions we are going to need from the Lit ES Modules and we then define a class that extends the base LitElement. LitElement itself extends a ReactiveElement base class which in turn extends the standard HTMLElement. It’s these base classes that give us the useful base implementations provided by Lit.

We can also see that our class is decorated with the @customElement attribute which is a Lit decorator which automates registering our component with the CustomElementRegistry with the provided name.

Lastly we see a secondary declaration for the interface HTMLElementTagNameMap. What this does is signals to TypeScript to tell it whenever it sees a call for document.createElement('message-box') that it should automatically type the returned instance as our MessageBox type, thus maintaining static type checking when using it’s API.

Reactivity

Lit provides two main types of reactive state, public reactive properties defined using the @property decorator, and internal reactive properties defined using the @state decorator.

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('message-box')
export class MessageBox extends LitElement {

  @property()
  kind = 'info';

  @state()
  protected _active = false;

  ...
}

Rendering

Rendering a Lit web component is handled via a render function defined on our class. Initally this will get called when the web component is added to the page DOM, but will also get re-called whenever any of the reactive properties used inside the render function are updated.

This ensures our UI stays up to date as our data changes without us needing to expressly update it.

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('message-box')
export class MessageBox extends LitElement {

  ...

  render() {
    return html`<div id="msg-box" class="msg-box--${this.kind}">
      <h3><slot name="title">Default Message Title</slot></h3>
      <p><slot name="body">Deafult Message Body</slot></p>
    </div>`;
  }
}

We write our components template as HTML inside a JavaScript tagged template literal using Lit’s html tag function.

Our template can make use of our Web Component properties as well as contain JavaScript expressions to dynamically render details based on these properties.

Properties defined using the @property decorator will automatically be exposed as Web Component properties/attributes, where as properties defined using the @state decorator will not be exposed publically.

As these are both reactive, any changes to these properties will automatically notify any code that is monitoring them for changes or trigger a re-render if used within the render function.

There is a lot you can do in a template so be sure to checkout the templating docs for more details.

Styling

In our vanilla Web Component example we embedded the CSS for our component in it’s markup template. In Lit we can expose our styles via a static style property on our class using the tagged template literal css function:

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('message-box')
export class MessageBox extends LitElement {

  static styles = css`#msg-box {
        padding: 10px;
        border-style: solid;
        border-width: 2px;
        border-radius: 5px;
        font-family: sans-serif;
      }
      #msg-box > h3 {
        margin: 0 0 10px;
        padding: 0;
        font-size: 16px;
      }
      #msg-box > p {
        margin: 0;
        padding: 0;
        font-size: 12px;
      }
      .msg-box--success {
        border-color: #059669;
        background-color: #a7f3d0;
      }
      .msg-box--warn {
        border-color: #eab308;
        background-color: #fef08a;
      }
      .msg-box--info {
        border-color: #2563eb;
      }`;

  ...
}

Similar to html templates, we can easily embed variables and expressions in our styles to generated computed styles.

Events

Listening to Events

To listen for events you can either use the standard addEventListener API, or Lit also includes a declarative way to add event listeners within your templates.

import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('message-box')
export class MessageBox extends LitElement {

  ...

  render() {
    return html`<div id="msg-box" class="msg-box--${this.kind}" @click="${this._handleClick}">
      <h3><slot name="title">Default Message Title</slot></h3>
      <p><slot name="body">Deafult Message Body</slot></p>
    </div>`;
  }

  private _handleClick(e: Event) {
    console.log("Clicked");
  }
}

Dispatching Events

Components can dispatch their own events using the standard dispatchEvent API.

this.dispatchEvent(new CustomEvent('value-change', { bubbles: true, composed: true }));

What about the Shadow DOM?

You may have noticed that so far we haven’t mentioned or done anything with the shadow DOM. Well that’s because by default all Lit components are set up with a shadow DOM to which the render function is applied and the styles attached.

If you need to access an element from the shadow DOM, components expose a renderRoot on which you can call standard query selectors.

Our Complete Example Web Component

Checkout the CodeSandbox below for a fully working example of our Lit based Web Component built during this post.

{% embed https://codesandbox.io/embed/epic-rubin-77pzgb?fontsize=14&hidenavigation=1&theme=dark %}

Conclusion

What I’ve blogged in this article is a really high level view and there is soooooo much more to Lit, but I think this should give you a simple intro to the major bits that you’ll see used within the backoffice UI codebase.

Additional Resources