diff --git a/app/angular/src/client/preview/angular-beta/StorybookModule.test.ts b/app/angular/src/client/preview/angular-beta/StorybookModule.test.ts index a05fbf67c14..900f2d1b7a4 100644 --- a/app/angular/src/client/preview/angular-beta/StorybookModule.test.ts +++ b/app/angular/src/client/preview/angular-beta/StorybookModule.test.ts @@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, NgModule, Output, Type } from '@angular import { TestBed } from '@angular/core/testing'; import { BehaviorSubject } from 'rxjs'; +import { ICollection } from '../types'; import { getStorybookModuleMetadata } from './StorybookModule'; describe('StorybookModule', () => { @@ -132,7 +133,7 @@ describe('StorybookModule', () => { ); }); - it('should not override outputs if storyProps$ Subject emit', async () => { + it('should override outputs if storyProps$ Subject emit', async () => { let expectedOutputValue; let expectedOutputBindingValue; const initialProps = { @@ -155,10 +156,10 @@ describe('StorybookModule', () => { const newProps = { input: 'new input', output: () => { - expectedOutputValue = 'should not be called'; + expectedOutputValue = 'should be called'; }, outputBindingPropertyName: () => { - expectedOutputBindingValue = 'should not be called'; + expectedOutputBindingValue = 'should be called'; }, }; storyProps$.next(newProps); @@ -168,8 +169,43 @@ describe('StorybookModule', () => { fixture.nativeElement.querySelector('p#outputBindingPropertyName').click(); expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(newProps.input); - expect(expectedOutputValue).toEqual('outputEmitted'); - expect(expectedOutputBindingValue).toEqual('outputEmitted'); + expect(expectedOutputValue).toEqual('should be called'); + expect(expectedOutputBindingValue).toEqual('should be called'); + }); + + it('should change template inputs if storyProps$ Subject emit', async () => { + const initialProps = { + color: 'red', + input: 'input', + }; + const storyProps$ = new BehaviorSubject(initialProps); + + const ngModule = getStorybookModuleMetadata( + { + storyFnAngular: { + props: initialProps, + template: '

', + }, + parameters: { component: FooComponent }, + }, + storyProps$ + ); + const { fixture } = await configureTestingModule(ngModule); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('p').style.color).toEqual('red'); + expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual( + initialProps.input + ); + + const newProps = { + color: 'black', + input: 'new input', + }; + storyProps$.next(newProps); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('p').style.color).toEqual('black'); + expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(newProps.input); }); }); }); diff --git a/app/angular/src/client/preview/angular-beta/StorybookWrapperComponent.ts b/app/angular/src/client/preview/angular-beta/StorybookWrapperComponent.ts index 2cae9a8ae44..0e3d850ea4f 100644 --- a/app/angular/src/client/preview/angular-beta/StorybookWrapperComponent.ts +++ b/app/angular/src/client/preview/angular-beta/StorybookWrapperComponent.ts @@ -48,19 +48,30 @@ export const createStorybookWrapperComponent = ( styles, }) class StorybookWrapperComponent implements AfterViewInit, OnDestroy { - private storyPropsSubscription: Subscription; + private storyComponentPropsSubscription: Subscription; + + private storyWrapperPropsSubscription: Subscription; @ViewChild(storyComponent ?? '', { static: true }) storyComponentElementRef: ElementRef; @ViewChild(storyComponent ?? '', { read: ViewContainerRef, static: true }) storyComponentViewContainerRef: ViewContainerRef; + // eslint-disable-next-line no-useless-constructor constructor( @Inject(STORY_PROPS) private storyProps$: Subject, private changeDetectorRef: ChangeDetectorRef - ) { - // Initializes Inputs/Outputs values - Object.assign(this, initialProps); + ) {} + + ngOnInit(): void { + // Subscribes to the observable storyProps$ to keep these properties up to date + this.storyWrapperPropsSubscription = this.storyProps$.subscribe((storyProps = {}) => { + // All props are added as component properties + Object.assign(this, storyProps); + + this.changeDetectorRef.detectChanges(); + this.changeDetectorRef.markForCheck(); + }); } ngAfterViewInit(): void { @@ -81,7 +92,7 @@ export const createStorybookWrapperComponent = ( this.changeDetectorRef.detectChanges(); // Once target component has been initialized, the storyProps$ observable keeps target component inputs up to date - this.storyPropsSubscription = this.storyProps$ + this.storyComponentPropsSubscription = this.storyProps$ .pipe( skip(1), map((props) => { @@ -128,8 +139,11 @@ export const createStorybookWrapperComponent = ( } ngOnDestroy(): void { - if (this.storyPropsSubscription != null) { - this.storyPropsSubscription.unsubscribe(); + if (this.storyComponentPropsSubscription != null) { + this.storyComponentPropsSubscription.unsubscribe(); + } + if (this.storyWrapperPropsSubscription != null) { + this.storyWrapperPropsSubscription.unsubscribe(); } } } diff --git a/app/angular/src/client/preview/angular/decorators.ts b/app/angular/src/client/preview/angular/decorators.ts index f66287ced29..7ef8ad9a55f 100644 --- a/app/angular/src/client/preview/angular/decorators.ts +++ b/app/angular/src/client/preview/angular/decorators.ts @@ -1,9 +1,9 @@ /* eslint-disable no-param-reassign */ import { Type } from '@angular/core'; -import { DecoratorFunction } from '@storybook/addons'; +import { DecoratorFunction, StoryContext } from '@storybook/addons'; import { computesTemplateFromComponent } from '../angular-beta/ComputesTemplateFromComponent'; import { isComponent } from '../angular-beta/utils/NgComponentAnalyzer'; -import { NgModuleMetadata, StoryFnAngularReturnType } from '../types'; +import { ICollection, NgModuleMetadata, StoryFnAngularReturnType } from '../types'; export const moduleMetadata = (metadata: Partial) => (storyFn: () => any) => { const story = storyFn(); @@ -26,13 +26,26 @@ export const moduleMetadata = (metadata: Partial) => (storyFn: }; export const componentWrapperDecorator = ( - element: Type | ((story: string) => string) -): DecoratorFunction => (storyFn) => { + element: Type | ((story: string) => string), + props?: ICollection | ((storyContext: StoryContext) => ICollection) +): DecoratorFunction => (storyFn, storyContext) => { const story = storyFn(); + const currentProps = typeof props === 'function' ? (props(storyContext) as ICollection) : props; const template = isComponent(element) - ? computesTemplateFromComponent(element, {}, story.template) + ? computesTemplateFromComponent(element, currentProps ?? {}, story.template) : element(story.template); - return { ...story, template }; + return { + ...story, + template, + ...(currentProps || story.props + ? { + props: { + ...currentProps, + ...story.props, + }, + } + : {}), + }; }; diff --git a/app/angular/src/client/preview/decorateStory.test.ts b/app/angular/src/client/preview/decorateStory.test.ts index 57ec7957338..f7960f9eaab 100644 --- a/app/angular/src/client/preview/decorateStory.test.ts +++ b/app/angular/src/client/preview/decorateStory.test.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, Input, Output } from '@angular/core'; import { DecoratorFunction, StoryContext } from '@storybook/addons'; import { componentWrapperDecorator } from './angular/decorators'; @@ -7,6 +7,77 @@ import { StoryFnAngularReturnType } from './types'; describe('decorateStory', () => { describe('angular behavior', () => { + it('should use componentWrapperDecorator with args', () => { + const decorators: DecoratorFunction[] = [ + componentWrapperDecorator(ParentComponent, ({ args }) => args), + componentWrapperDecorator( + (story) => `${story}`, + ({ args }) => args + ), + componentWrapperDecorator((story) => `${story}`), + ]; + const decorated = decorateStory(() => ({ template: '' }), decorators); + + expect( + decorated( + makeContext({ + parameters: { component: FooComponent }, + args: { + parentInput: 'Parent input', + grandparentInput: 'grandparent input', + parentOutput: () => {}, + }, + }) + ) + ).toEqual({ + props: { + parentInput: 'Parent input', + grandparentInput: 'grandparent input', + parentOutput: expect.any(Function), + }, + template: + '', + }); + }); + + it('should use componentWrapperDecorator with input / output', () => { + const decorators: DecoratorFunction[] = [ + componentWrapperDecorator(ParentComponent, { + parentInput: 'Parent input', + parentOutput: () => {}, + }), + componentWrapperDecorator( + (story) => `${story}`, + { + grandparentInput: 'Grandparent input', + sameInput: 'Should be override by story props', + } + ), + componentWrapperDecorator((story) => `${story}`), + ]; + const decorated = decorateStory( + () => ({ template: '', props: { sameInput: 'Story input' } }), + decorators + ); + + expect( + decorated( + makeContext({ + parameters: { component: FooComponent }, + }) + ) + ).toEqual({ + props: { + parentInput: 'Parent input', + parentOutput: expect.any(Function), + grandparentInput: 'Grandparent input', + sameInput: 'Story input', + }, + template: + '', + }); + }); + it('should use componentWrapperDecorator', () => { const decorators: DecoratorFunction[] = [ componentWrapperDecorator(ParentComponent), @@ -249,4 +320,10 @@ class FooComponent {} selector: 'parent', template: ``, }) -class ParentComponent {} +class ParentComponent { + @Input() + parentInput: string; + + @Output() + parentOutput: any; +} diff --git a/examples/angular-cli/src/stories/decorators/__snapshots__/decorators.stories.storyshot b/examples/angular-cli/src/stories/decorators/__snapshots__/decorators.stories.storyshot index afbd54c2bc1..afda93eff15 100644 --- a/examples/angular-cli/src/stories/decorators/__snapshots__/decorators.stories.storyshot +++ b/examples/angular-cli/src/stories/decorators/__snapshots__/decorators.stories.storyshot @@ -39,6 +39,86 @@ exports[`Storyshots Core / Decorators With Component 1`] = ` `; +exports[`Storyshots Core / Decorators With Component Decorator And Args 1`] = ` + + Grandparent
+ + Parent +
+ Input text: +
+ Output : + +
+
+ + Child +
+ Input text: Child text +
+ Output : + +
+ Private text: Child private text +
+
+
+
+
+
+`; + +exports[`Storyshots Core / Decorators With Component Decorator And Props 1`] = ` + + Grandparent
+ + Parent +
+ Input text: Parent text +
+ Output : + +
+
+ + Child +
+ Input text: Child text +
+ Output : + +
+ Private text: Child private text +
+
+
+
+
+
+`; + exports[`Storyshots Core / Decorators With Component Wrapper Decorator 1`] = ` Grandparent
`; +exports[`Storyshots Core / Decorators With Component Wrapper Decorator And Args 1`] = ` + + Grandparent
+ + Parent +
+ Input text: +
+ Output : + +
+
+ + Child +
+ Input text: Child text +
+ Output : + +
+ Private text: Child private text +
+
+
+
+
+
+`; + +exports[`Storyshots Core / Decorators With Component Wrapper Decorator And Props 1`] = ` + + Grandparent
+ + Parent +
+ Input text: Parent text +
+ Output : + +
+
+ + Child +
+ Input text: Child text +
+ Output : + +
+ Private text: Child private text +
+
+
+
+
+
+`; + exports[`Storyshots Core / Decorators With Custom Decorator 1`] = ` Grandparent
({ @@ -46,6 +47,40 @@ WithComponentWrapperDecorator.decorators = [ componentWrapperDecorator(ParentComponent), ]; +export const WithComponentWrapperDecoratorAndProps = (args) => ({ + component: ChildComponent, + props: { + ...args, + }, +}); +WithComponentWrapperDecoratorAndProps.decorators = [ + moduleMetadata({ declarations: [ParentComponent] }), + componentWrapperDecorator(ParentComponent, { + parentText: 'Parent text', + onClickParent: () => { + console.log('onClickParent'); + }, + }), +]; + +export const WithComponentWrapperDecoratorAndArgs = (args) => ({ + component: ChildComponent, + props: { + ...args, + }, +}); +WithComponentWrapperDecoratorAndArgs.argTypes = { + parentText: { control: { type: 'text' } }, + onClickParent: { action: 'onClickParent' }, +}; +WithComponentWrapperDecoratorAndArgs.decorators = [ + moduleMetadata({ declarations: [ParentComponent] }), + componentWrapperDecorator(ParentComponent, ({ args }) => ({ + parentText: args.parentText, + onClickParent: args.onClickParent, + })), +]; + export const WithCustomDecorator = (args) => ({ template: `Child Template`, props: {