Storybook en Nx Angular monorepo: Stories

Libro en blanco
Foto de Markus Spiske en Unsplash

Llegado el momento hay que crear las stories necesarias para las diferentes funcionalidades. Pero, ¿Qué es una stories? ¿Cómo se escriben?

Bien, según la web de Storybook, una story  captura el estado representado de un componente de la interfaz de usuario. Es una función que devuelve el estado de un componente dado un conjunto de argumentos.

Como siempre mejor explicarlo mediante la práctica donde explicaremos las partes más fundamentales.

Crear una story

Nomenclatura

Habitualmente las stories se escriben dentro de un archivo con la misma nomenclatura más el sufijo «stories». Por ejemplo, si tenemos un componente con el archivo «button.component.ts», el archivo de stories se crea en su misma ubicación con el nombre «button.component.stories.ts». Naturalmente ahí solo existirán stories respecto a ese componente.

Escribiendo una story

Hay un par de aspectos o características que hay que tener en cuenta a la hora de escribir:

  1. El meta componente: Describe y configura el componente.
  2. Las stories.

Ejemplo de la definición de meta componente y una story en «button.component.stories.ts»:

import type { Meta, StoryObj } from '@storybook/angular';
import { TextButtonComponent } from './text-button.component';

/* Meta componente */
const meta: Meta = {
  component: TextButtonComponent
};
export default meta;

/* Story */
type Story = StoryObj;
export const Basic: Story = {};

Al ejecutar el storybook ya podemos ver el botón renderizado.

Args

Para inicializar el componente con unos valores específicos asignaremos a las propiedades del botón unos argumentos. En el caso de querer cambiar la etiqueta/texto del botón, creamos en la story la propiedad «args» que contendrá las propiedades a inicializar:

import type { Meta, StoryObj } from '@storybook/angular';
import { TextButtonComponent } from './text-button.component';

const meta: Meta = {
  component: TextButtonComponent
};
export default meta;

type Story = StoryObj;
export const Basic: Story = {};

export const Primary: Story = {
  args: {
    label: 'clic aquí',
    type: 'primary'
  }
};

export const Secondary: Story = {
  args: {
    ...Primary.args,
    type: 'secondary'
  }
};

En la story «Secondary», vemos que podemos utilizar la configuración de la story «Primary» (mediante el operador spread) cambiando luego su tipo.

Al ejecutar Storybook, vemos que en el sidebar aparecen las stories con el nombre dado al objeto de la story. Al ir navegando por cada una, vemos cómo se inicializa el botón con los argumentos asignados.

Renombrando stories

Podemos asignar un nombre a cada story para que se visualice algo más adecuado para entender mejor su significado. Para ello, añadimos la propiedad «name» con la descripción en la story:

export const Basic: Story = {
  name: 'Botón básico o vacío',
  tags: ['hidden']
};

export const Primary: Story = {
  name: 'Botón primario o principal'
  args: {
    label: 'clic aquí',
    type: 'primary'
  }
};

export const Secondary: Story = {
  name: 'Botón secundario o auxiliar'
  args: {
    ...Primary.args,
    type: 'secondary'
  }
};

Además podemos asignar tags a cada story para que podamos luego clasificar, ordenar, ocultar, o cualquier otra acción que se nos ocurra…. Aquí la story «Basic», tiene el tag «hidden», para ocultarla solamente el sidebar mediante un pequeño proceso hecho por nosotros descrito en el artículo de configuración de Storybook. También podemos utilizar mejor la propiedad «excludeStories» del meta componente para hacer que esa story no se muestre, pero no se mostraría ni en la documentación generada automáticamente.

Meta componente

Estructura y jerarquía

En el sidebar de Storybook se organizan las stories para poder acceder a ellas. Esta organización es fundamental para tenerlas bien clasificadas con nombres adecuados a cada categoría, carpeta, componente, etcétera, a parte de tener el buscador.

Para asignar a las stories una ubicación dentro de esa estructura, modificamos el meta componente añadiendo la propiedad «title» con una estructura específica. Para que las stories se coloquen bajo el mismo paraguas, así por ejemplo, queremos que los botones estén bajo la categoría de «componentes» y carpeta «botones» dicha propiedad debería tener el valor «componentes/botones»:

const meta: Meta = {
  component: TextButtonComponent,
  title: 'Componentes/Botones'
};

Observa cómo utiliza la barra inclinada «/» para hacer de separador.

Tipos de argumentos

Se puede dar al usuario más información respecto a los argumentos de los componentes, es decir, no solo basta con darle el nombre del argumento y su valor actual, si no también una descripción (qué es, para qué sirve, ….), los valores posibles (type, enum, objeto complejo, …),  categorización, etcétera mediante la propiedad “argTypes” en el meta componente. Por defecto podemos ver la siguiente información en la pestaña «Controls»:

Storybook de Angular Frontenders visualización stories
Visualización de stories en el sidebar, selección de una story y su interacción

Apreciamos que solo nos ofrece información del nombre, descripción (vacía), su valor por defecto (-) y el valor que se está aplicando al componente mediante una caja de texto.

En el caso que nos compete, podemos informar más detalladamente al usuario sobre sus propiedades con argTypes de la siguiente manera:

const meta: Meta = {
  component: TextButtonComponent,
  title: 'Componentes/Botones',
  decorators: [
    componentWrapperDecorator((story) =>      <div style="text-align: center; padding: 1em; background-color: #6f6f6f">${story}</div>    )
  ],
  argTypes: {
    label: {
      name: 'label',
      type: {
        name: 'string',
        required: false
      },
      description: 'Text to display inside the button.',
      table: {
        category: 'content',
        type: {
          summary: 'string'
        },
        defaultValue: {
          summary: ''
        }
      },
      control: {
        type: 'text'
      }
    },
    type: {
      name: 'type',
      type: {
        name: 'string',
        required: false
      },
      description: 'Modify the main purpose of the button.',
      table: {
        category: 'display',
        type: {
          summary: 'primary | secondary'
        },
        defaultValue: {
          summary: 'primary'
        }
      },
      options: ['primary', 'secondary'],
      control: {
        type: 'select'
      }
    }
  }
};

A parte de dar una descripción a cada propiedad, podemos categorizarla mediante «category» (content, display … lo que se nos ocurra e incluso hacer subcategorías) así como decidir que control usar para que el usuario «juegue» con el valor de la propiedad, en nuestro caso con una caja de texto para el «label» y un dropdown/select para el «type» con sus posibles valores. También podemos informar si es o no requerido, el tipo de dato (en nuestro caso string y un type -conjunto de valores definido-) y el valor por defecto de esa propiedad.

Esto aparecería en la parte de «Controls» para que vea más información.

Storybook de Angular Frontenders visualización stories ampliando argTypes
Visualización de stories en el sidebar, selección de una story y su interacción ampliando información con argTypes

Decoradores

Un decorador es una forma de envolver una story con una funcionalidad adicional de «renderizado». Muchos complementos definen decoradores para aumentar sus historias con renderizado adicional o recopilar detalles sobre cómo se representa su historia. Existe la propiedad «decorators» que será una lista de decoradores.

Decorador del módulo del componente

Para el caso de que nuestro componente sea standalone no haría falta crear un decorador pero si no lo es,  seguramente necesita un módulo y es necesario que la story tenga un decorador con la definición de ese módulo como por ejemplo:

import { moduleMetadata } from '@storybook/angular';
import { CommonModule } from '@angular/common';

const meta: Meta = {
  component: MyComponent,
  title: 'Componentes',
  decorators: [
    moduleMetadata({
      declarations: [MyComponent],
      imports: [CommonModule]
    })
  ]
};

Vemos que en la lista de decoradores, existe uno que es la definición del módulo dentro del objeto moduleMetada. De esta forma decimos a Storybook qué necesita nuestro botón para funcionar.

Decorador wrapper

A la story podemos «envolverla» por ejemplo dentro de un mark-up de HTML. Para eso, utilizaremos el objeto «componentWrapperDecorator» que nos permita añadir ese HTML alrededor del componente.

Por ejemplo, queremos que todas las stories del botón se muestren dentro de un cuadro con un padding de 1em, fondo gris y el contenido centrado, quedaría de la siguiente manera:

import { componentWrapperDecorator} from '@storybook/angular';

const meta: Meta = {
  component: TextButtonComponent,
  title: 'Componentes/Botones',
  decorators: [
    componentWrapperDecorator((story) =>`<div style="padding: 1em; background-color: #6f6f6f; text-align: center">${story}</div>`)  ]
};

Resumen

Hemos creado nuestras primeras stories de manera muy sencilla organizándolas y dando también un nombre que ayude a entender cuál es su funcionalidad y qué pretendemos que represente. Incluso podemos aprovechar inputs de unas stories para crear otras y explicar mejor al usuario los parámetros que tiene nuestro componente.
Puedes ver el código en nuestro repo de github.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *