Tweak Angular components with content projection in Storybook
Tags: angular, storybook
Published: 2022-04-03 00:00:00

---
title: "Tweak Angular components with content projection in Storybook"
published: true
date: "2022-04-26"
draft: false
description: "Learn how to write stories for complex Angular components that use ng-content in their templates."
tags: 
  - angular
  - storybook
canonical_url: https://tsvetan.dev/blog/article/render-angular-components-with-ng-content-in-storybook/
---

Storybook is one of my favorite front-end tools nowadays and I'm advocating for its usage across all projects that I work on. While the Angular support is getting more refined with each new version, there are still tricky parts that require diving deep into the tools's documentation. One such scenario is creating stories for complex Angular components that support content projection (`ng-content`) and wiring them up with Storybook's control knobs.

## Set-up

You can skip this section if you already have Angular and Storybook integrated with each other.

Let's create a new Angular 13 project:

```bash
npx @angular/cli@13.3.0 new ng-content-storybook --style=scss --prefix=evil-corp --strict=true --routing=true
```

Then we can add the Storybook schematic:

```bash
npx sb init
```

This installs the latest Storybook version (at the time of writing 6.4). To check if the installation was successful, you can start the Storybook UI:

```bash
npm run storybook
```

If everything works you should see a bunch of demo components in the Storybook UI. From this point onward we don't need the auto-generated demo stories and components anymore - delete them with `rm -rf src/stories`.

## Creating a sample component with content projection

Let's create a simple checkbox component which can take advantage of content projection.

```bash
npx ng g c components/evil-corp-checkbox --prefix='' --inline-template --inline-style --skip-tests
```

```ts
// evil-corp-checkbox.component.ts
import { Component, Input } from "@angular/core";

@Component({
  selector: "evil-corp-checkbox",
  template: `
    <label>
      <input type="checkbox" [(ngModel)]="checked" />
      <span class="label">
        <ng-content></ng-content>
      </span>
    </label>
  `,
  styles: [
    `
      :host {
        .label {
          margin-left: 1ch;
        }
      }
    `,
  ],
})
export class EvilCorpCheckboxComponent {
  @Input()
  checked: boolean = false;
}
```

This is a simplified wrapper for the native HTML checkbox input with the ability to provide a label with `ng-content`. The component API looks like:

```html
<evil-corp-checkbox [checked]="true"> Check me! </evil-corp-checkbox>
```

## Create the component's story

Let's add a story for the checkbox component:

```ts
// evil-corp-checkbox.stories.ts
import { FormsModule } from "@angular/forms";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { EvilCorpCheckboxComponent } from "./evil-corp-checkbox.component";

export default {
  title: "Checkbox",
  component: EvilCorpCheckboxComponent,
  decorators: [
    moduleMetadata({
      imports: [FormsModule],
    }),
  ],
  argTypes: {
    checked: {
      control: "boolean",
    },
  },
  args: {
    checked: false,
  },
} as Meta<EvilCorpCheckboxComponent>;

export const Checked: StoryObj<EvilCorpCheckboxComponent> = {
  args: {
    checked: true,
  },
};

export const Unchecked: StoryObj<EvilCorpCheckboxComponent> = {
  args: {
    checked: false,
  },
};

export const WithLabel: StoryObj<EvilCorpCheckboxComponent> = {
  // ???
};
```

The story UI renders a boolean control knob for the `checked` `@Input` property of the component but we can't change the label yet. It's passed to the template as projected content and that requires special handling on our side.

A quick note: we are using the new Storybook story format CSF 3 in the examples since CSF 2 is going to be deprecated in Storybook 7.0 (the next major version at the time of writing). The main difference is that the story configuration is an object (`StoryObj`) instead of a function (`Story`/`StoryFn`). This makes creating and combining stories way easier. You can read more about the new format [here](https://storybook.js.org/blog/component-story-format-3-0/). We can achieve the same result demonstrated in the article with CSF 2 as well, so don't get discouraged if you're using that format in your existing project.

## Make the story support projected content

By default Storybook automatically maps the current values of the control knobs to the displayed component's properties. This works for the trivial cases where we don't use `ng-content` and we have all control knobs named the same way as our component's properties. To allow content projection for our checkbox, we must go a step further and manually render an Angular template in our story. This will offer us more flexibility in regard how Storybook's UI manipulates our component but will also mean we have to take care of some new caveats.

Let's update the story by adding - our own `render` method and a control knob (text field) for changing the checkbox label:

```ts
// evil-corp-checkbox.stories.ts
import { FormsModule } from "@angular/forms";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { EvilCorpCheckboxComponent } from "./evil-corp-checkbox.component";

type StoryType = EvilCorpCheckboxComponent & { label?: string };

export default {
  title: "Checkbox",
  component: EvilCorpCheckboxComponent,
  decorators: [
    moduleMetadata({
      imports: [FormsModule],
    }),
  ],

  render: (args) => {
    const { label, ...props } = args;

    return {
      props,
      template: `
        <evil-corp-checkbox [checked]="checked">
          ${label}
        </evil-corp-checkbox>
      `,
    };
  },

  argTypes: {
    checked: {
      control: "boolean",
    },

    label: {
      control: "text",
    },
  },
  args: {
    checked: false,
    label: "",
  },
} as Meta<StoryType>;

export const Checked: StoryObj<StoryType> = {
  args: {
    checked: true,
  },
};

export const Unchecked: StoryObj<StoryType> = {
  args: {
    checked: false,
  },
};

export const WithLabel: StoryObj<StoryType> = {
  args: {
    label: "I have read the terms and conditions.",
  },
};
```

The first change is the addition of a new custom type called `StoryType` which appends the `label` string field to the `EvilCorpCheckboxComponent` interface. We also use that new type as the generic argument for the `Meta` and `StoryObj` interfaces. This trick is useful when we have more Storybook UI controls than a component's `@Input` properties. In this case, the `label` is not part of the Angular component's interface but still we want to edit it via the Storybook control knobs.

We've also added the `label` property to the `argTypes` and `args` metadata fields. This allows us to edit the label's value and the component will automatically update itself with the current one.

The most notable change is the `render` method added to the default export of type `Meta`. It takes the values from the Storybook control knobs as a parameter and allows us to dynamically build a story metadata object. With these new powers we can render an Angular template that receives all its properties from the Storybook knobs. One gotcha is that we must provide all component `@Input`s by hand. Storybook magically took care of this before, but now that we've requested complete control over the rendering, we have to do it by ourselves. The `template` field contains an Angular template string which evaluates each time the function's arguments change. The `props` field in the return value of the `render` function binds its contents to the template's variables, for example:

```ts
// passing these props as argument to render()
{ checked: true, theAnswer: 42 }

// results in
component.checked = true
component.theAnswer = 42

// and now we can use them in an Angular template
<my-comp [checked]="checked" [theAnswer]="theAnswer"></my-comp>
```

The end result is a story that allows us to edit both the component's `@Input`s and its projected content.

![Screenshot of the Storybook story for the checkbox component](https://tsvetan.dev/images/blog/storybook-ng-content-label.png)

Knowing this allows us to showcase even the most complex Angular components in Storybook without losing control over all the different component aspects.