Storybook con componente web nativo

Añadir Componentes Web nativo a framework storybook para creación de stories útil para testing, pruebas de diseño
Storybook con componente web nativo

Hace unos meses estuve creando Componentes web nativos sin frameworks ni librerías y llevaba tiempo queriendo llevarlo a storybook para aprenderlo. El framework empecé a usarlo hace unas semanas para un proyecto Angular y me está encantando

Dejo un enlace donde puede verse el resultado final :)

Creación de proyecto

Iniciar proyecto indicando el flag --type html

npx storybook init --type html

Es necesario tener ya un proyecto. Si aún no existe puedes iniciarlo con npm init para que el generador de storybook pueda escribir en el package.json todas las dependencias y script

Estructura de proyecto en storybook

La estructura puede ser personalizada al gusto, aunque el scaffolding es similar a muchos otros proyectos

Ubicación de stories en storybook

El scaffolding de storybook, por lo que he podido ver en proyectos del tipo Web Components, Angular y Html, es el mismo. Carpeta src/stories que contiene las stories.

Los ficheros para la creación de stories tiene que terminar con el siguiente nombre .stories.js

La instalación se genera con datos de ejemplo: ficheros de estilos CSS, componentes, recursos estáticos en src/stories/assets y fichero Readme. Estos ficheros son de ejemplo que se pueden eliminar. Los componentes creados de ejemplo para proyecto tipo HTML no son WebComponents nativos que se genera mediante clases que extiendan de HTMLElement y por tanto no se define la etiqueta customElements.define("mi-componente", MiComponente). Lo que se hace en los Componentes de ejemplo es el tradicional .createElement()

Ficheros de configuración de storybook

En la raiz del proyecto se crea una carpeta .storybook que por defecto contiene dos ficheros. En la DOC se puede ver que se pueden ir añadiendo más ficheros según el grado de personalización. Si más adelante tengo tiempo (y ganas :)) escribiré más sobre este tema en proyecto con Angular

  • main.js: contiene configuraciones. Por defecto indica las ruta de las stories y ficheros .mdx, así como los addons instalados
  • preview.js: para la configuración de actions y controls
  • del previsualizador de stories

Hasta aquí lo básico, no hay que tocar mucho. En mi caso eliminé los ejemplos, ajuste de rutas a las stories y añadido un nuevo addons de Custom Properties de CSS para que se pueda personalizar las stories mediante variables CSS

Creación de stories con Web Components en storybook

Ubicación de los Web Components

Lo primero comentar que he usado tres Web Components que hice hace tiempo y quería ver que tal sería incorporarlos en storybook. Pongo enlace a entradas de Blog:

Los ficheros de los componentes están ubicados en src/js. Lo único que necesitamos es su ruta para poder incluirlos en las stories

Estructura de las stories en Storybook

Aunque tengo tres componentes sólo comentaré cosas sobre el componente Tabs que tiene más cosas de las que hablar. Este componente tiene tres etiquetas con atributos opciones por si se quieren personalizar

<iga-tab active="2" justify="space-evenly" style="--color-tab-background-active: #e66c4d"></iga-tab>
<iga-tab-item></iga-tab-item>
<iga-tab-panel></iga-tab-panel>

La estructura básica que he aplicado es (que es lo mínimo):

import { IgaTab } from '../js/tab/iga-tab';

export default {
  title: 'Tab/Tab',
  component: IgaTab,
};

const Template = ({...args}) => `<html-tags />`;

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

El primer bloque es la importación de nuestro WebComponent e importación de otros componentes, librerías, datos... que fuera necesarios

El segundo bloque es la definición básica (o no tan básica) de la story

El tercer bloque es la creación de una Template que sirva como modelo para las diferentes stories que necesitemos

El cuarto bloque son cada una de las stories personalizadas que necesitemos crear basándonos en la/s Template/s que tengamos creadas y personalizadas según interese mediente argumentos

Entrando ahora en detalle sobre código, el fichero src/stories/IgaTab.stories.js contiene todo el sistema de Tabs: contenedor + botones + contenidos

Se obtiene el WebComponent y un array con objetos para los botones y los contenidos de los Tabs

import { IgaTab } from '../js/tab/iga-tab';
import datas from './data/tab-content'
const data = datas.items

Definición general de la storie

export default {
  // Nombre con el que aparece en el sidebar izquierdo (cada barra '/' indentará en el tree)
  title: 'Tab/Tab',
  // Nombre de nuestro componente
  component: IgaTab,
  // Se pueden definir las entradas o inputs que puede tener nuestro componente, que se muestran en los 'Controls'
  // En este caso, será una lista 'select' con las diferentes opciones
  argTypes: {
    justify: {
      options: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'],
      control: { type: 'select' },
    },
  },
  // Configuración de los Addons
  parameters: {
    // Hemos habilitado uno que permite 'jugar' con Custom Property de CSS
    // Más info: https://github.com/ljcl/storybook-addon-cssprops
    cssprops: {
      // No se añaden los '--'
      "color-tab-background-active": {
        category: 'Optional',
        value: "tomato",
        description: "BG button active color",
      },
      "color-tab-foreground": {
        category: 'Optional',
        value: "#414144",
        description: "FOREGROUND button color",
      },
      "trans-dur": {
        category: 'Optional',
        value: "0.2s",
        description: "Transition duration (ms/s units)",
      },
      "trans-del": {
        category: 'Optional',
        value: "0.1s",
        description: "Transition delay (ms/s units)",
      },
    }
  },
};

Creación de Template base para cada una de las stories de nuestro componente. No tendría porque añadirse HTML. Por ejemplo, usando Angular en muchas ocasiones no es necesario salvo que queramos envolver el componente o similar. En nuestro caso hacemos uso de las tres tags que hemos creado

Para acceder a los parámetros lo hacemos mediante args

const Template = ({...args}) => `
<iga-tab justify="${args.justify}" active="${args.active}">
  <div slot="group-tabs" role="tablist">
    ${args.data.map(tab => `<iga-tab-item><span>${tab.label}</span></iga-tab-item>`).join("")}
  </div>
  <main slot="group-panels" class="tabs__panels">
    ${args.data.map(tab => `<iga-tab-panel>${tab.content}</span></iga-tab-panel>`).join("")}
  </main>
</iga-tab>
`;

Finalmente hacemos uso de la Template personalizando los atributos que queramos para la carga por defecto. Hemos añadido los datos de ejemplo data que tenemos en un fichero independiente para no "ensuciar" mucho el código

export const Tab1 = Template.bind({});
Tab1.args = {
  active: 0,
  justify: 'flex-start',
  data
};

Con todo hasta aquí, ya tenemos una storie completa del Componente Tab. Pero se pueden hacer stories más pequeñas. Muestro snippet comentado de dos stories para los botones del Tab

export default {
  // Nombre con el que aparece en el sidebar izquierdo (cada barra '/' indentará en el tree)
  title: 'Tab/TabItem',
  // Nombre de nuestro componente
  component: IgaTabItem,
  // Configuración de los Addons
  parameters: {
    // Hemos habilitado uno que permite 'jugar' con Custom Property de CSS
    // Más info: https://github.com/ljcl/storybook-addon-cssprops
    cssprops: {
      // No se añaden los '--'
      "color-tab-background-active": {
        category: 'Optional',
        value: "red",
        description: "BG button active color",
      },
      "color-tab-foreground": {
        category: 'Optional',
        value: "#414144",
        description: "FOREGROUND button color",
      },
    }
  },
};

// Template de nuestra storie a la que añadimos parámetros para que tenga
// datos y sea personalizable
const Template = ({...args}) => `<iga-tab-item>${args?.label}</iga-tab-item>`;

// Storie 1
export const TabItem1 = Template.bind({});
TabItem1.args = {
  label: 'Tab ítem demo1'
};

// Storie 2
export const TabItem2 = Template.bind({});
TabItem2.args = {
  label: 'Tab ítem 2'
};

Arrancar proyecto en local

Puedes clonarte el repo y hacer las pruebas localmente

Instalación de dependencias

npm install

Lanzar storybook

npm run storybook

Ver los web components independiente a storybook

npm start