Web Components in Drupal

(and other CMSs)

Adam Juran & Christopher Bloom

Source: Pixabay

Christopher Bloom

Adam Juran

Principal Developer, Knapsack.cloud

Design systems advocate

Former maintainer of the Particle project

Swears too much in professional settings

Frontend Architect, 1xINTERNET.de

Design systems advocate

Standard-bearer of best practices

Trained opera singer, black belt in Kung Fu

Foundations

  • JavaScript ES6 Class syntax
  • NPM
  • HTML5
  • CMS theme layer
  • Coming soon: ES6 modules!

What?

<ds-card mode="light">
  <ds-image slot="card-header" image="/desk.jpg" lazyload></ds-image>
  <ds-title slot="card-title" variant="large">
    Boost your conversion rate!
  </ds-title>
  <div slot="card-button">
    <ds-cta link="/prices" text="Click here!"></ds-cta>  
    <ds-avatar url="dudeface.jpeg"></ds-avatar>
  </div>
  Card body text goes here!
</ds-card>

"Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps."

—https://www.webcomponents.org/

Why?

Because that means you can create frontend components that are usable within CMSs and other applications.

<ds-card></ds-card>

Really?

We've been searching for the Holy Grail of design systems for over a decade.

It exists.

 

Web Components have been a web standard since 2011, with widespread browser support since 2018.

Anatomy of a Web Component

// index.js
class MyHello extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    this.innerHTML = 
      `<h1>Hello ${this.getattribute('name')}</h1>`
  }
}

customElements.define('my-hello', MyHello);
<!-- index.html -->
<my-hello name="world"></my-hello>
  1. Extend HTMLElement
  2. Provide markup in connectedCallback()
  3. Register class with an element name
  4. Profit!

But wait, there's more: Shadow DOM

// index.js
class MyHello extends HTMLElement {
  connectedCallback() {
    const shadow = 
      this.attachShadow({ mode: "open" })
    shadow.innerHTML = 
      `<h1>Hello ${this.getAttribute("name")}</h1>`
  }
}

customElements.define("my-hello", MyHello)
<!-- index.html -->
<my-hello name="world"></my-hello>
  • Fully encapsulated, separate DOM tree per element!
  • Protected from global styles
  • SCOPED styles (goodbye BEM)
  • Distribute your components with confidence

Styling

  

  • :host
  • SCOPED styles
  • CSS variables
  • But CSS-in-JS?
  • Global styles? Overrides?
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators';

@customElement('my-hello');
class MyHello extends LitElement {
  static get styles() {
    return css`
      :host {
        background: var(--primary-color, rebeccapurple);
        display: inline-block;
        color: white;
        padding: 10px;
        border-radius: 5px;
        font-family: sans-serif;
      }
    `;
  }
  render() {
    return html`I'm styled!`;
  }
}

LIT 2.0!

Override styles?

  CSS variables provide flexibility.

// index.js
class MyHello extends LitElement {
  static get styles() {
    return css`
      :host {
        background: var(--primary-color, rebeccapurple);
      }
    `;
  }
}
<!-- index.html -->
<div class="css-var-example">
  <my-hello name="world"></my-hello>
</div>

Default

/* styles.css */
:root {
  --primary-color: red;
}
/* styles.css */
:root {
  --primary-color: red;
}
.css-var-example {
  --primary-color: blue;
}

:root variable overrides default

scoped variable overrides :root

Great, how do I get this in Drupal?

<!-- block--my-hello.html.twig -->

<div{{ attributes }}>
  {{ title_prefix }}{{ title_suffix }}

  {% block content %}
    <my-hello name="{{ content.field_headline|field_raw('value') }}"></my-hello>
  {% endblock %}
</div>

That's it. Treat custom elements like Twig partials.

Recommendation 1

Publish your design system (web components) as an NPM package.

GitHub NPM package repository

GitHub git repository

Recommendation 2

Consume your design system within your Drupal/React/etc app.

{
  "name": "drupal-project",
  "version": "0.0.1",
  "description": "Frontend functionality for this Drupal 9",
  "author": "Christopher Bloom",
  "license": "ISC",
  "dependencies": {
    "@illepic/lit": "^1.0.0"
  },
  "scripts": {
    "postinstall": "cp -R node_modules/@illepic/lit themes/awesome-theme/dist"
  }
}
[DRUPAL_PROJECT]/package.json
design-system:
  js:
    dist/example-card.js:
      attributes:
        type: module
  css:
    base:
      dist/global.css: {}
[DRUPAL_PROJECT]/themes/awesome-theme/awesome_theme.libraries.yml

Recommendation 3

Automatically release new versions of your design system packages.

Why

Benefits from design systems tooling (i.e. knapsack.cloud!)

Why

Release cycle out of bound from application

 

Much easier to iterate on the design system with a mechanism to lock Drupal to a specific version!

 

Simple "rollback" for frontend regressions.

{
  "name": "drupal-project",
  "version": "0.0.1",
  "dependencies": {
    "@illepic/lit": "1.12.0"
  }
}
{
  "name": "drupal-project",
  "version": "0.0.1",
  "dependencies": {
    "@illepic/lit": "1.11.0"
  }
}

"Oops, go back!"

Why

  • Vastly improved reusability
  • Cross-framework support
  • What about React?
// github.com/lit/lit/tree/main/packages/labs/react

import * as React from 'react';
import {createComponent} from '@lit-labs/react';
import {MyElement} from './my-element.js';

export const MyElementComponent = createComponent(
  React,
  'my-element',
  MyElement,
  {
    onactivate: 'activate',
    onchange: 'change',
  }
);

Why

... not Twig Pattern Lab 

 

  • Bound to PHP
  • Frontend reusability
  • But you told us to use Pattern Lab 6 years ago! (literally here, at Decoupled Dev Days!)
  • “The Only Constant in Life Is Change.” -Heraclitus.

Why

Why

Encapsulation.

Distribute your components with confidence.

Drupal

Does this mean we no longer need Twig?

 

Twig is essential as the theming layer to connect Drupal data to frontend design components. In fact, we highly recommend these twig projects:

 

When?

  • Will your design system ever be used outside of Drupal?
  • How about within JavaScript widgets inside Drupal?
  • Does your frontend team have/want modern JS skills?
  • Can you ditch old browsers?

IE11

Yes AND.

https://lit.dev/docs/releases/upgrade/#load-polyfill-support-when-using-web-components-polyfills

Serverside rendering

Performance

Bundling

FoUC
SEO

 

TTI

SSR

Recommendation 4

  1. LitElement 2.0
  2. StencilJS

Frameworks

  1. https://shoelace.style/
  2. https://lion-web.netlify.app/

Resources

Questions + Code Demo

https://github.com/illepic/lit

Thank you.

Made with Slides.com