storybook: [Angular] Can't seem to generate a story with ng-content and knob-passed props

I’m building out an Angular app with Storybook. I want my stories to have controllable knobs, but some of these components take ng-content.

I’m having trouble getting these two to work together, because, from what I’ve found, passing content into a component using Storybook involves setting a template on the story. Unfortunately, template seems to essentially overwrite Storybook’s knob-passed props.

Here’s the example:

button.component.ts

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

    @Component({
      selector: 'ui-button',
      templateUrl: './button.component.html',
      styleUrls: ['./button.component.scss']
    })
    export class ButtonComponent implements OnInit {
      types: String[] = [];
    
      constructor() {}
    
      ngOnInit(): void {}
    
      typeClasses(): String[] {
        return this.types.map(t => `button--${t}`);
      }
    }

button.component.html

    <a class="button" [ngClass]="typeClasses()">
      <ng-content></ng-content>
    </a>

button.component.stories.ts

    import { text, array } from '@storybook/addon-knobs';
    import { ButtonComponent } from './button.component';
    
    export default {
      title: 'ButtonComponent'
    };
    
    export const dark = () => ({
      moduleMetadata: {
        declarations: [ButtonComponent], // Not needed when not using template
        imports: []
      },
      // component: ButtonComponent, replaced with the below because of ng-content
      template: `<ui-button>Button content</ui-button>`, // Needed to pass arbitrary child content
      props: {
        types: array('class', ['dark']), // Ignored, because it's not in template
      }
    });

Am I missing a better way to pass content in? Because I have to give a full template, it seems that any props not passed in that template aren’t injected into the component, and so the knobs are rendered useless. This seems to mean that I should just get rid of props on all my component stories, and instead just pass them in through the template, but that would render them non-configurable in the served Storybook and defeat much of the point.

Am I doing this wrong? Is there a way to both A) pass content, and B) allow for props? The Angular Storybook guide doesn’t seem to address this.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 18
  • Comments: 36 (12 by maintainers)

Most upvoted comments

Any news?

LE: This seems to be working, discovered by pure luck. You can to be able to use inputs declared under props object.

export const Default = () => ({
    moduleMetadata: {
        declarations: [AppComponent],
    },
    props: {
        propInput: {
            foo: 1,
            bar: {
                baz: ["zxc"]
            }
        }
    },
    template: `<app-component [componentInput]="propInput"> Hello World </app-component>`,
});

Still this issue is not fixed. This is the smallest workaround I created:

import { componentWrapperDecorator, Meta, Story } from '@storybook/angular';
import { ButtonComponent } from './button.component';

export default {
  title: 'Components/Button',
  component: ButtonComponent,
  decorators: [
    componentWrapperDecorator((story) => {
      return story.replace('><', '>Button<');
    }),
  ],
} as Meta;

const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
  props: args,
});

export const Primary = Template.bind({});
Primary.args = {
  variant: 'primary',
};

In this example the content is provided to the component and all the controls are working properly. Not sure about Actions. For further improvement you can use helper function:

import { componentWrapperDecorator } from '@storybook/angular';

export const componentContentDecorator = (content: string) => {
  return componentWrapperDecorator((story) => {
    return story.replace('><', `>${content}<`);
  });
};

and then use it in decorators like that:

...
decorators: [
  componentContentDecorator('Button'),
],
...
const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
  props: {
    propInput: {
      size: args.size || "medium",
      primary: args.primary || false,
    }
  },
  template: `<ag-button [primary]="propInput.primary" [size]="propInput.size"> Hello World </ag-button>`,
});

The template: ... part can’t be right. Since you are using template literals your propInput references should be expressions and thus be written like this:

  template: `<ag-button [primary]="${propInput.primary}" [size]="${propInput.size}"> Hello World </ag-button>`,

I don’t understand why this worked out for you actually… Now hold your horses; For me this solution, with expressions, is still not working, unfortunately.

Wasn’t it better to just expose/create a property like ngContent besides the template and props properties on the story?

Ta-da!! I just released https://github.com/storybookjs/storybook/releases/tag/v6.2.0-alpha.13 containing PR #13507 that references this issue. Upgrade today to the @next NPM tag to try it out!

npx sb upgrade --prerelease

Closing this issue. Please re-open if you think there’s still more to do.

Hi gang, We’ve just released addon-controls in 6.0-beta!

Controls are portable, auto-generated knobs that are intended to replace addon-knobs long term.

Please upgrade and try them out today. Thanks for your help and support getting this stable for release!

I have no idea if this will be helpful to someone today, but I managed to get a component using <ng-content> working for with something similar to what’s recommended in the tuts and boilerplate examples using the Meta approach and all with this:

import { Meta, Story } from '@storybook/angular/types-6-0';
import ButtonComponent from './button.component';

export default {
  title: 'Example/Button',
  component: ButtonComponent,
} as Meta;

const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
  props: {
    propInput: {
      size: args.size || "medium",
      primary: args.primary || false,
    }
  },
  template: `<ag-button [primary]="propInput.primary" [size]="propInput.size"> Hello World </ag-button>`,
});

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  size: "large",
};

export const Secondary = Template.bind({});
Secondary.args = {
};

If you’re an Angular noob like I currently am, you’ll need to ensure your component has selector: ag-button or whatever name you want to be able to use that name for the web component looking markup.

I have 2 ideas for this issues

1️⃣

What do you think about adding a dedicated property for this case? something like componentTemplate ?

    export const dark = () => ({
      component: ButtonComponent,
      componentTemplate: `Button content`
      props: {
        types: array('class', ['dark']), 
      }
    });

componentTemplate is only taken into account if template is empty.

  • When only component is given storybook angular rendering generates something like <ui-button ...input ...output></ui-button> and declare component in Angular module componentTemplate will allow to add internal content to the component
  • When a template is given storybook angular rendering uses only template. And the component is only declared in the Angular module. This is why the componentTemplate would be ignored.

[Edit] no longer seems very relevant with https://github.com/storybookjs/storybook/pull/13383

2️⃣

use the Decorators? It is not yet taken into account by angular but I think it is possible (I am thinking of opening a PR for this subject)

if it could work the way I think it would. It could work something like this :

export const dark = () => ({
  template: `Button content`,
});
dark.decorators = [() => ({ component: ButtonComponent })];

(only theory 🙈 ) (several decorators can be nested whether, it is a template or a component ) would solve other issues than this one

👨‍💻

The 1st one seems to me quite simple and quick to set up. The 2nd one is longer and more complex. I think both could be considered and that it could be complementary. 🤔

@storybookjs/angular what do you think about it?

Some examples are missing in doc https://storybook.js.org/docs/angular/essentials/actions I don’t know if there is an issue but we can complete them. …

@ThibaudAV Thank you so much, I was looking for this.

@ThibaudAV i have the missing angular snippets almost ready. The only reason why they are not up on a pull request is based on a small item that i need to take up with @shilman that is applied to both angular and Vue. Once i get the ok will push them up. Sounds good?

Stay safe

normally yes

Hi everyone! Seems like there hasn’t been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don’t have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

For anybody who is interested in Controls but don’t know where to start, I’ve created a quick & dirty step-by-step walkthrough to go from a fresh CRA project to a working demo. Check it out:

=> Storybook Controls w/ CRA & TypeScript

There are also some “knobs to controls” migration docs in the Controls README:

=> How do I migrate from addon-knobs?

Any news on whether or not this is possible with Angular Storybook at the moment? Also created a Stack Overflow post and asked on the Storybook Discord, and haven’t gotten a sense of whether this is possible.