diff --git a/app/angular/index.d.ts b/app/angular/index.d.ts index 09fc1b7c611f..1e067cc761f3 100644 --- a/app/angular/index.d.ts +++ b/app/angular/index.d.ts @@ -1,4 +1,5 @@ import { NgModuleMetadata, ICollection } from './dist/client/preview/angular/types'; +export { moduleMetadata } from './dist/client/preview/angular/decorators'; export interface IStorybookStory { name: string; diff --git a/app/angular/src/client/index.js b/app/angular/src/client/index.js index 925ffed29bb3..e2e3fc59d028 100644 --- a/app/angular/src/client/index.js +++ b/app/angular/src/client/index.js @@ -1 +1,3 @@ export { storiesOf, setAddon, addDecorator, configure, getStorybook } from './preview'; + +export { moduleMetadata } from './preview/angular/decorators'; diff --git a/app/angular/src/client/preview/angular/decorators.test.ts b/app/angular/src/client/preview/angular/decorators.test.ts new file mode 100644 index 000000000000..2fa5b8d7ac06 --- /dev/null +++ b/app/angular/src/client/preview/angular/decorators.test.ts @@ -0,0 +1,99 @@ +import { moduleMetadata } from './decorators'; +import { addDecorator, storiesOf, clearDecorators, getStorybook } from '..'; + +class MockModule {} +class MockModuleTwo {} +class MockService {} +class MockComponent {} + +describe('moduleMetadata', () => { + it('should add metadata to a story without it', () => { + const result = moduleMetadata({ + imports: [MockModule], + providers: [MockService], + })(() => ({ + component: MockComponent, + })); + + expect(result).toEqual({ + component: MockComponent, + moduleMetadata: { + declarations: [], + entryComponents: [], + imports: [MockModule], + schemas: [], + providers: [MockService], + }, + }); + }); + + it('should combine with individual metadata on a story', () => { + const result = moduleMetadata({ + imports: [MockModule], + })(() => ({ + component: MockComponent, + moduleMetadata: { + imports: [MockModuleTwo], + providers: [MockService], + }, + })); + + expect(result).toEqual({ + component: MockComponent, + moduleMetadata: { + declarations: [], + entryComponents: [], + imports: [MockModule, MockModuleTwo], + schemas: [], + providers: [MockService], + }, + }); + }); + + it('should return the original metadata if passed null', () => { + const result = moduleMetadata(null)(() => ({ + component: MockComponent, + moduleMetadata: { + providers: [MockService], + }, + })); + + expect(result).toEqual({ + component: MockComponent, + moduleMetadata: { + declarations: [], + entryComponents: [], + imports: [], + schemas: [], + providers: [MockService], + }, + }); + }); + + it('should work when added globally', () => { + const metadata = { + declarations: [MockComponent], + providers: [MockService], + entryComponents: [MockComponent], + imports: [MockModule], + }; + + addDecorator(moduleMetadata(metadata)); + + storiesOf('Test', module).add('Default', () => ({ + component: MockComponent, + })); + + const [storybook] = getStorybook(); + + expect(storybook.stories[0].render().moduleMetadata).toEqual({ + declarations: [MockComponent], + providers: [MockService], + entryComponents: [MockComponent], + imports: [MockModule], + schemas: [], + }); + + clearDecorators(); + }); +}); diff --git a/app/angular/src/client/preview/angular/decorators.ts b/app/angular/src/client/preview/angular/decorators.ts new file mode 100644 index 000000000000..7897d6f0c380 --- /dev/null +++ b/app/angular/src/client/preview/angular/decorators.ts @@ -0,0 +1,21 @@ +import { NgModuleMetadata } from './types'; + +export const moduleMetadata = (metadata: Partial) => (storyFn: () => any) => { + const story = storyFn(); + const storyMetadata = story.moduleMetadata || {}; + metadata = metadata || {}; + + return { + ...story, + moduleMetadata: { + declarations: [...(metadata.declarations || []), ...(storyMetadata.declarations || [])], + entryComponents: [ + ...(metadata.entryComponents || []), + ...(storyMetadata.entryComponents || []), + ], + imports: [...(metadata.imports || []), ...(storyMetadata.imports || [])], + schemas: [...(metadata.schemas || []), ...(storyMetadata.schemas || [])], + providers: [...(metadata.providers || []), ...(storyMetadata.providers || [])], + }, + }; +}; diff --git a/app/angular/src/client/preview/index.d.ts b/app/angular/src/client/preview/index.d.ts new file mode 100644 index 000000000000..ef89ba61ed6a --- /dev/null +++ b/app/angular/src/client/preview/index.d.ts @@ -0,0 +1,6 @@ +export function storiesOf(kind: string, module: NodeModule): IApi; +export function setAddon(addon: any): void; +export function addDecorator(decorator: any): IApi; +export function configure(loaders: () => NodeRequire, module: NodeModule): void; +export function getStorybook(): IStoribookSection[]; +export function clearDecorators(): void; diff --git a/docs/src/pages/basics/guide-angular/index.md b/docs/src/pages/basics/guide-angular/index.md index 8592774aca4e..73a7f86194dd 100644 --- a/docs/src/pages/basics/guide-angular/index.md +++ b/docs/src/pages/basics/guide-angular/index.md @@ -113,3 +113,55 @@ npm run storybook Now you can change components and write stories whenever you need to. You'll get those changes into Storybook in a snap with the help of webpack's HMR API. + +## Module Metadata + +If your component has dependencies on other Angular directives and modules, these can be supplied using the `moduleMetadata` property on an individual story: + +```js +import { CommonModule } from '@angular/common'; +import { storiesOf } from '@storybook/angular'; +import { MyButtonComponent } from '../src/app/my-button/my-button.component'; +import { MyPanelComponent } from '../src/app/my-panel/my-panel.component'; +import { MyDataService } from '../src/app/my-data/my-data.service'; + +storiesOf('My Panel', module) + .add('Default', () => ({ + component: MyPanelComponent, + moduleMetadata: { + imports: [CommonModule], + schemas: [], + declarations: [MyButtonComponent], + providers: [MyDataService], + } + })); +``` + +If you have metadata that is common between your stories, this can configured once using the `moduleMetadata()` decorator: + +```js +import { CommonModule } from '@angular/common'; +import { storiesOf, moduleMetadata } from '@storybook/angular'; +import { MyButtonComponent } from '../src/app/my-button/my-button.component'; +import { MyPanelComponent } from '../src/app/my-panel/my-panel.component'; +import { MyDataService } from '../src/app/my-data/my-data.service'; + +storiesOf('My Panel', module) + .addDecorator( + moduleMetadata({ + imports: [CommonModule], + schemas: [], + declarations: [MyButtonComponent], + providers: [MyDataService], + }) + ) + .add('Default', () => ({ + component: MyPanelComponent + })) + .add('with a title', () => ({ + component: MyPanelComponent, + props: { + title: 'Foo', + } + })); +``` diff --git a/examples/angular-cli/src/stories/__snapshots__/metadata-combined.stories.storyshot b/examples/angular-cli/src/stories/__snapshots__/metadata-combined.stories.storyshot new file mode 100644 index 000000000000..d7c93c72db25 --- /dev/null +++ b/examples/angular-cli/src/stories/__snapshots__/metadata-combined.stories.storyshot @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Metadata|Combined Combined 1 1`] = ` + + + + + +

+ Prop Name +

+ + +

+ Items: +

+ + +
    + + + +
  • + + Joe + +
  • +
  • + + Jane + +
  • + + +
+ + +
+
+
+`; + +exports[`Storyshots Metadata|Combined Combined 2 1`] = ` + + + + + +

+ CustomPipe: Prop Name +

+ + +

+ Items: +

+ + +
    + + + +
  • + + Joe + +
  • +
  • + + Jane + +
  • + + +
+ + +
+
+
+`; diff --git a/examples/angular-cli/src/stories/__snapshots__/metadata-individual.stories.storyshot b/examples/angular-cli/src/stories/__snapshots__/metadata-individual.stories.storyshot new file mode 100644 index 000000000000..3f4b1d5b133d --- /dev/null +++ b/examples/angular-cli/src/stories/__snapshots__/metadata-individual.stories.storyshot @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Metadata|Individual Individual 1 1`] = ` + + + + + +`; + +exports[`Storyshots Metadata|Individual Individual 2 1`] = ` + + + + + +

+ Provider Name +

+ + +

+ Items: +

+ + +
    + + + +
  • + + Jim + +
  • +
  • + + Jill + +
  • + + +
+ + +
+
+
+`; diff --git a/examples/angular-cli/src/stories/__snapshots__/metadata-shared.stories.storyshot b/examples/angular-cli/src/stories/__snapshots__/metadata-shared.stories.storyshot new file mode 100644 index 000000000000..06342cd5f760 --- /dev/null +++ b/examples/angular-cli/src/stories/__snapshots__/metadata-shared.stories.storyshot @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Metadata|Shared Shared 1 1`] = ` + + + + + +

+ Prop Name +

+ + +

+ Items: +

+ + +
    + + + +
  • + + Joe + +
  • +
  • + + Jane + +
  • + + +
+ + +
+
+
+`; + +exports[`Storyshots Metadata|Shared Shared 2 1`] = ` + + + + + +

+ Provider Name +

+ + +

+ Items: +

+ + +
    + + + +
  • + + Joe + +
  • +
  • + + Jane + +
  • + + +
+ + +
+
+
+`; diff --git a/examples/angular-cli/src/stories/custom-pipes.stories.ts b/examples/angular-cli/src/stories/custom-pipes.stories.ts index 45b84c68ff0e..b8512cd6a525 100644 --- a/examples/angular-cli/src/stories/custom-pipes.stories.ts +++ b/examples/angular-cli/src/stories/custom-pipes.stories.ts @@ -3,18 +3,21 @@ import { withKnobs, text } from '@storybook/addon-knobs/angular'; import { NameComponent } from './moduleMetadata/name.component'; import { CustomPipePipe } from './moduleMetadata/custom.pipe'; +import { moduleMetadata } from '@storybook/angular'; storiesOf('Custom|Pipes', module) - .add('Simple', () => ({ - component: NameComponent, - props: { - field: 'foobar', - }, - moduleMetadata: { + .addDecorator( + moduleMetadata({ imports: [], schemas: [], declarations: [CustomPipePipe], providers: [], + }) + ) + .add('Simple', () => ({ + component: NameComponent, + props: { + field: 'foobar', }, })) .addDecorator(withKnobs) @@ -23,10 +26,4 @@ storiesOf('Custom|Pipes', module) props: { field: text('field', 'foobar'), }, - moduleMetadata: { - imports: [], - schemas: [], - declarations: [CustomPipePipe], - providers: [], - }, })); diff --git a/examples/angular-cli/src/stories/custom-providers.stories.ts b/examples/angular-cli/src/stories/custom-providers.stories.ts index 420c2f1c64a2..6101990a35a1 100644 --- a/examples/angular-cli/src/stories/custom-providers.stories.ts +++ b/examples/angular-cli/src/stories/custom-providers.stories.ts @@ -3,18 +3,21 @@ import { withKnobs, text } from '@storybook/addon-knobs/angular'; import { DummyService } from './moduleMetadata/dummy.service'; import { ServiceComponent } from './moduleMetadata/service.component'; +import { moduleMetadata } from '@storybook/angular'; storiesOf('Custom|Providers', module) - .add('Simple', () => ({ - component: ServiceComponent, - props: { - name: 'Static name', - }, - moduleMetadata: { + .addDecorator( + moduleMetadata({ imports: [], schemas: [], declarations: [], providers: [DummyService], + }) + ) + .add('Simple', () => ({ + component: ServiceComponent, + props: { + name: 'Static name', }, })) .addDecorator(withKnobs) @@ -26,11 +29,5 @@ storiesOf('Custom|Providers', module) props: { name, }, - moduleMetadata: { - imports: [], - schemas: [], - declarations: [], - providers: [DummyService], - }, }; }); diff --git a/examples/angular-cli/src/stories/custom-styles.stories.ts b/examples/angular-cli/src/stories/custom-styles.stories.ts index 6d8cabd8f2df..611b7667280f 100644 --- a/examples/angular-cli/src/stories/custom-styles.stories.ts +++ b/examples/angular-cli/src/stories/custom-styles.stories.ts @@ -1,14 +1,16 @@ -import { storiesOf } from '@storybook/angular'; +import { storiesOf, moduleMetadata } from '@storybook/angular'; import { action } from '@storybook/addon-actions'; import { withKnobs, text } from '@storybook/addon-knobs/angular'; import { Button } from '@storybook/angular/demo'; storiesOf('Custom|Style', module) + .addDecorator( + moduleMetadata({ + declarations: [Button], + }) + ) .add('Default', () => ({ template: ``, - moduleMetadata: { - declarations: [Button], - }, props: { text: 'Button with custom styles', onClick: action('log'), @@ -25,9 +27,6 @@ storiesOf('Custom|Style', module) .addDecorator(withKnobs) .add('With Knobs', () => ({ template: ``, - moduleMetadata: { - declarations: [Button], - }, props: { text: text('text', 'Button with custom styles'), onClick: action('log'), diff --git a/examples/angular-cli/src/stories/index.ts b/examples/angular-cli/src/stories/index.ts index 625a86f6bc64..785e16620a63 100644 --- a/examples/angular-cli/src/stories/index.ts +++ b/examples/angular-cli/src/stories/index.ts @@ -1,5 +1,6 @@ import { storiesOf } from '@storybook/angular'; import { Welcome, Button } from '@storybook/angular/demo'; +import { moduleMetadata } from '@storybook/angular'; storiesOf('Welcome', module).add('to Storybook', () => ({ template: ``, @@ -10,11 +11,13 @@ storiesOf('Welcome', module).add('to Storybook', () => ({ })); storiesOf('Button', module) + .addDecorator( + moduleMetadata({ + declarations: [Button], + }) + ) .add('with text', () => ({ template: ``, - moduleMetadata: { - declarations: [Button], - }, props: { text: 'Hello Button', onClick: event => { @@ -25,9 +28,6 @@ storiesOf('Button', module) })) .add('with some emoji', () => ({ template: ``, - moduleMetadata: { - declarations: [Button], - }, props: { text: '😀 😎 👍 💯', onClick: () => {}, diff --git a/examples/angular-cli/src/stories/metadata-combined.stories.ts b/examples/angular-cli/src/stories/metadata-combined.stories.ts new file mode 100644 index 000000000000..31c5289378e3 --- /dev/null +++ b/examples/angular-cli/src/stories/metadata-combined.stories.ts @@ -0,0 +1,36 @@ +import { storiesOf, moduleMetadata } from '@storybook/angular'; +import { TokenComponent, ITEMS, DEFAULT_NAME } from './moduleMetadata/token.component'; +import { CustomPipePipe } from './moduleMetadata/custom.pipe'; + +storiesOf('Metadata|Combined', module) + .addDecorator( + moduleMetadata({ + imports: [], + declarations: [TokenComponent], + providers: [ + { + provide: ITEMS, + useValue: ['Joe', 'Jane'], + }, + { + provide: DEFAULT_NAME, + useValue: 'Provider Name', + }, + ], + }) + ) + .add('Combined 1', () => ({ + template: ``, + props: { + name: 'Prop Name', + }, + })) + .add('Combined 2', () => ({ + template: ``, + props: { + name: 'Prop Name', + }, + moduleMetadata: { + declarations: [CustomPipePipe], + }, + })); diff --git a/examples/angular-cli/src/stories/metadata-individual.stories.ts b/examples/angular-cli/src/stories/metadata-individual.stories.ts new file mode 100644 index 000000000000..79b3a5a12b78 --- /dev/null +++ b/examples/angular-cli/src/stories/metadata-individual.stories.ts @@ -0,0 +1,37 @@ +import { storiesOf } from '@storybook/angular'; +import { TokenComponent, ITEMS, DEFAULT_NAME } from './moduleMetadata/token.component'; + +storiesOf('Metadata|Individual', module) + .add('Individual 1', () => ({ + template: ``, + props: { + name: 'Prop Name', + }, + moduleMetadata: { + imports: [], + declarations: [], + providers: [ + { + provide: ITEMS, + useValue: ['Joe', 'Jane'], + }, + ], + }, + })) + .add('Individual 2', () => ({ + template: ``, + moduleMetadata: { + imports: [], + declarations: [TokenComponent], + providers: [ + { + provide: ITEMS, + useValue: ['Jim', 'Jill'], + }, + { + provide: DEFAULT_NAME, + useValue: 'Provider Name', + }, + ], + }, + })); diff --git a/examples/angular-cli/src/stories/metadata-shared.stories.ts b/examples/angular-cli/src/stories/metadata-shared.stories.ts new file mode 100644 index 000000000000..d4b38c3f0052 --- /dev/null +++ b/examples/angular-cli/src/stories/metadata-shared.stories.ts @@ -0,0 +1,29 @@ +import { storiesOf, moduleMetadata } from '@storybook/angular'; +import { TokenComponent, ITEMS, DEFAULT_NAME } from './moduleMetadata/token.component'; + +storiesOf('Metadata|Shared', module) + .addDecorator( + moduleMetadata({ + imports: [], + declarations: [TokenComponent], + providers: [ + { + provide: ITEMS, + useValue: ['Joe', 'Jane'], + }, + { + provide: DEFAULT_NAME, + useValue: 'Provider Name', + }, + ], + }) + ) + .add('Shared 1', () => ({ + template: ``, + props: { + name: 'Prop Name', + }, + })) + .add('Shared 2', () => ({ + template: ``, + })); diff --git a/examples/angular-cli/src/stories/moduleMetadata/token.component.ts b/examples/angular-cli/src/stories/moduleMetadata/token.component.ts new file mode 100644 index 000000000000..4afb457d1624 --- /dev/null +++ b/examples/angular-cli/src/stories/moduleMetadata/token.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, InjectionToken, Inject, Optional } from '@angular/core'; + +export const ITEMS = new InjectionToken('TokenComponent.Items'); +export const DEFAULT_NAME = new InjectionToken('TokenComponent.DefaultName'); + +@Component({ + selector: 'storybook-simple-token-component', + template: ` +

{{ name }}

+

Items:

+
    +
  • + {{ item }} +
  • +
+ `, +}) +export class TokenComponent { + items; + @Input() name; + + constructor( + @Optional() + @Inject(DEFAULT_NAME) + defaultName: string, + @Inject(ITEMS) items: string[] + ) { + this.name = defaultName; + this.items = items; + } +} diff --git a/integration/__image_snapshots__/angular-cli-snap.png b/integration/__image_snapshots__/angular-cli-snap.png index 06d6b3e5e180..833db5a360ae 100644 Binary files a/integration/__image_snapshots__/angular-cli-snap.png and b/integration/__image_snapshots__/angular-cli-snap.png differ diff --git a/scripts/test.js b/scripts/test.js index 40d6ff255ae2..28c89f8b39c3 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -43,7 +43,7 @@ const createOption = ({ defaultValue, option, name, extraParam }) => ({ const tasks = { core: createProject({ - name: `Core & React & Vue & Polymer ${chalk.gray('(core)')}`, + name: `Core & React & Vue & Polymer & Angular ${chalk.gray('(core)')}`, defaultValue: true, option: '--core', projectLocation: path.join(__dirname, '..'),