Einführung von Storybook für Stencil Komponenten

Einführung von Storybook für Stencil Komponenten

Storybook (https://storybook.js.org) hat sich über die letzten Jahre als Standard Tool für das Entwickeln von isolierten Komponenten in Frontend Frameworks herauskristallisiert. Durch die leichte Einbindung von Komponenten, deren hierarchische Darstellung und der Möglichkeit, die Auswirkungen der Properties einer Komponente direkt zu sehen, erleichtert sich die Entwicklung sowie die Zusammenarbeit mit den Designern.

Stencil (https://stenciljs.com) ist eine Kombination aus verschiedenen Technologien und Ideen mit denen man wiederverwendbare Web Komponenten die den entsprechenden Standard unterstützen (https://www.webcomponents.org) erstellen kann und die dann auch in allen Browsern einsetzbar sind. 

Leider existiert keine bereits fertige Anleitung, um diese beiden Technologien miteinander zu verbinden. Auch in der Dokumentation von Storybook gibt es überwiegend React Beispiele, was es teilweise erschwert Web Komponenten in Verbindung mit Storybook zu verwenden. Daher möchte ich im Folgenden darauf eingehen, wie wir Stencil in Verbindung mit Storybook nutzen. 

Als Beispiel nehme ich eine simple Button Komponente, die wir in Stencil angelegt haben. 

import { Component, Host, h, Prop } from '@stencil/core';
import classnames from 'classnames';

@Component({
  tag: 'ihg-button',
  styleUrl: 'ihg-button.scss',
  shadow: true,
})
export class IhgButton {
  @Prop() label = '';
  @Prop() type: 'primary' | 'secondary' = 'primary';
  @Prop({ reflect: true }) disabled = false;

  render(): HTMLElement {
    return <Host>
      <button
        class={classnames(
          'button',
          this.type,
          this.disabled && 'disabled',
        )}
        type="button"
      >
        <span>{this.label}</span>
      </button>
    </Host>;
  }
}

Um zu testen, dass dieser Button in allen möglichen Kombinationen der Spezifikation entsprechend programmiert ist, müsste ich jede dieser Kombinationen in einem HTML Dokument anlegen. 

<ihg-button type="primary" label="primary"></ihg-button>
<ihg-button type="secondary" label="secondary"></ihg-button>
<ihg-button type="primary" label="primary" disabled></ihg-button>
<ihg-button type="secondary" label="secondary" disabled></ihg-button>

Für solch eine einfache Komponente geht der Aufwand noch, allerdings ist leicht vorstellbar wie viele Fälle von etwas komplexeren Komponenten notwendig wären. 

Konfiguration von Storybook und Stencil

Um Storybook zum Projekt hinzu zu fügen wird der Befehl

npx sb init

verwendet. Bei der Auswahl des Projekttyps haben wir web_components ausgewählt.

Damit Storybook die Komponenten aus Stencil verwenden kann muss Storybook die Stencil Komponenten einbinden. Stencil stellt unter einem Output Target (https://stenciljs.com/docs/custom-elements) die geschriebenen Web Komponenten so zur Verfügung, dass diese woanders eingebunden werden können. Damit dies bei Storybook funktioniert können wir diese gebauten Elemente in der Storybook Datei preview.js (./.storybook/preview.js) einfügen. 

import { defineCustomElements } from '../dist/loader';

defineCustomElements();

Der dist Ordner ist hierbei der Ordner den Stencil unter dem Output target ‚dist‘ verwendet (https://stenciljs.com/docs/distribution). Damit unsere Änderungen in den Stencil Dateien auch direkt Auswirkungen auf diesen Ordner haben verwenden wir den stencil build --watch Befehl.

Anbei der Code für die Story Datei „Button.stories.tsx“:

export default {
  title: 'Components/ihg-button',
  component: 'ihg-button',
};

const defaultArgs = {
  label: 'button label',
  type: 'primary',
  disabled: false,
};

const Template = ({label, type, disabled}) => `<ihg-button label="${label}" type="${type}" ${disabled ? 'disabled' : ''} />`;

export const PrimaryButton = Template.bind({});
PrimaryButton.args = {...defaultArgs};

Der default export beschreibt hierbei die Struktur der Storybook Navigation. Jeder Named Export würde dann zu einem eigenen Punkt in der Navigation führen. 

Wenn wir nun Storybook starten (Der Installationsprozess von Storybook legt hierbei automatisch einen storybook Befehl in der package.json Datei an) erhalten wir folgende Ausgabe: 

Um unsere verschiedenen Button Varianten anzuzeigen kann man einfach die Werte in dem unteren Bereich anpassen

Man kann jedoch auch die Story Datei so bearbeiten, dass mehrere Links die Funktionen des Buttons beschreiben. Fügen wir zum Beispiel folgenden Code hinzu:

export const SecondaryButton = Template.bind({});
SecondaryButton.args = {...defaultArgs, type: 'secondary'};

export const DisabledPrimaryButton = Template.bind({});
DisabledPrimaryButton.args = {...defaultArgs, disabled: true};

export const SecondaryDisabledButton = Template.bind({});
SecondaryDisabledButton.args = {...defaultArgs, type: 'secondary', disabled: true};

Dann erhalten wir für jeden export die entsprechende Prop Kombination

Somit können wir Web Komponenten flexibel und isoliert entwickeln und auch gleichzeitig alle Möglichkeiten und States leicht ersichtlich machen. 

Hinzufügen von JSX

Was uns noch nicht gefallen hat, war das wir einen String verwenden müssen, um das Template zu beschreiben:

<ihg-button label="${label}" type="${type}" ${disabled ? 'disabled' : ''} /> 

Stattdessen wollten wir JSX verwenden, da es flexibler und angenehmer ist. Des Weiteren kann man somit auch den Spread-Operator nutzen, um die Properties komfortabler in die Komponenten einfügen zu können. Um das zu lösen haben wir uns die Framework Integration von Stencil in React genauer angeschaut (https://stenciljs.com/docs/react) und sind den Installationsanweisungen entsprechend gefolgt.

Alle Story Dateien müssen dadurch dann allerdings vom Stencil Build-Prozess ausgeschlossen werden. Dies haben wir über eine Konfiguration in der Datei tsconfig.json sichergestellt.

...
"exclude": [
    "node_modules",
    "**/*.stories.*"
  ]
...

Danach kann man in den Stories JSX nutzen und bekommt dasselbe Endergebnis wie zuvor:

import React from 'react';
import { IhgButton } from '../../../component-library-react/src/components';

export default {
  title: 'Components/ihg-button',
  component: 'ihg-button',
};

const defaultArgs = {
  label: 'button label',
  type: 'primary',
  disabled: false,
};

const Template = (props) => <IhgButton {...props} />

export const PrimaryButton = Template.bind({});
PrimaryButton.args = {...defaultArgs};

export const SecondaryButton = Template.bind({});
SecondaryButton.args = {...defaultArgs, type: 'secondary'};

export const DisabledPrimaryButton = Template.bind({});
DisabledPrimaryButton.args = {...defaultArgs, disabled: true};

export const SecondaryDisabledButton = Template.bind({});
SecondaryDisabledButton.args = {...defaultArgs, type: 'secondary', disabled: true};

Wie zu Beginn schon erwähnt, können wir somit unsere Komponenten viel leichter anzeigen und isoliert weiterentwickeln. Des Weiteren kann man das Storybook Projekt auch im Laufe der Continuous Integration Pipeline automatisch bauen lassen und die Komponenten so nochmal einem Manuellen Test unterziehen. 

Zur leichteren Nachvollziehbarkeit ist der gesamte Code für dieses Beispiel auch unter GitHub unter https://github.com/Interhyp/stencil-storybook-demo zu finden. 

Dieser Beitrag hat 2 Kommentare

  1. René

    Vielen Dank für den Artikel! Habt ihr in der Zwischenzeit auch schon Erfahrungen beim Einsatz von MDX-Dateien für die Storybook-Docs und beim Verwenden von Storyshots (Snapshot-Tests für Stories) sammeln können? Und seid ihr nach nun einigen Monaten mit diesem Setup weiterhin glücklich? Gibt es Grenzen, an die ihr gestoßen seid?

  2. Thiede Markus

    Hi René, vielen Dank für deine Nachfrage.

    Leider entwickeln wir das Stencil Projekt momentan nicht aktiv weiter und sind mit neueren Entwicklungen auf Next.js gewechselt. Da funktioniert Storybook dank React auch ohne vorher kompilierte Komponenten. Unsere Stencil Komponenten verwenden wir dort einfach durch den react-output-target weiter, sind aber dabei diese Stück für Stück zu ersetzen.

    Deswegen haben wir leider auch noch nicht mit Storybook Docs oder Storyshots in Bezug auf Stencil gearbeitet.

Schreibe einen Kommentar zu Thiede Markus Antworten abbrechen