Federico Gambarino

Using Storybook with Angular Material

June 23, 2020
Angular, Storybook

Storybook is a great library that let you test your components UI in standalone mode. You can write stories, where you change inputs or properties of the component and you can see directly the differences and how well the component behaves. You can even launch automatic test on it. A wonderful UI testing approach!

In this post I will show you how I managed to integrate Storybook in an Angular Material project. I think that there is few documentation regarding the integration in an Angular application, so I hope that this will be useful for you.

Web app creation and Storybook installation

First things first. I created a new Angular Material application using Angular CLI and Angular Material schematics. Then I followed the Storybook’s Angular guide. The only difference from what is written, something that I obviously suggest, is to create in src folder the stories folder, where to put the components stories.
I left the story example that is suggested in the guide, the My button one. Quite easy, I would say, since all the styles are simple and defined in the storybook button… but how can we do the same with our components?

Choose the architecture and create the components

Well, to keep it simple, let’s say that we want an application with a header and a content. In the header we want a title and an icon button to go to the previous page (or a similar behaviour), while in the content we want some text and a button. Basically something that looks like this:

The simple web app

So I decided to create three new components, the header, the content and the button.
Using the amazing Nx Console I added them to the project (I use it since it’s really simple to set specific component options, without the need to remember all Angular CLI’s parameters). Here they are:

header.component.ts
@Component({
  selector: 'app-header',
  template: `
    <div class="header-background">
      <h2>
        <button (click)="onClick()" mat-icon-button>
          <mat-icon>keyboard_backspace</mat-icon></button
        ><span>{{ title }}</span>
      </h2>
    </div>
  `,
  styleUrls: ['./header.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent {
  @Input() title: string
  @Output() clickToGoBack: EventEmitter<void> = new EventEmitter<void>()

  constructor() {}

  onClick() {
    this.clickToGoBack.emit()
  }
}
content.component.ts
@Component({
  selector: 'app-content',
  template: `
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
      velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
      cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
      est laborum.
    </p>
    <div class="ng-content-wrapper"><ng-content></ng-content></div>
  `,
  styleUrls: ['./content.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ContentComponent {
  constructor() {}
}
button.component.ts
@Component({
  selector: 'app-button',
  template: `
    <button mat-raised-button [color]="color" (click)="onClick()">
      {{ text }}
    </button>
  `,
  styleUrls: ['./button.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ButtonComponent {
  @Input() color: 'primary' | 'accent' | 'warn'
  @Input() text: string
  @Output() buttonClick: EventEmitter<void> = new EventEmitter<void>()

  constructor() {}

  onClick() {
    this.buttonClick.emit()
  }
}

I implemented the presentational/container components pattern, where - in this case - the app component is the container. The presentational components have just input and output properties, and adopt the On Push change detection strategy. The app component is the one that provides the data for children components input and that is “listening” to the children components output. Remember that we would like to test with Storybook just the presentational components, since they have the responsability of presenting the UI. Moreover if a service is injected in a component through the constructor (making it usually no more presentational), when you write its stories you need to provide it, thus making the code more complex and messing with responsabilities, with more chances of writing some infamous spaghetti code.

Basically, I wrote the app component template like this:

app.component.html
<app-header
  title="Storybook with Angular Material"
  (clickToGoBack)="onClickToGoBack()"
></app-header>
<div class="app-container">
  <app-content
    ><app-button
      (buttonClick)="onButtonClick()"
      color="primary"
      text="click me!"
    ></app-button
  ></app-content>
</div>

Where I used Angular’s content projection to insert our button component in the content component.

Let’s write some stories!

Perfect then, we have all the components we need to write some tests. To keep it simple I just wrote some stories for header and button components. So, I followed the My button example to write the following stories:

header.stories.js
storiesOf('Header Component', module)
  .addDecorator(
    moduleMetadata({
      imports: [MatIconModule, MatButtonModule],
    })
  )
  .add('Short text', () => ({
    component: HeaderComponent,
    props: { title: 'Hello!' },
  }))
  .add('Very long text', () => ({
    component: HeaderComponent,
    props: {
      title:
        'This is a very very long text, so you can check how the header behaves in such cases!',
    },
  }))

But then I saw something unexpected:

Header component does not display the Mat Icon

The Material Icon was missing!

That’s related to the fact that when we add Material to an Angular project with schematics, stylesheets are added in the index.html head:

<link
  href="https://fonts.googleapis.com/icon?family=Material+Icons"
  rel="stylesheet"
/>

Add a custom head tag

If we want to have injected in the head of the storybook page the same stylesheet that is being added in index.html, we need to add a Custom Head Tag. As described in the Storybook documentation we can add a preview-head.html file in the .storybook folder. And that’s what I did:

preview-head.html
<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet"
    />
  </head>
</html>

Having then this result:

Header component displays the Mat Icon

What to do in case of wrappers and global styles

Then I wrote the stories related to the button component. But the width, the position and the text case of the button were different between application and story. If we look into the styles.scss file it’s easy to understand why:

styles.scss
.ng-content-wrapper {
  text-align: center;
  button {
    min-width: 50%;
    text-transform: uppercase;
  }
}

There are some global styles that are defined in the styles.scss file. How can we try to manage those? Obviously we can rewrite all with a more component oriented style… but what to do if we want to integrate Storybook in an already existing project where there are some global scss rules that we want to take into account?

Luckily Storybook let us define a decorator to wrap what we want, so we can write a higher order function like this one:

template-wrapper.utils.js
export const wrap = templateFn => storyFn => {
  const story = storyFn()
  return {
    ...story,
    template: templateFn(story.template),
  }
}

where through the templateFn we can define what wraps the template. Basically it is possible to write stories like this:

button-with-template-wrap.stories.js
storiesOf('Button Component', module)
  .addDecorator(
    moduleMetadata({
      imports: [MatButtonModule],
      declarations: [ButtonComponent],
    })
  )
  .addDecorator(
    wrap(content => `<div class="ng-content-wrapper">${content}<div>`)
  )
  .add('with wrapper', () => ({
    template: `<app-button [text]="text" [color]="color"></app-button>`,
    props: {
      text: 'with wrapper',
      color: 'accent',
    },
  }))

and have this result for the button without the wrapper class:

Button story without wrapper

while having this result for the button with the wrapper class:

Button story with wrapper


With this post you should be able to write stories for many different scenarios now, and properly test the presentational components in your projects.

If you want to see the code of this proof of concept with more detail, there is this repository available: Storybook with Angular Material

...and that's it for today! Thank you for your time 😃

Federico


© 2022, Federico Gambarino - All Rights Reserved

Made with ❤️ with Gatsby