Building Components with Angular and Storybook

Plus support for Markdown!

Every developers nightmare is keeping documentation up to date, right? There’s never going to be a silver bullet to make that requirement go away, but there are some tools out there that make it a bit more enjoyable… Storybook being one of them.

Storybook is written in React but there is also decent support for Angular and Vue. In this article I am going to explore their support for Angular.

Storybook is an open source tool for developing UI components in isolation for React, Vue, and Angular. It makes building stunning UIs organised and efficient.
Storybook UI

One of the greatest qualities of Storybook is the ability to configure components in isolation. Taking them completely out of context of your application allows you to think about and test different permutations of how the components might look and behave with different properties etc. This also makes it easier to detect those tricky edge cases.

Storybook also provides a beautiful platform to document and showcase each use case to share with other developers.

Create some Angular Components

I used Storybook when I worked with React and it was a great tool. I’ve recently implemented in Angular and thought I would share how I configured it. I am going to create a new ng project from scratch and walk through the set-up:

ng new ng-storybook-markdown

Let’s add a few components, which we can document in Storybook:

ng generate component componentA
ng generate component componentB

For the vast majority of use cases Angular components will render based on Inputs parameters. These are defined at a component level and generally tell the component what it needs to know, in order to render itself. The components can also distribute events using Outputs which can then tell parent components or application services what to do in the event a user interacts with the component usually in the form of events.

It is good design to keep the components dumb to what happens outside of themselves. Components don’t need to care about anything else happening around them, other than what the application tells them (Input) and what they need to tell the application based on events (Output).

component-a.component.ts:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
  @Component({
selector: 'app-component-a',
templateUrl: './component-a.component.html',
styleUrls: ['./component-a.component.scss']
})
  export class ComponentAComponent implements OnInit {
    @Input() name: string;
@Output() myEvent: EventEmitter<any> = new EventEmitter();
    constructor() { }

ngOnInit() {
  }
}

Now the component has a value for name and an event it can trigger. To use these in our template is simple:

component-a.component.html:

<p>hello, my name is {{name}}!</p>
<button (click)="myEvent.next()">Click me please</button>

Remember myEvent is an observable so we need to invoke next().

Now we have some components, let’s get Storybook up and running and create some stories.

Configure Storybook for Angular

First, let’s add Angular storybook support to our project by running the following command:

npx -p @storybook/cli sb init --type angular

If you get stuck, there’s a decent manual walkthrough here.

This command should install all required dependencies and configure storybook for Angular including support for Typescript. Winner!

Let’s take a look at the important bits:

.storybook

All the configuration is held in the .storybook directory. We will take at some options in here shortly.

src/stories

We will be adding our “stories” in the src/stories directory. You will see an example in there already called index.stories.ts.

package.json

Storybook has also created two handy scripts for us. One to start the local development instance and another to build our storybook as static HTML files.

Let’s spin the local dev instance up now by running npm run storybook and connecting to http://localhost:6006.

Create some Storybook Stories

Let’s add a new file called component-a.stories.ts in src/stories. Storybook will look in this directory for any files ending in .stories.ts and attempt to load them as stories.

In the below example, we are creating 3 stories each with a different name prop which maps to the component Input:

src/stories/compenent-a-stories.ts:

import { storiesOf } from '@storybook/angular';
import { ComponentAComponent } from '../../src/app/component-a/component-a.component';
storiesOf('Component A', module)
.add('Chris', () => ({
component: ComponentAComponent,
props: {
name: 'Chris',
},
}))
.add('Jane', () => ({
component: ComponentAComponent,
props: {
name: 'Jane',
},
}))
.add('Joe', () => ({
component: ComponentAComponent,
props: {
name: 'Joe',
},
}));

But what about events? Storybook provides actions which can be triggered as events, mapped to the Angular component Output:

src/stories/compenent-a-stories.ts:

import { storiesOf } from '@storybook/angular';
import { action } from '@storybook/addon-actions';
import { ComponentAComponent } from '../../src/app/component-a/component-a.component';
storiesOf('Component A', module)
.add('Chris', () => ({
component: ComponentAComponent,
props: {
name: 'Chris',
myEvent: action('Hello Chris!')
},
}))
.add('Jane', () => ({
component: ComponentAComponent,
props: {
name: 'Jane',
myEvent: action('Hello Jane!')
},
}))
.add('Joe', () => ({
component: ComponentAComponent,
props: {
name: 'Joe',
myEvent: action('Hello Joe!')
},
}));

The triggered actions are then visualised in the storybook UI:

The scenarios above are basic, what happens if a component has additional requirements such as a service or an external dependency, can Storybook handle those?

Of course it can…. Similar to writing modules or unit tests you can pass imports and providers to your stories:

src/stories/compenent-a-stories.ts:

storiesOf('Component A', module)
.add('Chris', () => ({
component: ComponentAComponent,
props: {
name: 'Chris',
myEvent: action('Hello Chris!')
},
imports: [ MyExampleModule ],
providers: [ MyExampleService ]

}))
.add('Jane', () => ({
component: ComponentAComponent,
props: {
name: 'Jane',
myEvent: action('Hello Jane!')
},
imports: [ MyExampleModule ],
providers: [ MyExampleService ]

}))
.add('Joe', () => ({
component: ComponentAComponent,
props: {
name: 'Joe',
myEvent: action('Hello Joe!')
},
imports: [ MyExampleModule ],
providers: [ MyExampleService ]

}));

The composition of stories are in JSON, so you can be creative on how you structure them.

If you find yourself declaring the same imports and providers on each story, you can add a decorator to add them to all the stories within your module:

src/stories/compenent-a-stories.ts:

import { storiesOf, moduleMetadata } from '@storybook/angular';
...
storiesOf('Component A', module)
.addDecorator(
moduleMetadata({
imports: [ MyExampleModule ],
providers: [ MyExampleService ]
}),
)

.add('Chris', () => ({
component: ComponentAComponent,
props: {
name: 'Chris',
myEvent: action('Hello Chris!')
}
}))
.add('Jane', () => ({
component: ComponentAComponent,
props: {
name: 'Jane',
myEvent: action('Hello Jane!')
}
}))
.add('Joe', () => ({
component: ComponentAComponent,
props: {
name: 'Joe',
myEvent: action('Hello Joe!')
}
}));

Add Markdown support

One of the great features of Storybook is it’s support for Markdown for documentation. The ability to document components and version in source control, and present them in Storybook’s clean UI is a big bonus.

Markdown is a lightweight markup language with plain text formatting syntax. Its design allows it to be converted to many output formats, but the original tool by the same name only supports HTML.

You may have noticed the Notes tab in the Storybook UI:

Let’s look at what’s involved in getting some text in there formatted using Markdown.

I am going to create a notes directory to live inside my stories directory and add a placeholder in there for component-a.notes:

cd src/stories/
mkdir notes
cd notes
touch component-a.notes.md

src/stories/notes/component-a.notes.md:

# Component A
Some text to go here.

Now let’s go ahead and import the markdown file into our story module and assign it to each of our stories:

src/stories/compenent-a-stories.ts:

import * as markdown from './notes/component-a.notes.md';
...
storiesOf('Component A', module)
.addDecorator(
moduleMetadata({
imports: [ MyExampleModule ],
providers: [ MyExampleService ]
}),
)
.add('Chris', () => ({
component: ComponentAComponent,
props: {
name: 'Chris',
myEvent: action('Hello Chris!')
}
}), { notes: { markdown }})
.add('Jane', () => ({
component: ComponentAComponent,
props: {
name: 'Jane',
myEvent: action('Hello Jane!')
}
}), { notes: { markdown }})
.add('Joe', () => ({
component: ComponentAComponent,
props: {
name: 'Joe',
myEvent: action('Hello Joe!')
}
}), { notes: { markdown }});

Boom! When you try to compile with the above changes, the compiler will complain saying it can’t find the module…

This is because it doesn’t natively support Markdown files as modules and therefore can’t find it:

ERROR in /ng-storybook-markdown/src/stories/component-a.stories.ts
ERROR in /ng-storybook-markdown/src/stories/component-a.stories.ts(4,27):
TS2307: Cannot find module './notes/component-a.notes.md'.

To fix this, we need to add a new file called typings.d.ts inside the .storybook folder:

.storybook/typings.d.ts:

declare module '*.md' {
const content: string;
export = content;
}

Finally, we need to tell TypeScript to look for *.md files as modules by referencing typings.d.ts file in tsconfig.json:

.storybook/tsconfig.json:

{
"extends": "../src/tsconfig.app.json",
"compilerOptions": {
"types": [
"node"
]
},
"exclude": [
"../src/test.ts",
"../src/**/*.spec.ts",
"../projects/**/*.spec.ts"
],
include": [
"../src/**/*",
"../projects/**/*"
],
"files": [
"./typings.d.ts"
]

}

Now we have support for Markdown notes, check it out!!

Configure Addons

Storybook is a platform which is growing in popularity and as such as a strong community around it. Storybook also has released some additional functionality in the form of Addons.

There are also open-source community projects out there supporting new features for the platform plus you can even create your own:

Some don’t have Angular support just yet, so make sure you find that out before getting too carried away — you can find this out here.

To finish this article, I am going to walk through adding Viewport.

Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in Storybook. This helps build responsive components inside of Storybook.

Configuring Addons is pretty straightforward, first you need to install the additional dependency:

npm i --save-dev @storybook/addon-viewport

Then add the following line to .storybook/addons.js:

.storybook/addons.js:

import '@storybook/addon-viewport/register';

That’s it! Now we have Viewport and can view our components across a range of device screen sizes.

As always, here is a working version on my Github, enjoy!!:

Thank you for taking the time to read my article.