Storybook: Build UI with confidence
Guan Xin Wang
Technical Solution Engineer | Software Engineer | Software Developer | Data Engineer
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:
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:
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.
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/)