Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
sashafklein opened this issue Mar 31, 2020 · 36 comments
Closed

Comments

@sashafklein
Copy link

sashafklein commented Mar 31, 2020

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.

@sashafklein
Copy link
Author

sashafklein commented Apr 17, 2020

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.

@maplion
Copy link

maplion commented May 2, 2020

@sashafklein I was just introduced to Storybook and also ran into this roadblock pretty quickly.

@stale
Copy link

stale bot commented May 23, 2020

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!

@stale stale bot added the inactive label May 23, 2020
@shilman
Copy link
Member

shilman commented May 28, 2020

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!

@stale stale bot removed the inactive label May 28, 2020
@shilman
Copy link
Member

shilman commented Jun 1, 2020

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?

@stale
Copy link

stale bot commented Jun 26, 2020

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!

@stale stale bot added the inactive label Jun 26, 2020
@stale stale bot removed the inactive label Jun 26, 2020
@stale
Copy link

stale bot commented Jul 18, 2020

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!

@stale stale bot added the inactive label Jul 18, 2020
@ruspaull
Copy link

ruspaull commented Jul 23, 2020

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>`,
});

@stale stale bot removed the inactive label Jul 23, 2020
@stale
Copy link

stale bot commented Aug 16, 2020

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!

@stale stale bot added the inactive label Aug 16, 2020
@mhombach
Copy link

mhombach commented Sep 7, 2020

I also came across this problem just now.
I tried using the method @ruspaull mentioned but it doesn't work well. Some things like the action-events are not displayed and even though default-values are set, they are not changing when i use the knobs/inputs on the page.
I would really appreciate if we would have just another parameter for passing in some text into the content of the element.

@stale stale bot removed the inactive label Sep 7, 2020
@Marklb
Copy link
Member

Marklb commented Sep 10, 2020

There seems to be multiple points being brought up in this issue, but I think I get what it is mainly asking. I will try and break it down into what I think the different points are, but correct me if I am wrong.

1: I don't think this is about knobs really. More about the props that are either applied to the component inputs/outputs or the template context.

  • When setting the component property of the Story, the inputs/outputs in the props property of the Story will get set when rendering the component for the story.
  • When setting the template property of the Story, the props will be available in the template context, but you have to set where the go in the template yourself. I do think this is inconvenient, but below I will try to explain why I don't know what the right way to improve this is.

2: As for providing ng-content to the component, when not using template for the story, I don't think there is a way to do that currently. From what I can remember, Angular doesn't provide a way to dynamically set ng-content outside of a template, without some limitations, but I am not positive about that.

I was going to describe some solutions, but decided to just start an issue focused on improving the way args are passed to the component. That way it doesn't get mixed with bug solutions. #12438

@stale
Copy link

stale bot commented Oct 4, 2020

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!

@stale stale bot added the inactive label Oct 4, 2020
@shilman shilman added the PN label Oct 5, 2020
@stale stale bot removed the inactive label Oct 5, 2020
@amclin
Copy link

amclin commented Oct 14, 2020

Fundamentally, what's trying to be accomplished here with supporting ng-content is to do the same thing that React storybook supports with the React's native prop children

Controls vs Knobs is irrelevant if the template isn't being re-parsed when a storybook user changes the input values in their browser

@Marklb
Copy link
Member

Marklb commented Oct 15, 2020

It looks like ViewContainerRef.createComponent has a projectableNodes parameter that can be used to support ng-content. https://angular.io/api/core/ViewContainerRef#createcomponent

I don't know when I would be able to attempt this, since I am busy with work currently and have some other things I am already looking into in this project when I get time. I will put some info on what I think will work below, if anyone wants to try it, but I don't know if it will work or is how it should be done.

Since, I have never used the projectableNodes parameter, I don't know much about it. It's type is any[][] and there doesn't seem to really be any documentation about it, so I assume it will not be as simple as just passing a component to that parameter. From a few examples I saw when doing a quick search, it seems to take html nodes.

Storybook creates the Story component here with createComponent, so the content components could probably be created before that and their nativeElement passed to the projectableNodes parameter of their parent.

One thing I could see being a little difficult would be making sure change detection is triggered on the children correctly. There may be some library or examples for this that I didn't find with my fairly quick search though.

@ThibaudAV
Copy link
Contributor

ThibaudAV commented Dec 5, 2020

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 #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?

@daan93
Copy link

daan93 commented Dec 30, 2020

I am building a button component with <ng-content> and running into this issue now. I see the second option mentioned by @ThibaudAV is already in development, I hope this will also support my use case.

I am planning to create an icon component that can be used inside the button component. For this it would be ideal to pass the icon component with ng-content like this:

<app-button>
  <app-icon type="arrow-right"/>
  Click me
</app-button>

Would this use case be supported by the decorator solution?

@ThibaudAV
Copy link
Contributor

ThibaudAV commented Dec 30, 2020

What is in development changes a little from what I said above but not by a lot.

With what is currently in development you can define in export default ... the decorator for your main component (app-button). And then each story defines the template to be applied inside

see -> Exemple with ng-content #13507

if you have any other ideas or feedback on this pr, please do not hesitate =)

@daan93
Copy link

daan93 commented Dec 30, 2020

@ThibaudAV So then in a story I would be able to do this for nested components and have automatic controls for the main component? That would be awesome.

import IconComponent from '...';

...

export const WithButton = () => ({
  moduleMetadata: {
    declarations: [IconComponent],
  },  
  template: '<app-icon type="arrow-right"/> Click me',
});

@ThibaudAV
Copy link
Contributor

ThibaudAV commented Dec 30, 2020

normally yes

@daan93
Copy link

daan93 commented Dec 30, 2020

@mhombach I now have this working for changing the values with the controls on the page. Still looking into getting the actions getting logged though.

const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
  component: ButtonComponent,
  moduleMetadata: {
    declarations: [ButtonComponent],
  },    
  props: args,
  argTypes: { onClick: { action: 'clicked' } },
  template: `<app-button [type]="type" [size]="size" [icon]="icon">Click me</app-button>`,
});

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

@ThibaudAV
Copy link
Contributor

ThibaudAV commented Dec 30, 2020

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.

const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
  component: ButtonComponent, // <- also declare component in angular if necessary // <- this is deprecated should only be specified in `export default`
//  moduleMetadata: {
//    declarations: [ButtonComponent],
//  },    
  props: args,
  argTypes: { onClick: { action: 'clicked' } },
  template: `<app-button [type]="type" [size]="size" [icon]="icon" (onClick)="onClick($event)">Click me</app-button>`,
  // if you use template you have to manually add the output binding
  // but you might not add template and let storybook guess it 
});

export const Playground = Template.bind({});
Playground.args = {
  'type': 'primary',
};
Complete Exemple
import { action } from '@storybook/addon-actions';
import { Button } from '@storybook/angular/demo';

export default {
  component: Button,
  title: 'Addon/Actions',
};

export const ActionOnly = () => ({
  props: {
    text: 'Action only',
    onClick: action('log 1'),
  },
});

ActionOnly.storyName = 'Action only';

export const ActionAndMethod = () => ({
  props: {
    text: 'Action and Method',
    onClick: (e) => {
      console.log(e);
      e.preventDefault();
      action('log2')(e.target);
    },
  },
});

ActionAndMethod.storyName = 'Action and method';

export const ActionWithTemplate = () => ({
  template:
    '<storybook-button-component [text]="text" (onClick)="onClick($event)"></storybook-button-component>',
  props: {
    text: 'Action and Method',
    onClick: action('log 1'),
  },
});

ActionWithTemplate.storyName = 'Action with template';

export const ActionWithControl = (args) => ({
  props: args,
});
ActionWithControl.args = { text: 'Click' };
ActionWithControl.argTypes = { onClick: { action: 'clicked' } };
ActionWithControl.storyName = 'Action with args';

@jonniebigodes
Copy link
Contributor

@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

@daan93
Copy link

daan93 commented Dec 30, 2020

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.

@shilman
Copy link
Member

shilman commented Jan 13, 2021

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.

@shilman shilman closed this as completed Jan 13, 2021
@MickL
Copy link

MickL commented Feb 16, 2021

@shilman I tried out the latest alpha with your PR but I am not exactly sure how to create a component that contains multiple components:

<app-tabs>
  <app-tab tabTitle="my tab 1">
    Content 1
  </app-tab>
  <app-tab tabTitle="my tab 2">
    Content 2
  </app-tab>
</app-tabs>

@shilman
Copy link
Member

shilman commented Feb 17, 2021

cc @ThibaudAV

@ThibaudAV
Copy link
Contributor

ThibaudAV commented Feb 17, 2021

@MickL There are several ways to do this.

You can look at the examples of #13507
or a more complete one here : https://github.com/storybookjs/storybook/blob/next/examples/angular-cli/src/stories/core/decorators/componentWrapperDecorator/decorators.stories.ts

but in your case you will have to use a template to add the content :

  <app-tab tabTitle="my tab 1">
    Content 1
  </app-tab>
  <app-tab tabTitle="my tab 2">
    Content 2
  </app-tab>

of the component app-tabs

I hope this will help you :)

@roblevintennis
Copy link

roblevintennis commented May 14, 2021

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.

@mrpharderwijk
Copy link

mrpharderwijk commented May 29, 2021

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?

@mike-fam
Copy link

Any update on this?

@czemar
Copy link

czemar commented Jul 7, 2022

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'),
],
...

@PhilippMDoerner
Copy link

I can not confirm that this is broken. It worked just fine for me. For any future readers:

Example Component

<div>
  <h1>
    Heading: 
    <ng-content select="[heading]"></ng-content>
  </h1>
  
  <h5>Body: <ng-content select="[body]"></ng-content></h5>
  
</div>

Example story:

import { Meta, StoryFn, moduleMetadata } from '@storybook/angular';
import { ExampleComponent } from "./example.component";

export default {
  title: 'DesignSystem/Molecules/ExampleComponent',
  component: ExampleComponent,
  decorators: [
    moduleMetadata({
      imports: [
      ],
    }),
  ],
  args: {
  },
} as Meta<ExampleComponent>;

const Template: StoryFn<ExampleComponent> = (args: ExampleComponent) => ({ 
  props: {
    ...args,
  },
  template: `
    <app-example>
      <ng-container heading> Test Heading </ng-container>
      <ng-container body> Test Body </ng-container>
    </app-example>
  `
});

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

This renders as expected:
image

So I'm not sure what the issues are that others are seeing.

@shivarajnaidu
Copy link

Is there any straightforward way to work with content projection in storybook for angular components

ButtonComponent looks like below

<button class="btn">
  <ng-content></ng-content>
</button>

how to write story for this using storybook 7.x?

const meta: Meta<ButtonComponent> = {
  title: 'DS/Button',
  component: ButtonComponent,
  tags: ['autodocs'],
  render: (args: ButtonComponent) => ({
    props: {
      ...args,
    },
  }),
  argTypes: {
  },
};

@CapoD
Copy link

CapoD commented Jul 31, 2023

I am very curious if there was an easy way to set the content of a component, since this might be a common task.
Currently, I am using the solution below, but this solution requires me to configure all additional @Input() properties manually.

export default {
  title: 'Button',
  component: ButtonComponent,
  render: (args) => {
    const { ...props } = args;
    return {
      props,
      template: `
       <app-button>{{ngContent}}</app-button>
      `,
    };
  },
  parameters: {
    layout: 'centered',
  },
} as Meta<ButtonComponent>;

type Story = StoryObj<ButtonComponent>;

export const Default: Story = {
  name: 'Button',
  args: {
    ngContent: 'Click me!',
  },
};

@rafagarcialepper
Copy link

rafagarcialepper commented Aug 25, 2023

@shilman This issue isn't fixed.

@PhilippMDoerner

I can not confirm that this is broken. It worked just fine for me. For any future readers:

Example Component

<div>
  <h1>
    Heading: 
    <ng-content select="[heading]"></ng-content>
  </h1>
  
  <h5>Body: <ng-content select="[body]"></ng-content></h5>
  
</div>

Example story:

import { Meta, StoryFn, moduleMetadata } from '@storybook/angular';
import { ExampleComponent } from "./example.component";

export default {
  title: 'DesignSystem/Molecules/ExampleComponent',
  component: ExampleComponent,
  decorators: [
    moduleMetadata({
      imports: [
      ],
    }),
  ],
  args: {
  },
} as Meta<ExampleComponent>;

const Template: StoryFn<ExampleComponent> = (args: ExampleComponent) => ({ 
  props: {
    ...args,
  },
  template: `
    <app-example>
      <ng-container heading> Test Heading </ng-container>
      <ng-container body> Test Body </ng-container>
    </app-example>
  `
});

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

This renders as expected: image

So I'm not sure what the issues are that others are seeing.

That works, but if you want to customise args they are not added to the template as inputs.

@rafagarcialepper
Copy link

rafagarcialepper commented Aug 25, 2023

I've written a workaround that works for my use case and I think should probably work for more complicated cases, just a little function that takes args and returns an array with the attributes

const argsToAttrs = (args: ButtonComponent) => {
  let attrs = '';

  for (const key in args) {
    if (Object.prototype.hasOwnProperty.call(args, key)) {
      attrs += `${key}="${args[key]}" `;
    }
  }

  return attrs.trim();
}

const Template: Story<ButtonComponent> = (args: ButtonComponent) => {
  const attrs: string  = argsToAttrs(args);
  return {
    template: `<test-button ${attrs}>Button</test-button>`,
    props: args,
  }
}; 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests