Storybook for Angular – creating components library
Imagine a situation where you have a big monolith project and there’s a need to separate re-usable components throughout application or you’re starting a microfrontend application with separated UI components. Additionally, a readable and well-defined documentation is essential in both cases.
This is where Storybook can help you!
Storybook is a framework that allows you to create a visual documentation for UI libraries. It enables the developer to have a deeper understanding of the components and their interactions.
Advantages of using Storybook:
- fast development
- code quality
- scalability
- components are pretty easy to test
Thanks to Storybook, our styles/behaviors of components become standardized, reducing duplicates or unexpected behaviors. Overall, development time decreases due to predefined components like inputs, buttons etc. Views can be implemented very fast, almost in puzzle-like manner.
Storybook is definitely NOT for developers who:
- does not like writing documentation
- create messy code 🙂
Let’s jump into quick Storybook example!
For our simple case we will use an already created Angular project with two components: ButtonComponent
and InputComponent
.
As the library will be shared across application, a documentation needs to be written.
Setup
Let’s start with installing Storybook inside application via:
npx @storybook/addon-knobs
Storybook will automatically detect which framework are you using currently, and create files for you.
Let’s run Storybook:
npm run storybook
After running the command above, initial Storybook page should become visible…
… and the src
folder should have stories
directory created, containing pre-defined, default stories.
For now, Storybook is set up and we can start writing stories.
Let’s start with ButtonComponent.
src/button/button.component.ts
import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button [disabled]="disabled" class="pure-material-button-contained" [style.background-color]="color" (click)="onClickTask.emit($event)">
{{text}}
</button>
`,
styleUrls: ['./button.component.css']
})
export class ButtonComponent {
@Input() text: string;
@Input() color: string;
@Input() disabled: boolean;
@Output() onClickTask: EventEmitter<any> = new EventEmitter();
}
ButtonComponent
is very simple. Custom text, background color or disabled flag and click event has been added.
Now we can write our stories.
Storybook is all about writing stories. A story usually contains a single state of one component, almost like a visual test case.
Let’s jump into src/stories/Button.stories.ts
and change a bit of the generated, Storybook code.
First, we need to change the component declaration name to ButtonComponent
, and link the new component into ButtonWithLinkToAnotherStory
. For now, rest of code can be deleted.
src/stories/Button.stories.ts
import { linkTo } from '@storybook/addon-links';
import {ButtonComponent} from '../app/button/button.component';
// settngs for story module
export default {
title: 'Custom Button',
component: ButtonComponent //changing the component declaration here
};
//story case
export const ButtonWithLinkToAnotherStory = () => ({
component: ButtonComponent, //changing the component declaration here
name: 'button with link to another story',
props: {
text: 'Go to Welcome Story',
onClickTask: linkTo('Welcome'), // redirecting to Welcome story
},
});
After saving, you should see:
Knobs
Let’s create more advanced case for ButtonComponent
story with knobs. Storybook Knobs add-on allows you to edit props dynamically using the Storybook UI. You can also use Knobs as a dynamic variable inside stories in Storybook.
There are many types of knobs, such as: text, boolean, number, color, object, array, select, etc.
More info about available knobs can be found here:
https://www.npmjs.com/package/@storybook/addon-knobs
First, knobs needs to be installed into application: npm i @storybook/addon-knobs
within .storybook/main.js:
module.exports = {
addons: ['@storybook/addon-knobs/register']
}
Let’s go back to Button.stories.ts
and add a decorator into our default story:
export default {
title: 'Button',
component: ButtonComponent,
decorators: [withKnobs]
};
It tells us some knobs can be added to component. We can now add some knobs into project as a separate story item:
export const ButtonKnobs = () => ({
component: ButtonComponent,
name: 'Button Knobs', // name of our story case
props: {
text: text('text', 'Hello Storybook'), // we are able to change text inside button component
color: color('color label', '#e74141'), // we are able to change background color inside button component
disabled: boolean('should be disabled', false), // we are able disable button component
onClickTask: e => { // handling click event
e.preventDefault();
action('log')(e.target);
}
},
});
Do not forget to import all necessary dependencies.
src/stories/Button.stories.ts
import {action} from '@storybook/addon-actions';
import {linkTo} from '@storybook/addon-links';
import {ButtonComponent} from '../app/button/button.component';
import {boolean, color, text, withKnobs} from '@storybook/addon-knobs';
export default {
title: 'Button',
component: ButtonComponent,
decorators: [withKnobs]
};
export const ButtonKnobs = () => ({
component: ButtonComponent,
name: 'Button Knobs', // name of our story case
props: {
text: text('text', 'Hello Storybook'), // we are able to change text inside button component
color: color('color label', '#e74141'), // we are able to change background color inside button component
disabled: boolean('should be disabled', false), // we are able disable button component
onClickTask: e => { // handling click event
e.preventDefault();
action('log')(e.target);
}
},
});
export const ButtonWithLinkToAnotherStory = () => ({
component: ButtonComponent,
props: {
text: 'Go to Welcome Story',
onClickTask: linkTo('Welcome'),
},
});
ButtonWithLinkToAnotherStory.story = {
name: 'button with link to another story',
};
After saving, you should receive:
We’ve got some basic story cases for ButtonComponent
!
Composition
To speed up development, whole views can be created in Storybook. In case of this example, AppComponent
consists of ButtonComponent
and InputComponent
:
src/app.component.html
<app-input name="login" type="text"></app-input>
<app-input name="password" type="password"></app-input>
<app-button text="Login"></app-button>
src/input/input.component.ts
import {Component, Input} from '@angular/core';
@Component({
selector: 'app-input',
template: `
<article>
<div class="material-input">
<input type="{{type}}" required="">
<label>{{name}}</label><span></span>
</div>
</article>
`,
styleUrls: ['./input.component.css']
})
export class InputComponent {
@Input() name: string;
@Input() type: string;
}
Let’s write a story which will show the whole view of AppComponent
.
Create a Combined.stories.ts
file inside src/stories
. Same as before, module needs to be defined, with some additional info:
import {AppComponent} from '../app/app.component';
import {moduleMetadata} from '@storybook/angular';
import {InputComponent} from '../app/input/input.component';
import {ButtonComponent} from '../app/button/button.component';
import {CommonModule} from '@angular/common';
export default {
title: 'Combined',
decorators: [
moduleMetadata({
// imports both components to allow component composition with storybook
declarations: [AppComponent, InputComponent, ButtonComponent],
imports: [CommonModule]
}),
],
};
export const AppComponentView = () => ({
component: AppComponent,
props: {},
});
moduleMetadata
allows us to create component composition inside our stories, more information available at:
https://www.learnstorybook.com/intro-to-storybook/angular/en/composite-component/
Wrap up
The article is just a simple introduction into Storybook’s capabilities. There’s a huge variety of use cases that can be covered with it.
I would like to encourage you to use Storybook in your project. A well-written, extensive documentation is always the perfect way to create scalable, error-proof projects 🙂