bp

8 Days of Web Components Tips

In honour of Hannukah this year, I undertook to write 8 web components tips, one for each night of the festival. Tonight is the 8th and final night of the festival. The mystics said that this night combines and contains aspects of each of the seven previous nights, so I'd like to share a compilation of those tips with the dev community.

Wishing you and yours a fully Lit Hannukah!

1st night: Adding Controllers via TypeScript Decorators 🕯

Did you know you can add reactive controllers to an element via a class or field decorator? You don't even need to assign it to an instance property!

/**
 * Adds a given class to a ReactiveElement when it upgrades
 */
export function classy(classString: string): ClassDecorator {
  return function(klass) {
    if (!isReactiveElementClass(klass))
      throw new Error(`@classy may only decorate ReactiveElements.`);

    klass.addInitializer(instance => {
      // Define and add an ad-hoc controller!
      // Look, mah! No instance property!
      instance.addController({
        hostConnected() {
          instance.classList.add(classString);
        },
      });
    });
  };
}

@customElement('pirsumei-nissa') @classy('al-hanissim')
export class PirsumeiNissa extends LitElement {}

2nd night: Adding Controllers Inside Other Controllers 🕯🕯

Like a delicious sufganya (traditional holiday donut) with many fillings, a Lit component can have multiple reactive controllers, and controllers can even add other controllers

export class MutationController<E extends ReactiveElement> implements ReactiveController {
  private logger: Logger;
  
  mo = new MutationObserver(this.onMutation);

  constructor(public host: E, public options?: Options<E>) {
    // Add another controller
    this.logger = new Logger(this.host);
    host.addController(this);
  }

  onMutation(records: MutationRecord[]) {
    this.logger.log('Mutation', records);
    this.options?.onMutation?.(records)
  }

  hostConnected() {
    this.mo.observe(this.host, this.options?.init ?? { attributes: true, childList: true });
  }

  hostDisconnected() {
    this.mo.disconnect();
  }
}

3rd night: Web Component Context API 🕯🕯🕯

Did you know web components can have context? The protocol is based on composed events. Define providers & consumers, & share data across the DOM.

https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md

4th night: Using SASS, PostCSS, etc. 🕯🕯🕯🕯

Building #webcomponents with #SASS? (You probably don't need it but if you can't resist…) you can develop using a buildless workflow with Web Dev Server and esbuild-plugin-lit-css

Want to use #PostCSS instead for sweet-sweet future CSS syntax? No problem

5th night: Stacking Slots 🕯🕯🕯🕯🕯

Who doesn't like a piping hot stack of latkes?

Stack slots to toggle component states. Adding content into the outer slot automatically 'disables' the inner slot

State management in HTML! 🤯

Check out @Westbrook's blog on the topic:

6th night: Better TypeScript Imports 🕯🕯🕯🕯🕯🕯

In #TypeScript 4.5, if you set preserveValueImports, you can import the class definitions of your element dependencies without worrying that TS will elide the side-effecting value.

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

@customElement('lit-candle')
export class LitCandle extends LitElement {
  @property({ type: Boolean }) lit = false;
  
  render() {
    return this.lit ? '🕯' : ' ';
  }
}
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { LitCandle } from './lit-candle.js';

@customElement('lit-menorah')
export class LitMenorah extends LitElement {
  @property({ type: Number }) night = 6;
  
  // Although the value of `LitCandle` isn't used, only the type
  // with `preserveValueImports`, TS 4.5 won't strip the import
  // So you can be sure that `<lit-candle>` will upgrade
  @query('lit-candle') candles: NodeListOf<LitCandle>;
  
  render() {
    return Array.from({ length: 8 }, (_, i) => html`
      <lit-candle ?lit="${(i + 1) <= this.night}"></lit-candle>
    `);
  }
}

live demo

7th night: GraphQL Web Components 🕯🕯🕯🕯🕯🕯🕯

Looking to add #GraphQL to your frontend? Give Apollo Elements a try. Use Apollo reactive controllers with lit+others, or try a 'functional' library like atomic

import { ApolloQueryController } from '@apollo-elements/core';
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { HelloQuery } from './Hello.query.graphql';

@customElement('hello-query')
export class HelloQueryElement extends LitElement {
  query = new ApolloQueryController(this, HelloQuery);

  render() {
    return html`
      <article class=${classMap({ skeleton: this.query.loading })}>
        <p id="error" ?hidden=${!this.query.error}>${this.query.error?.message}</p>
        <p>
          ${this.query.data?.greeting ?? 'Hello'},
          ${this.query.data?.name ?? 'Friend'}
        </p>
      </article>
    `;
  }
}

8th night: Component Interop 🕯🕯🕯🕯🕯🕯🕯🕯

You don't need to use only #lit components in your #lit app

Mix old-school #Polymer 3 components with #vue js web components. Put #stencil js Microsoft's #FAST UI on the same page

It's your party!

<!DOCTYPE html>
<head>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.61/dist/themes/light.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css"/>
  <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.61/dist/shoelace.js"></script>
  <script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
  <script type="module" src="https://unpkg.com/@microsoft/fast-components"></script>
  <script type="module" src="https://unpkg.com/@patternfly/pfe-datetime@1.12.2/dist/pfe-datetime.js?module"></script>
  <script type="module" src="https://unpkg.com/@material/mwc-button?module"></script>
</head>
<body>
  <sl-card>
    <pfe-datetime slot="header" type="relative" datetime="Mon Jan 2 15:04:05 EST 2010"></pfe-datetime>
    <ion-img slot="image" src="https://placekitten.com/300/200"></ion-img>
    <fast-progress-ring min="0" max="100" value="75"></fast-progress-ring>
    <mwc-button slot="footer">More Info</mwc-button>
  </sl-card>
</body>