Storybook: Build UI with confidence

Storybook: Build UI with confidence

Overview

Building UI components for the frontend is a common task in modern web development. To meet the demands of complex applications, developers often need to create extensive component libraries.?Storybook?comes to the rescue by helping validate components from multiple perspectives. First, it provides a testing environment to verify component interactions with visualization. Second, it makes it easier to manage and organize components, improving the overall structure and consistency of your UI library. This article will guide you through these concepts step by step, helping you write UI components with confidence.

Prerequisites

Nx

Nx?is a powerful monorepo tool that helps manage UI libraries, whether they are meant for publishing or internal use. In addition to JavaScript and TypeScript projects, Nx supports a wide range of plugins, including those for Java Spring Boot and Python Django. In my project, I use Angular and Storybook plugins for the frontend, along with a NestJS backend and an Electron setup within the monorepo. Nx provides an intuitive way to manage multiple projects together, streamlining your development workflow.

Angular

As of the time of writing, Nx supports?Nuxt.js, allowing developers to work seamlessly with Vue applications. While we'll be using Angular for illustration, Vue and React components follow the same patterns for organizing UI components effectively. The concepts discussed here are applicable across these frameworks, aiding in building robust UI libraries regardless of your choice of technology.

Storybook

At the moment of writing this article,?Storybook?is at version 8, supporting frameworks like Next.js, React, React Native, Vue.js, Angular, Svelte, and web components. Storybook provides a versatile environment for developing and testing UI components in isolation, enhancing the development experience across different technologies.

Demonstration: toolbar

Controls

In this section, we'll demonstrate how to create a toolbar component with Angular and integrate it with Storybook. We'll start with the HTML code for the toolbar component:

<mat-toolbar class="file-table-toolbar mat-elevation-z8">
  <button
    mat-icon-button
    class="icon"
    matTooltip="Delete"
    matTooltipPosition="above"
    aria-label="Delete"
    (click)="deleteSelected()"
  >
    <mat-icon>delete_outline</mat-icon>
  </button>
  <button
    mat-icon-button
    class="icon"
    matTooltip="Download"
    matTooltipPosition="above"
    aria-label="Download"
    (click)="downloadSelected()"
  >
    <mat-icon>download</mat-icon>
  </button>

  <div class="spacer"></div>
  @if (!isSearchVisible) {
  <mat-button-toggle-group
    #group="matButtonToggleGroup"
    (change)="onToggleChange($event)"
  >
    <mat-button-toggle value="search" aria-label="search event">
      <mat-icon>search</mat-icon>
    </mat-button-toggle>
  </mat-button-toggle-group>
  } @if (isSearchVisible) {
  <mat-form-field appearance="outline" class="filter">
    <mat-label>Filter</mat-label>
    <input
      matInput
      (keyup)="applyFilter($event)"
      placeholder="add_to_cart"
      #searchInput
    />
  </mat-form-field>
  }
</mat-toolbar>
        

In the code above, we use Angular's?@if?directive to conditionally display either the search toggle button or the search input field based on the?isSearchVisible?property. When the user clicks the search button,?isSearchVisible?toggles, and the search input appears.

Next, we'll set up the Storybook configuration for this component:

// storybook for Angular
import {
  applicationConfig,
  moduleMetadata,
  type Meta,
  type StoryObj
} from '@storybook/angular';
// component to be tested
import { FileTableToolbarComponent } from './file-table-toolbar.component';
import { expect, within } from '@storybook/test';
// mandatory modules accordingly
import { provideHttpClient } from '@angular/common/http';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { PROJECT_ROUTES } from '../../routes';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
// mandatory services
import { FileTableDataSourceService } from '../../../../shared/services/file-table-data-source/file-table-data-source.service';

const meta: Meta<FileTableToolbarComponent> = {
  component: FileTableToolbarComponent,
  title: 'Modules/Project/Components/FileTableToolbarComponent',
  decorators: [
    moduleMetadata({
      //?? Imports both components to allow component composition with Storybook
      imports: [
        MatIconModule,
        MatToolbarModule,
        MatButtonModule,
        MatTooltipModule,
        MatInputModule,
        MatFormFieldModule,
        FormsModule,
        ReactiveFormsModule,
        MatButtonToggleModule
      ],
      // may mock the injected service
      providers: [FileTableDataSourceService]
    }),
    applicationConfig({
      providers: [
        provideAnimationsAsync(),
        provideHttpClient(),
        provideRouter(PROJECT_ROUTES)
      ]
    })
  ],
  // fields according to the FileTableComponent.ts
  args: {
    isSearchVisible: false
  },
  // set the Storybook control
  argTypes: {
    isSearchVisible: { control: 'boolean' }
  }
};
export default meta;
type Story = StoryObj<FileTableToolbarComponent>;

// default layout
export const Default: Story = {
  args: {}
};

// perform the searching action and verify that the search bar exists
export const Searching: Story = {
  args: {},
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    // Find the mat-button-toggle
    const buttonToggle = await canvas.findByText('search');

    // Verify that the toggle exists
    expect(buttonToggle).toBeTruthy();
    buttonToggle.click();
  }
};        



In the Storybook configuration above:

  • We import necessary Angular Material modules and include them in?moduleMetadata.
  • We define?args?and?argTypes?to control the?isSearchVisible?property through Storybook's UI.
  • We create two stories:?Default, where the search input is hidden, and?Searching, where the search input is visible.

Note:?Ensure all dependencies are correctly imported and any services used in the component are mocked if necessary.

Visual tests

Storybook allows us to write visual tests to interact with components and verify their behavior. Here's how we can perform a visual test for our toolbar component:

// mentioned in the previous code block
// perform the searching action and verify that the search bar exists
export const Searching: Story = {
  args: {},
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    // Find the mat-button-toggle
    const buttonToggle = await canvas.findByText('search');

    // Verify that the toggle exists
    expect(buttonToggle).toBeTruthy();
    buttonToggle.click(); // click to show the search bar
  }
};
        

In this visual test:

  • We use the?play?function to simulate user interactions.
  • We locate the search button using?findByTest?and simulate a click.


Conclusion

In this article, we explored how to use Nx, Angular, and Storybook to build and test UI components effectively. Storybook enhances the development experience by providing an isolated environment to develop, manage, and visually test components. While our example focused on an Angular component, the same principles apply to Vue, React, and other frameworks. By leveraging controls and visual tests in Storybook, you can confidently develop robust UI components and improve your application's overall quality. I encourage you to incorporate these tools into your workflow and experience the benefits firsthand. Please refer to my published Storybook site, source code, and formal documentation for more details.

Johan Adrian Marin Garcia

Full stack developer | Front end | Back end | Angular | React | Node.js | Express.js | NestJS | ASP.NET | SQL | NoSQL | CI/CD

3 个月

Great article! I recommend using tools like Carbon to create code images. (https://carbon.now.sh/)

要查看或添加评论,请登录

Guan Xin Wang的更多文章

社区洞察

其他会员也浏览了