question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Angular: Overwriting Input/Output properties of base component leads to incomplete metadata

See original GitHub issue

Describe the bug StorybookWrapperComponent does not correctly get @Input and @Output properties from an inherited parent component. This can lead to them getting overwritten and behaving unexpectedly.

As far as I was able to recreate this issue it’s actually a bug in how Angular handles it’s metadata. Since I don’t have high hopes that this gets fixed anytime soon in Angular is there interest in fixing this via a workaround in storybook?

I have mitigated this issue in my local branch by iterating and merging the parent prop metadata:

export const getComponentPropsDecoratorMetadata = (component: any) => {
  const decoratorKey = '__prop__metadata__';
  let propsDecorators: Record<string, (Input | Output)[]> =
    Reflect &&
    Reflect.getOwnPropertyDescriptor &&
    Reflect.getOwnPropertyDescriptor(component, decoratorKey)
      ? Reflect.getOwnPropertyDescriptor(component, decoratorKey).value
      : component[decoratorKey];

  // START WORKAROUND
  const parent = Reflect && Reflect.getPrototypeOf && Reflect.getPrototypeOf(component);

  if (parent) {
    const parentPropsDecorators = getComponentPropsDecoratorMetadata(parent);

    propsDecorators = { ...parentPropsDecorators, ...propsDecorators };
  }
  // END WORKAROUND

  return propsDecorators;
};

Please let me know if we should go down this route then I will create a PR with my fix.

To Reproduce

Add these two files anywhere in the angular example in the repo:

test.component.ts
// eslint-disable-next-line max-classes-per-file
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'hadv-storybook-test',
  template: '',
})
export class BaseTestComponent {
  private _value?: string;

  @Input()
  public get value(): string | undefined {
    return this._value;
  }

  public set value(value: string | undefined) {
    this.writeValue(value);
  }

  @Output()
  public get valueChange(): EventEmitter<string | undefined> {
    return this.#valueChange;
  }

  #valueChange = new EventEmitter<string | undefined>();

  public writeValue(value: string | undefined) {
    this._value = value;
    this.valueChange.emit(value);
  }
}

@Component({
  selector: 'storybook-test',
  template: '{{ value }} <button (click)="changeValue()">Set Values</button>',
})
export class TestComponent extends BaseTestComponent {
  @Input()
  public get value(): string | undefined {
    return `${super.value} test`;
  }

  public set value(value: string | undefined) {
    super.value = value;
  }

  public changeValue(): void {
    this.value = (Math.random() * 100).toFixed(0);
  }
}
test.component.stories.ts
import { Story } from '@storybook/angular';

import { TestComponent } from './test.component';

export default {
  title: 'Base/Test',
  component: TestComponent,
  argTypes: { valueChange: { action: 'valueChange' } },
};

export const Base: Story = (args: any) => ({
  props: args,
  template: `
    <storybook-test value="${args.value}"></storybook-test>
  `,
});

Base.args = {
  value: '2',
};

System Please paste the results of npx sb@next info here.

Additional context Add any other context about the problem here.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:15 (9 by maintainers)

github_iconTop GitHub Comments

2reactions
stefan-schweigercommented, Aug 3, 2021

@Olusha ok I’ve now tried to recreate your issue with this story:

child.component.stories.ts
import { AfterViewInit, Component, Directive, Input } from "@angular/core";
import { Story, Meta, ArgTypes } from "@storybook/angular";

@Directive()
abstract class AbstractParent implements AfterViewInit {
  @Input() getLabel: () => string;

  ngAfterViewInit(): void {
    // getLabel is not initialized here if running storybook
    console.log("ngAfterViewInit", this.getLabel && this.getLabel());
    this.loadLabel();
  }

  abstract loadLabel(): void;
}

@Component({
  selector: "app-child",
  template: `childInput: {{ childInput }}<br />
    getLabel: {{ getLabel && getLabel() }}<br />
    getLabelChild: {{ getLabelChild && getLabelChild() }}<br /> `,
})
class ChildComponent extends AbstractParent implements AfterViewInit {
  @Input() childInput: string;

  @Input() getLabelChild: () => string;

  constructor() {
    super();
  }

  loadLabel(): void {
    // getLabel is not initialized here if running storybook
    console.log("LABEL", this.getLabel && this.getLabel());
    console.log("LABEL", this.getLabelChild && this.getLabelChild());
  }
}

export default {
  title: "Example/ChildComponent",
  component: ChildComponent,
} as Meta;

export const Base: Story<ChildComponent> = (args: ChildComponent) => ({
  props: args,
});

Base.args = {
  getLabel: () => 'Test',
  getLabelChild: () => 'Test 2'
};

But the behavior is exactly the same with 6.3 and 6.4-alpha.22. So my PR didn’t fix your specific issue but at least it didn’t cause it.

It’s pretty easy to get the storybook source code running and trying to debug the issue yourself. I’m not a member of the team but PRs are always welcome. Else I would suggest creating a separate bug report, as I think those issues are at least a bit different.

0reactions
Olushacommented, Aug 4, 2021

@shilman , yes, my first issue with Input property inheritance is fixed in the 6.4-alpha.23, but the second issue I described is still there. The problem occurs if I try to send a function that returns the object as an arg to the story. If primitive value is return by the function, everything works as expected. Link to the sandbox (https://stackblitz.com/edit/angular-ivy-lo5uhk?file=src/app/child/child.component.ts)

  component: AbstractComponent,
  props: args,
});

export const Primary = Template.bind({});
Primary.args = {
  getLabel: () => {
    return  of(1);
  },
  ownFn: () => {
    return  of(1);
  },
};
Read more comments on GitHub >

github_iconTop Results From Across the Web

Angular: Overwriting Input/Output properties of base ...
I have mitigated this issue in my local branch by iterating and merging the parent prop metadata: export const getComponentPropsDecoratorMetadata = (component: ...
Read more >
Input and other decorators and inheritance - angular
Decorators are not inherited. They need to be applied to the class used as component directly. Decorators on subclasses are ignored. I have...
Read more >
Component Inheritance in Angular - Bits and Pieces
Learn how to write code efficiently using component inheritance ... Run the following command to create the base component:
Read more >
Sharing data between child and parent directives ... - Angular
The @Input() decorator in a child component or directive signifies that the property can receive its value from its parent component.
Read more >
User Guide - Black Duck - Synopsys
additional metadata such as license, vulnerability, and project health for those components. Component. Scanning lets users use the scanner to scan software ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found