Web Standards Meet User-Land: Using CSS-in-JS zum Stylen von Custom Elements

Avatar of Ollie Williams
Ollie Williams am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Die Popularität von CSS-in-JS stammt hauptsächlich aus der React-Community, und tatsächlich sind viele CSS-in-JS-Bibliotheken React-spezifisch. Jedoch ist Emotion, die beliebteste Bibliothek in Bezug auf npm-Downloads, framework-agnostisch.

Die Verwendung von Shadow DOM ist üblich bei der Erstellung von Custom Elements, aber es gibt keine Verpflichtung dazu. Nicht alle Anwendungsfälle erfordern dieses Maß an Kapselung. Während es auch möglich ist, Custom Elements mit CSS in einem normalen Stylesheet zu stylen, werden wir uns hier mit Emotion beschäftigen.

Wir beginnen mit einer Installation

npm i emotion

Emotion bietet die css-Funktion

import {css} from 'emotion';

css ist ein getaggtes Template-Literal. Es akzeptiert Standard-CSS-Syntax, fügt aber Unterstützung für Sass-ähnliche Verschachtelung hinzu.

const buttonStyles = css`
  color: white;
  font-size: 16px;
  background-color: blue;

  &:hover {
    background-color: purple;
  }
`

Sobald einige Stile definiert wurden, müssen sie angewendet werden. Die Arbeit mit Custom Elements kann etwas umständlich sein. Bibliotheken – wie Stencil und LitElement – kompilieren zu Web Components, bieten aber eine freundlichere API als das, was wir direkt aus der Box erhalten würden.

Wir werden also Stile mit Emotion definieren und sowohl Stencil als auch LitElement nutzen, um die Arbeit mit Web Components etwas einfacher zu gestalten.

Anwenden von Stilen für Stencil

Stencil nutzt die bahnbrechende JavaScript-Decorator-Funktion. Ein @Component-Decorator wird verwendet, um Metadaten über die Komponente bereitzustellen. Standardmäßig verwendet Stencil keinen Shadow DOM, aber ich setze ihn gerne explizit, indem ich shadow: false innerhalb des @Component-Decorators setze.

@Component({
  tag: 'fancy-button',
  shadow: false
})

Stencil verwendet JSX, daher werden die Stile mit einer Klammersyntax ({}) angewendet

export class Button {
  render() {
    return <div><button class={buttonStyles}><slot/></button></div>
  }
}

So würde eine einfache Beispielkomponente in Stencil aussehen

import { css, injectGlobal } from 'emotion';
import {Component} from '@stencil/core';

const buttonStyles = css`
  color: white;
  font-size: 16px;
  background-color: blue;
  &:hover {
    background-color: purple;
  }
`
@Component({
  tag: 'fancy-button',
  shadow: false
})
export class Button {
  render() {
    return <div><button class={buttonStyles}><slot/></button></div>
  }
}

Anwenden von Stilen für LitElement

LitElement hingegenverwendet standardmäßig Shadow DOM. Bei der Erstellung eines Custom Elements mit LitElement wird die LitElement-Klasse erweitert. LitElement hat eine createRenderRoot()-Methode, die einen Shadow DOM erstellt und öffnet.

createRenderRoot()  {
  return this.attachShadow({mode: 'open'});
}

Sie möchten den Shadow DOM nicht nutzen? Dazu müssen Sie diese Methode neu implementieren innerhalb der Komponente.

class Button extends LitElement {
  createRenderRoot() {
      return this;
  }
}

Innerhalb der Render-Funktion können wir die definierten Stile mit einem Template-Literal referenzieren

render() {
  return html`<button class=${buttonStyles}>hello world!</button>`
}

Es ist erwähnenswert, dass bei der Verwendung von LitElement wir ein slot-Element nur verwenden können, wenn wir auch Shadow DOM verwenden (Stencil hat dieses Problem nicht).

Zusammengenommen ergeben sich

import {LitElement, html} from 'lit-element';
import {css, injectGlobal} from 'emotion';
const buttonStyles = css`
  color: white;
  font-size: 16px;
  background-color: blue;
  &:hover {
    background-color: purple;
  }
`

class Button extends LitElement {
  createRenderRoot() {
    return this;
  }
  render() {
    return html`<button class=${buttonStyles}>hello world!</button>`
  }
}

customElements.define('fancy-button', Button);

Emotion verstehen

Wir müssen uns keine Gedanken über die Benennung unseres Buttons machen – Emotion generiert einen zufälligen Klassennamen.

Wir könnten CSS-Verschachtelung verwenden und eine Klasse nur an ein Elternelement anhängen. Alternativ können wir Stile als separate getaggte Template-Literale definieren

const styles = {
  heading: css`
    font-size: 24px;
  `,
  para: css`
    color: pink;
  `
} 

Und sie dann separat auf verschiedene HTML-Elemente anwenden (dieses Beispiel verwendet JSX)

render() {
  return <div>
    <h2 class={styles.heading}>lorem ipsum</h2>
    <p class={styles.para}>lorem ipsum</p>
  </div>
}

Styling des Containers

Bisher haben wir den inneren Inhalt des Custom Elements gestylt. Um den Container selbst zu stylen, benötigen wir einen weiteren Import von Emotion.

import {css, injectGlobal} from 'emotion';

injectGlobal injiziert Stile in den "globalen Bereich" (wie das Schreiben von normalem CSS in einem traditionellen Stylesheet – anstatt einen zufälligen Klassennamen zu generieren). Custom Elements sind standardmäßig display: inline (eine etwas seltsame Entscheidung der Spezifikationsautoren). In fast allen Fällen ändere ich diesen Standard mit einem Stil, der auf alle Instanzen der Komponente angewendet wird. Unten sind die buttonStyles, wie wir das ändern können, indem wir injectGlobal verwenden.

injectGlobal`
fancy-button {
  display: block;
}
`

Warum nicht einfach Shadow DOM verwenden?

Wenn eine Komponente in jeder Codebasis landen könnte, dann könnte Shadow DOM eine gute Option sein. Es ist ideal für Drittanbieter-Widgets – jedes CSS, das auf die Seite angewendet wird, wird die Komponente dank der isolierten Natur von Shadow DOM nicht stören. Deshalb wird es zum Beispiel von Twitter-Embeds verwendet. Die überwiegende Mehrheit von uns erstellt jedoch Komponenten nur für eine bestimmte Website oder App und nirgendwo sonst. In dieser Situation kann Shadow DOM mit begrenztem Nutzen Komplexität hinzufügen.