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] How to render a component that has <ng-content> in the template #2817

Closed
benbrowning opened this issue Jan 23, 2018 · 21 comments
Closed

Comments

@benbrowning
Copy link

Issue details

I'm pretty new to Angular so this is probably a noob question. I have a button component that uses the tags to project content passed into it when it is used:

Component template:

<button class="button">
    <img src={{iconSrc}} *ngIf="iconSrc" class="icon" />
    <ng-content></ng-content>
</button>

but I can't figure out how to pass some content in via my story book story:

storiesOf('Button', module)
  .add('Simple', () => ({
    component: ButtonComponent,
    props: {},
  }))

Are there any docs describing the properties that can be used in the object that you return to describe a component? Have tried 'content', 'text' etc but to no avail.

@igor-dv
Copy link
Member

igor-dv commented Jan 24, 2018

Can you please try v3.4.0-alpha.5, it contains a "template" prop that you can use instead of the "component"

if your ButtonComponent selector is named button-component:

storiesOf('Button', module)
  .add('Simple', () => ({
    moduleMetadata: {
      declarations: [ButtonComponent],
    },
    template: `<button-component> Put your content here </button-component>`,
  }))

@benbrowning
Copy link
Author

@igor-dv Just the ticket. Thanks!

@omt66
Copy link

omt66 commented Jan 31, 2018

@igor-dv How do you add v3.4.0-alpha.5 to try it? Can you explain it please?

@igor-dv
Copy link
Member

igor-dv commented Jan 31, 2018

@omt66 , just install all the @storybook/* related dependencies with the "3.4.0-alpha.5" version. Am I missing something in your question?

@omt66
Copy link

omt66 commented Jan 31, 2018

@igor-dv sorry I tried that but I am getting some errors.

@igor-dv
Copy link
Member

igor-dv commented Jan 31, 2018

What are the errors ?

@drewbourne
Copy link

Using the template & moduleMetadata fields with 3.4.0-alpha.8 is working for me.

I got some errors because I had a custom webpack.config.js (from following the Storybook Angular guide) which appeared to duplicate the module rules for CSS so that that style-loader/css-loader are run twice and error out attempting to parse exports = module.exports = ... as CSS. Fix was to remove the .storybook/webpack.config.js.

@teebszet
Copy link

Can you please try v3.4.0-alpha.5, it contains a "template" prop that you can use instead of the "component"

if your ButtonComponent selector is named button-component:

storiesOf('Button', module)
  .add('Simple', () => ({
    moduleMetadata: {
      declarations: [ButtonComponent],
    },
    template: `<button-component> Put your content here </button-component>`,
  }))

Is this (should this be) in the docs?

@benkeil
Copy link

benkeil commented Aug 15, 2019

That couldn't be the solution. Why I should write the HTML manually again? It already in the component. We need a additional content property or something to include it in the component's HTML.

@neryams
Copy link

neryams commented Mar 13, 2020

This is really cumbersome. Now we have to declare the component three(!) redundant times in a MDX story. Once in the Meta, once in the Props, and once in the moduleMetadata.... And then add the html with the selector too. There has to be a better way...

@SarcevicAntonio
Copy link

SarcevicAntonio commented Feb 25, 2021

is this the "official way" to put some html inside a component?

because it's not really compatible with args therefore not with the whole controls addon, right?

@ThibaudAV
Copy link
Contributor

ThibaudAV commented Jun 2, 2021

You can use componentWrapperDecorator with component

see exemples here :

pseudo code for this exemple :

export default {
  title: 'Button',
  component: ButtonComponent,
  decorators: [
    componentWrapperDecorator(ButtonComponent, (args) => ({ iconSrc: args.iconSrc })),
  ],
  args: { iconSrc: '/icon.png', buttonValue: 'some value' },
} as Meta;

export const WithTemplate = (args) => ({
  template: `Button text {{ buttonValue }}`,
  props: {
    ...args
  },
});

@SarcevicAntonio
Copy link

while the approach of @ThibaudAV works it feels like a hack

and there are some issues

  • like having to define a template or else i will get my component nested inside itself (because its a hack)
  • or the fact i now can't set args per story but have to set them in the default export
  • and it also doesn't seem to play nice with the "show code" feature inside Docs-tab

There needs to be an actual solution, please! I'd love to use all these cool new features like the Docs Tab and Controls Tab but it feels like its usage with Angular was an afterthought.

@dexster
Copy link
Contributor

dexster commented Jun 22, 2021

@SarcevicAntonio You can have args per story. Just remove them from the default export and add them to the story

export default {
  title: 'Button',
  component: ButtonComponent,
  decorators: [
    componentWrapperDecorator(ButtonComponent)
} as Meta;

export const WithTemplate = (args) => ({
  template: `Button text {{ buttonValue }}`,
  props: {
    ...args
  },
});
WithTemplate.args = {
   buttonValue: 'Click me'
}

You can also move the componentWrapperDecorator out the default export to the story as well.

WithTemplate.decorators = [
    componentWrapperDecorator(ButtonComponent)
];

I am seeing my args in "Show code". Now if I could just see my ng-content there as well then this solution would be great

@anuj-scanova
Copy link

@dexster The solution is not working for ng-content with select. My ButtonComponent looks like

<button class="btn btn-primary">
  <ng-content select="[label]"></ng-content>
</button>

Now using your code example (without any modification), it does not render the text
Screen Shot 2022-10-01 at 10 32 21 PM

If I remove the select="[label]" from the ng-content, it works then, but the @Input() properties not working then.

@BigPackie
Copy link

BigPackie commented Oct 13, 2022

I am using typescript for stories and had to adjust @ThibaudAV and @dexster solution otherwise the component would not reflect the changes to angular component @Input() when changing the arguments in storybook Control tab or when setting them in code args={}.

//component file, not full code

@Component({
  selector: 'a[myLink]',
  template: `<ng-content></ng-content>`,
  styleUrls: ['./my-link.component.scss'],
})
export class MyLinkComponent {
  @Input() set myLink(variant: LinkVariant | '') {
    if (variant !== '') {
      this.variant = variant;
    }
  }

  private variant: LinkVariant = 'primary';
  @Input() size: LinkTextSize = '100';
  @Input() bold = false;

  @HostBinding('class')
  private get classes() {
    return [
      `text${this.bold ? '-bold' : ''}${'-' + this.size}`,
      `link-class-${this.variant}`,
    ].join(' ');
  }
}
//story file, not full code

export default {
  title: 'ng-Components/Link',
  component: MyLinkComponent,
  decorators: [
    moduleMetadata({
      imports: [MyLinkModule], //component depends on some other components, so it has it's own module
    }),
    componentWrapperDecorator(MyLinkComponent, ({ args }) => {  //object destructuring was needed here '{args}' 
      return args;
    }),
  ],
} as Meta;

const Template: Story<MyLinkComponent | { ngContent: unknown }> = (args) => ({
  template: `Hello {{ ngContent }}`,  // whole content of this string with replaced ngContent will be correctly inserted, e.g. 'Hello World!'
  props: {
    ...args,
  },
});
export const Default = Template.bind({});
Default.args = {
  bold: true,
  myLink: 'secondary',
  ngContent: 'World!',
};

Usage example of the component:

<a href="google.com" myLink="secondary" [bold]="true" >  Hello World! </a>

This might help to some people, although there is a limitation to this approach - it is not possible to set attributes native to the <a> HTML element,for example href, unless it is exposed and wrapped through component's @Input().

So for a component that is using an attribute selector you cannot modify its native attribute in stroybook using this approach, for that you must use template @igor-dv .

I wonder whether there is a nice solution that supports <ng-content> with @Input() and also native element attrbiutes for stories. Please anybody, let me know.

@araymond11
Copy link

araymond11 commented Feb 22, 2023

@dexster Did you find any solution to make it work with ng-content and select ? I'm facing the same issue as you.

Thanks !

@Klapik
Copy link

Klapik commented May 25, 2023

Any news?

@PKHDK
Copy link

PKHDK commented Jun 20, 2023

Oh my.. by using the template solution all the control inputs don't do ANYTHING. At least showing them works as @dexster suggested... but these args/controls are useless since the inline template html code is now the "control" but outsiders that can't programm won't be able to quickly change the input properties for a component... which makes using storybook as a whole completely unnecessary
grafik

There needs to be a better solution, please! I need to know if there is at least some hope or plans to this problem or if I have to shut down storybook in my company immediately before we waste too much time on it... very sad

Edit: Ok, I did miss something... it can work if you write the template code like this
grafik
I made the mistake to write [hasFooter]="args.hasFooter"...
So the controls now at least control what the template shows

@maximLyakhov
Copy link

maximLyakhov commented Sep 5, 2023

Thanks @BigPackie! Your solution is the cleanest out there. Used like this:

export default {
  title: 'Button',
  component: ButtonComponent,
  args: {size: 'medium', color: 'default', disabled: false},
  decorators: [componentWrapperDecorator(ButtonComponent, ({args}) => args)],
  render: (args: ButtonComponent) => ({props: args, template: 'ng-content'}),
} as Meta<ButtonComponent>
Screenshot 2023-09-05 at 13 48 20

@ChadiEM
Copy link

ChadiEM commented Sep 5, 2023

For ng-content, I used a decorator and it worked perfectly.

Decorator code

export const withNgContent =
  <TArgs = any>(ngContent: string): DecoratorFunction<AngularRenderer, TArgs> =>
  (storyFn, storyContext) => {
    const story = storyFn();
    const metadata = reflectComponentType(storyContext.component);

    const selector = `</${metadata.selector}>`;
    const templateUpdated = story.template.replace(
      selector,
      `${ngContent}${selector}`
    );

    return {
      ...story,
      template: templateUpdated,
    };
  };

Usage

  decorators: [
    ...
    withNgContent('<p>My content</p>'),
  ],

You don't need a render anymore :-)

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