Building a Design-Driven Workflow in Storybook.js with Angular and Sketch – Part 2

In the first part of this series, we learned about the essentials of setting up a design-driven workflow. Now we’re ready to spice things up and add some fancy features to our new toolchain.

What we’ll be doing

The goal of this article is to extend your existing Storybook project by adding a few useful features for testing your components. Furthermore, we will be taking a look at more advanced techniques needed to test some aspects of a typical Angular project, like content projection.

For your convenience, I’ve prepared a GitHub repository that you can check out, if you don’t want to type all the examples in this post yourself. You can find the repository here: https://github.com/kaeku/storybook-workflow-blog-post

Making Components Accessible With Knobs

Now, let’s add that knobs feature we talked about previously. Knobs are a way for the user to interact with the component through the Storybook interface. I find this especially useful for the folks that just want to experiment and play around with the component without needing to launch the whole package (think testers, project/product managers, designers, etc.). But it’s also a powerful tool for developers to quickly throw some test data at the component, especially when trying to quickly reproduce bugs.  

We’ll start off by installing the knobs package in our project. Knobs are a Storybook addon, and can thus be easily added to any existing project.

Adding the package is as easy as typing npm i --save-dev @storybook/addon-knobs in the terminal of your choice. Once that went through, we need to tell Storybook to use this addon, as well. For this we’ll want to go to the addons.js file that was generated by the Storybook CLI. At the end of the file, just add the following line:

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

Now we’ve installed and loaded the knobs addon. That was easy, right? However, we still need to tell our stories about the newest addition to the family. So we go to our input.component.stories.ts file and add the following line right before our first story:

.addDecorator(withKnobs)

This spins up the knobs addon for all the stories we’re defining after it. Your file should now look like this:

import {storiesOf} from '@storybook/angular';
import {withKnobs, text, boolean} from '@storybook/addon-knobs';
import {InputComponent} from './input.component';

storiesOf('Input', module)
  .addDecorator(withKnobs)
  .add('Default', () => ({
    component: InputComponent
  }))
  .add('Disabled', () => ({
    component: InputComponent,
    props: {
      label: 'The disabled label',
      placeholder: 'The disabled placeholder',
      disabled: true
    }
  }));

If you paid close attention to your browser window, you’ll notice that nothing has changed yet, though. Bummer. That’s because we still have some work left to do. Because we might not want to configure each property and story will have a hard time guessing what value goes into the property, we will need to tell it about just those things. The knobs addon provides a range of different input types for you to choose from. We’ll start off with making our label configurable. So without further ado, let’s add a text knob to our story file:

label: text('Label', 'The disabled label', 'General'),

The text function takes three parameters:

  • the knob label
  • the knob default value
  • the knob group

These parameters are used to display the knob in the Storybook web interface. If everything went right, we should see a new tab called Knobs in your Storybook, showing off your very first knob:

Because we haven’t added any other category than General, this won’t show up just yet. Categories can be a good tool to organize your knobs, once the component gets more complex.

For our other two properties, placeholder and disabled we also add some knobs to turn (sorry, couldn’t resist! 😄):

placeholder: text('Placeholder', 'The disabled placeholder', 'General'),
disabled: boolean('Disabled', true, 'General')

The placeholder knob works just like the one for the label. The one for disabled, on the other hand, will result in a slightly different UI. The boolean function will create a checkbox to toggle between the two states. If all went well, we should now have our very own little component testbench:


We can now change around the values and see how the component reflects those changes in real time. Pretty neat, eh? Feel free to play around a bit with your newest toy! Once you’re ready to come back and build some more fancy stuff, take a look at the completed input.component.stories.ts file. I’ll be here!

import {storiesOf} from '@storybook/angular';
import {withKnobs, text, boolean} from '@storybook/addon-knobs';
import {InputComponent} from './input.component';

storiesOf('Input', module)
  .addDecorator(withKnobs)
  .add('Default', () => ({
    component: InputComponent,
    props: {
      label: text('Label', 'The label', 'General'),
      placeholder: text('Placeholder', 'The placeholder', 'General')
    }
  }))
  .add('Disabled', () => ({
    component: InputComponent,
    props: {
      label: text('Label', 'The label', 'General'),
      placeholder: text('Placeholder', 'The placeholder', 'General'),
      disabled: boolean('Disabled', true, 'General')
    }
  }));

Using Knobs To Test More Complex Data

Alright, now we’ve learned how to hand over data to our component and even further, we’re able to control this data in real time, with knobs. More often than not, though, your component will be needing more than simple strings or boolean values. But don’t worry, knobs have you covered. Let’s create a simple list component, where we render an array of strings:

import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss']
})
export class ListComponent {
  @Input() items: string[];
}

The respective template will be equally simple:

<ul>
  <li *ngFor="let item of items">
    {{ item }}
  </li>
</ul>

And last but not least, we’ll need a stories file to test our newly created component. This file will look very similar to the one we just created for our input component, with the one key difference being the object knob:

import {ListComponent} from './list.component';
import {storiesOf} from '@storybook/angular';
import {withKnobs, object} from '@storybook/addon-knobs';
import {listItems} from './list.component.mocks';

storiesOf('List', module)
  .addDecorator(withKnobs)
  .add('Default', () => ({
    component: ListComponent,
    props: {
      items: object('Items', listItems, 'General')
    }
  }));

You’ll probably notice that I’ve outsourced the list item mocks to a different file. While this is not really necessary at this point in time, we should remember that unit tests also need mock data in most cases. Having this data separated in a file is a good way to prepare your project for unit testing, at the cost of one more file. Sounds pretty good, right?

Our list.component.mocks.ts just exports a simple array of strings:

export const listItems = ['Foo', 'Bar', 'Baz'];

And that’s it! Once webpack did it’s job, you should see a new story, fresh off the presses, waiting to get tested:

In the items knob, you can now play around with the data, and the component should render those changes live once again. This is pretty cool, but let’s take this one step further. Arrays are nice and all, but what about those huge JSON objects your API’s been throwing at you this whole time? Not a problem, as well! To prove my point, I’ve created this humongous JSON: https://gist.github.com/kaeku/b9a17ac72f709a6ff288c41c5b5b67a7

100 lines of juicy JSON goodness! Let’s modify our list template, so it’s able to display the data:

<ul>
  <li *ngFor="let item of items">
    <span>
        {{ item.first_name }} {{ item.last_name }}
    </span>
    <ul>
      <li>
        {{ item.email }}
      </li>
      <li>
        {{ item.gender }}
      </li>
      <li>
        {{ item.ip_address }}
      </li>
    </ul>
  </li>
</ul>

And now throw the JSON data listed above right into your list.component.mocks.ts file. Once Storybook has finished building, it should look like this:


There we go. Some real data, rendered right there, waiting for even bigger datasets. Feel free to try out some even bigger JSON files! I took the data from https://mockaroo.com, with the default settings. I’ve tested up to 5000 rows of data on my machine, which worked just fine. Maybe you’ll dare to find the limits of what the UI can handle? 😄

Testing Projected Content

As of now, we’ve tested a simple input component, a list component with simple data and the same component with more complex data. A very common use-case for Angular applications is still missing, though: Content projection! Oftentimes, you’ll run into a container component that does some structural stuff, not knowing anything about its actual content. And this is exactly the kind of component we’re going to test now. For this, we will create a simple grid.component.ts, which sole purpose is to create a grid layout for its content, configurable by an @Input:

import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-grid',
  template: `
    <div class="grid" [ngStyle]="{'grid-template-columns': columnsTemplate}">
      <ng-content></ng-content>
    </div>

  `,
  styles: [`
    .grid {
      display: grid;
      grid-column-gap: 30px;
      grid-row-gap: 10px;
    }
  `]
})
export class GridComponent {
  private _columns = 2;

  @Input()
  public set columns(columns: number) {
    this._columns = columns;
  }

  public get columnsTemplate(): string {
    return `repeat(${this._columns}, 1fr)`;
  }
}

Defining this story we’ll be a bit different from what we’re used to, so far. Since we need to call the actual component we want to test, but also the components that go inside our grid, we need to make use of the Dynamic Module created by Storybook. The Dynamic Module is the Angular module used by Storybook to host our components in. Because Storybook itself is written in React, it needs the @storybook/angular adapter to give us the features from Angular that we know and love, like dependency injection for example. Luckily for us, though, the nice people building storybook have thought about what seems like almost everything.

For our example, we’ll need to tell Storybook about the components we’re going to use and define a template that will be used in the host component, provided by Storybook. That way we’ll be able to project content and wire up the input binding just like in any other Angular project.

We’re going to start with defining a template for our host component, containing the grid with its projected content:

import {moduleMetadata, storiesOf} from '@storybook/angular';
import {withKnobs, number} from '@storybook/addon-knobs';
import {GridComponent} from './grid.component';
import {InputComponent} from '../input/input.component';

const mockTemplate = `
  <app-grid [columns]="columns">
    <app-input></app-input>
    <app-input></app-input>
    <app-input></app-input>
  </app-grid>
`;

storiesOf('Grid', module)
  .addDecorator(withKnobs)
  .add('Default', () => ({
    component: GridComponent,
    props: {
      columns: number('Columns', '2', 'General')
    }
  }));

This is just a regular string, that will get passed into the template property of the host component’s decorator. We also already learned about knobs and props. No biggie. When we look at this in the browser, though, we’ll notice that something is missing. This is because we haven’t wired up the template to the hosting component yet, but this is an easy one:

import {moduleMetadata, storiesOf} from '@storybook/angular';
import {withKnobs, number} from '@storybook/addon-knobs';
import {GridComponent} from './grid.component';
import {InputComponent} from '../input/input.component';

const mockTemplate = `
  <app-grid [columns]="columns">
    <app-input></app-input>
    <app-input></app-input>
    <app-input></app-input>
  </app-grid>
`;

storiesOf('Grid', module)
  .addDecorator(withKnobs)
  .add('Default', () => ({
    component: GridComponent,
    props: {
      columns: number('Columns', '2', 'General')
    },
    template: mockTemplate,
  }));

We just add a line after the props, telling Storybook to use a custom template for this story. Once we check the browser again, we’ll see the following error:

Template parse errors: Can't bind to 'columns' since it isn't a known property of 'app-grid'. 1. If 'app-grid' is an Angular component and it has 'columns' input, then verify that it is part of this module.

What happened there? Well, because we specified this template for a component that is not part of our main Angular application, we need to declare the components we’re going to use in our template in the respective module. With the moduleMetadatafunction, we’re able to pass an options object to the Dynamic Module created by Storybook. We’ll wrap this in an addDecorator function, right after the withKnobs decorator:

.addDecorator(moduleMetadata({
      declarations: [GridComponent, InputComponent]
    }
  ))

Now we can see the component working correctly, with the content being projected inside the grid. When you change the value of the Columns-knob, it should also change the columns of the grid.

And there we go. Now we’ve covered the basics for most use-cases of a typical Angular application.

The “Convince Your Boss Kit”

So now you have created your very own Storybook. But what should you tell your boss to get him to greenlight this effort for your project? Here’s some reasons why you should be using Storybook in your project. Developing components with Storybook:

  • provides an easy way to test edge cases, which would usually be hard to reach and keep track of in your live application
  • lets you present different aspects of a feature in a clear and easy to understand manner once it’s deployed somewhere
  • enables you to test those components right away without being dependent on other business logic, data or component hierarchies
  • paves the way for unit testing, since you already have mock data and testable components at your disposal
  • gives everyone in the team a shared context to talk about.
  • makes reviewing features easy
  • helps you avoid regressions, especially when you implement automated testing
  • provides a common place for everyone in the team to experiment and play with the components
  • knobs make components easily accessible to the non-programmer members of your team

Wrapping Up

There goes the second part of this series. Look at you go, champ! You’ve created your very own Storybook instance and tuned it by adding knobs and projected content. You’re practically unstoppable and ripping out trees left and right. To calm you down a bit, we’ll talk about some infrastructure parts, like notes and source code, in part 3. Don’t worry, I’ll keep it short and simple – and I promise, this will be really useful once you roll this out to your team!

Have you tried this workflow and are keen to share your experiences? I’d love to hear from you on twitter!

 

Hi, my name is Andreas.

I'm a UX design consultant based in Germany, who's travelling most of his work-days to help his clients build a delightful user experience for their products.