DEV Community

Cover image for ๐Ÿ•Ž 8 Days of Web Components Tips

๐Ÿ•Ž 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 {}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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 ? '๐Ÿ•ฏ' : ' ';
  }
}
Enter fullscreen mode Exit fullscreen mode
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>
    `);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    `;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Discussion (2)

Collapse
binyamin profile image
Binyamin Green

This is just what I needed. Also, happy Chanukkah!

Collapse
bennypowers profile image
Benny Powers ๐Ÿ‡ฎ๐Ÿ‡ฑ๐Ÿ‡จ๐Ÿ‡ฆ Author

ื’ื ืœืš ืื—ื™ ื—ื•ืจืฃ ื—ื