From 90adc9e373b979e41dad42bd0d4288da5873442f Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 11 Feb 2020 17:39:11 +1100 Subject: [PATCH 01/39] Add very basic story state --- lib/client-api/src/story_store.test.ts | 14 ++++++++++++++ lib/client-api/src/story_store.ts | 7 +++++++ lib/client-api/src/types.ts | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 04184e7d457b..5610346957b0 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -81,6 +81,20 @@ describe('preview.story_store', () => { }); }); + describe('setStoryState', () => { + it('changes the state of a story, per-key', () => { + const store = new StoryStore({ channel }); + addStoryToStore(store, 'a', '1', () => 0); + expect(store.getRawStory('a', '1').state).toEqual({}); + + store.setStoryState('a--1', { foo: 'bar' }); + expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar' }); + + store.setStoryState('a--1', { baz: 'bing' }); + expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar', baz: 'bing' }); + }); + }); + describe('storySort', () => { it('sorts stories using given function', () => { const parameters = { diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index ce6f69ad0e9c..596f697393ac 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -15,6 +15,7 @@ import { StoreData, AddStoryArgs, StoreItem, + StoryState, ErrorLike, GetStorybookKind, } from './types'; @@ -187,6 +188,7 @@ export default class StoryStore extends EventEmitter { storyFn, parameters: allParameters, + state: {}, }; // LET'S SEND IT TO THE MANAGER @@ -346,6 +348,11 @@ export default class StoryStore extends EventEmitter { this.getStoriesForKind(kind).map(story => this.cleanHooks(story.id)); } + setStoryState(id: string, newState: StoryState) { + const { state } = this._stories[id]; + this._stories[id].state = { ...state, ...newState }; + } + // This API is a reimplementation of Storybook's original getStorybook() API. // As such it may not behave *exactly* the same, but aims to. Some notes: // - It is *NOT* sorted by the user's sort function, but remains sorted in "insertion order" diff --git a/lib/client-api/src/types.ts b/lib/client-api/src/types.ts index 6a9923b1ae79..c7e6b1b4ef35 100644 --- a/lib/client-api/src/types.ts +++ b/lib/client-api/src/types.ts @@ -20,6 +20,10 @@ export interface StoryMetadata { decorators: DecoratorFunction[]; } +export interface StoryState { + [key: string]: any; +} + export interface StoreItem extends StoryContext { id: string; kind: string; @@ -30,6 +34,7 @@ export interface StoreItem extends StoryContext { storyFn: StoryFn; hooks: HooksContext; parameters: Parameters; + state: StoryState; } export interface StoreData { From 8b08dc20801944e72bc38e0804996da89adc0c2d Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 11 Feb 2020 17:44:16 +1100 Subject: [PATCH 02/39] Add STORY_STATE_CHANGED event --- lib/client-api/src/story_store.test.ts | 15 +++++++++++++++ lib/client-api/src/story_store.ts | 3 +++ lib/core-events/src/index.ts | 2 ++ 3 files changed, 20 insertions(+) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 5610346957b0..613e47b42fa9 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -93,6 +93,21 @@ describe('preview.story_store', () => { store.setStoryState('a--1', { baz: 'bing' }); expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar', baz: 'bing' }); }); + + it('synchronously emits STORY_STATE_CHANGED if different', () => { + const onStateChanged = jest.fn(); + const testChannel = createChannel({ page: 'preview' }); + testChannel.on(Events.STORY_STATE_CHANGED, onStateChanged); + + const store = new StoryStore({ channel: testChannel }); + addStoryToStore(store, 'a', '1', () => 0); + + store.setStoryState('a--1', { foo: 'bar' }); + expect(onStateChanged).toHaveBeenCalledWith('a--1', { foo: 'bar' }); + + store.setStoryState('a--1', { baz: 'bing' }); + expect(onStateChanged).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); + }); }); describe('storySort', () => { diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 596f697393ac..263bc6e1d2bd 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -349,8 +349,11 @@ export default class StoryStore extends EventEmitter { } setStoryState(id: string, newState: StoryState) { + if (!this._stories[id]) throw new Error(`No story for id ${id}`); const { state } = this._stories[id]; this._stories[id].state = { ...state, ...newState }; + + this._channel.emit(Events.STORY_STATE_CHANGED, id, this._stories[id].state); } // This API is a reimplementation of Storybook's original getStorybook() API. diff --git a/lib/core-events/src/index.ts b/lib/core-events/src/index.ts index ef62ec54f093..a7a2ace516d0 100644 --- a/lib/core-events/src/index.ts +++ b/lib/core-events/src/index.ts @@ -10,6 +10,7 @@ enum events { STORY_ADDED = 'storyAdded', STORY_CHANGED = 'storyChanged', STORY_UNCHANGED = 'storyUnchanged', + STORY_STATE_CHANGED = 'storyStateChanged', FORCE_RE_RENDER = 'forceReRender', REGISTER_SUBSCRIPTION = 'registerSubscription', STORY_INIT = 'storyInit', @@ -44,6 +45,7 @@ export const { REGISTER_SUBSCRIPTION, STORY_INIT, STORY_ADDED, + STORY_STATE_CHANGED, STORY_RENDER, STORY_RENDERED, STORY_MISSING, From a784aa1b9b1335880db027791cd58a882c8108d5 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 11 Feb 2020 20:56:13 +1100 Subject: [PATCH 03/39] Listen to `CHANGE_STORY_STATE` and change it in the store --- lib/client-api/src/story_store.test.ts | 14 ++++++++++++-- lib/client-api/src/story_store.ts | 6 +++++- lib/core-events/src/index.ts | 2 ++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 613e47b42fa9..303880b20503 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -1,6 +1,6 @@ import createChannel from '@storybook/channel-postmessage'; import { toId } from '@storybook/csf'; -import addons from '@storybook/addons'; +import addons, { mockChannel } from '@storybook/addons'; import Events from '@storybook/core-events'; import StoryStore from './story_store'; @@ -96,7 +96,7 @@ describe('preview.story_store', () => { it('synchronously emits STORY_STATE_CHANGED if different', () => { const onStateChanged = jest.fn(); - const testChannel = createChannel({ page: 'preview' }); + const testChannel = mockChannel(); testChannel.on(Events.STORY_STATE_CHANGED, onStateChanged); const store = new StoryStore({ channel: testChannel }); @@ -108,6 +108,16 @@ describe('preview.story_store', () => { store.setStoryState('a--1', { baz: 'bing' }); expect(onStateChanged).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); }); + + it('should update if the CHANGE_STORY_STATE event is received', () => { + const testChannel = mockChannel(); + const store = new StoryStore({ channel: testChannel }); + addStoryToStore(store, 'a', '1', () => 0); + + testChannel.emit(Events.CHANGE_STORY_STATE, 'a--1', { foo: 'bar' }); + + expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar' }); + }); }); describe('storySort', () => { diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 263bc6e1d2bd..806da07e9151 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -84,12 +84,16 @@ export default class StoryStore extends EventEmitter { this._stories = {}; this._revision = 0; this._selection = {} as any; - this._channel = params.channel; this._error = undefined; + + if (params.channel) this.setChannel(params.channel); } setChannel = (channel: Channel) => { this._channel = channel; + channel.on(Events.CHANGE_STORY_STATE, (id: string, newState: StoryState) => + this.setStoryState(id, newState) + ); }; addGlobalMetadata({ parameters, decorators }: StoryMetadata) { diff --git a/lib/core-events/src/index.ts b/lib/core-events/src/index.ts index a7a2ace516d0..51ebca850acb 100644 --- a/lib/core-events/src/index.ts +++ b/lib/core-events/src/index.ts @@ -10,6 +10,7 @@ enum events { STORY_ADDED = 'storyAdded', STORY_CHANGED = 'storyChanged', STORY_UNCHANGED = 'storyUnchanged', + CHANGE_STORY_STATE = 'changeStoryState', STORY_STATE_CHANGED = 'storyStateChanged', FORCE_RE_RENDER = 'forceReRender', REGISTER_SUBSCRIPTION = 'registerSubscription', @@ -45,6 +46,7 @@ export const { REGISTER_SUBSCRIPTION, STORY_INIT, STORY_ADDED, + CHANGE_STORY_STATE, STORY_STATE_CHANGED, STORY_RENDER, STORY_RENDERED, From 208b4462a9b202d24647327628ac5b2df22437d4 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 11 Feb 2020 21:11:19 +1100 Subject: [PATCH 04/39] Ensure that the state is passed to the story function --- .../stories/core/state.stories.js | 16 +++++++++++++++ lib/client-api/src/story_store.test.ts | 20 ++++++++++++++++--- lib/client-api/src/story_store.ts | 1 + 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 examples/official-storybook/stories/core/state.stories.js diff --git a/examples/official-storybook/stories/core/state.stories.js b/examples/official-storybook/stories/core/state.stories.js new file mode 100644 index 000000000000..6d2b181f2966 --- /dev/null +++ b/examples/official-storybook/stories/core/state.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default { + title: 'Core/State', +}; + +export const PassedToStory = ({ state: { name } }) => ( +
The value of the name field is {name}
+); + +PassedToStory.propTypes = { + state: PropTypes.shape({ + name: PropTypes.string, + }).isRequired, +}; diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 303880b20503..1ca9ded4f161 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -81,8 +81,8 @@ describe('preview.story_store', () => { }); }); - describe('setStoryState', () => { - it('changes the state of a story, per-key', () => { + describe('state', () => { + it('setStoryState changes the state of a story, per-key', () => { const store = new StoryStore({ channel }); addStoryToStore(store, 'a', '1', () => 0); expect(store.getRawStory('a', '1').state).toEqual({}); @@ -94,7 +94,21 @@ describe('preview.story_store', () => { expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar', baz: 'bing' }); }); - it('synchronously emits STORY_STATE_CHANGED if different', () => { + it('is passed to the story in the context', () => { + const storyFn = jest.fn(); + const store = new StoryStore({ channel }); + addStoryToStore(store, 'a', '1', storyFn); + store.setStoryState('a--1', { foo: 'bar' }); + store.getRawStory('a', '1').storyFn(); + + expect(storyFn).toHaveBeenCalledWith( + expect.objectContaining({ + state: { foo: 'bar' }, + }) + ); + }); + + it('setStoryState synchronously emits STORY_STATE_CHANGED if different', () => { const onStateChanged = jest.fn(); const testChannel = mockChannel(); testChannel.on(Events.STORY_STATE_CHANGED, onStateChanged); diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 806da07e9151..dfb388465835 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -181,6 +181,7 @@ export default class StoryStore extends EventEmitter { hooks, // NOTE: we do not allow the passed in context to override parameters parameters: allParameters, + state: this._stories[id].state, }); _stories[id] = { From d2a7e63983a3cfc86eb77c8754ed28734bd67fdc Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 11 Feb 2020 21:23:36 +1100 Subject: [PATCH 05/39] Ensure the story re-renders when the state changes --- lib/client-api/src/story_store.test.ts | 14 +++++++++----- lib/client-api/src/story_store.ts | 3 +++ lib/core/src/client/preview/start.js | 1 + 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 1ca9ded4f161..3b78e66b3671 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -108,19 +108,23 @@ describe('preview.story_store', () => { ); }); - it('setStoryState synchronously emits STORY_STATE_CHANGED if different', () => { - const onStateChanged = jest.fn(); + it('setStoryState emits STORY_STATE_CHANGED', () => { + const onStateChangedChannel = jest.fn(); + const onStateChangedStore = jest.fn(); const testChannel = mockChannel(); - testChannel.on(Events.STORY_STATE_CHANGED, onStateChanged); + testChannel.on(Events.STORY_STATE_CHANGED, onStateChangedChannel); const store = new StoryStore({ channel: testChannel }); + store.on(Events.STORY_STATE_CHANGED, onStateChangedStore); addStoryToStore(store, 'a', '1', () => 0); store.setStoryState('a--1', { foo: 'bar' }); - expect(onStateChanged).toHaveBeenCalledWith('a--1', { foo: 'bar' }); + expect(onStateChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar' }); + expect(onStateChangedStore).toHaveBeenCalledWith('a--1', { foo: 'bar' }); store.setStoryState('a--1', { baz: 'bing' }); - expect(onStateChanged).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); + expect(onStateChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); + expect(onStateChangedStore).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); }); it('should update if the CHANGE_STORY_STATE event is received', () => { diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index dfb388465835..91d00516240c 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -358,7 +358,10 @@ export default class StoryStore extends EventEmitter { const { state } = this._stories[id]; this._stories[id].state = { ...state, ...newState }; + // TODO: Sort out what is going on with both the store and the channel being event emitters. + // It has something to do with React Native, but need to get to the bottom of it this._channel.emit(Events.STORY_STATE_CHANGED, id, this._stories[id].state); + this.emit(Events.STORY_STATE_CHANGED, id, this._stories[id].state); } // This API is a reimplementation of Storybook's original getStorybook() API. diff --git a/lib/core/src/client/preview/start.js b/lib/core/src/client/preview/start.js index 8fb75467ebc2..07535ea36ca5 100644 --- a/lib/core/src/client/preview/start.js +++ b/lib/core/src/client/preview/start.js @@ -350,6 +350,7 @@ export default function start(render, { decorateStory } = {}) { }); storyStore.on(Events.STORY_RENDER, renderUI); + storyStore.on(Events.STORY_STATE_CHANGED, () => renderUI(true)); if (typeof window !== 'undefined') { window.__STORYBOOK_CLIENT_API__ = clientApi; From f070609b229aa569ac2d5fee3e5d7befca62451e Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 13 Feb 2020 20:29:32 +1100 Subject: [PATCH 06/39] Started on README for client_api --- lib/client-api/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/client-api/README.md diff --git a/lib/client-api/README.md b/lib/client-api/README.md new file mode 100644 index 000000000000..a7c31c286e45 --- /dev/null +++ b/lib/client-api/README.md @@ -0,0 +1,29 @@ +# `@storybook/client-api` -- APIs that control the rendering of stories in the preview iframe. + +## Story store + +The story store contains the list of stories loaded in a Storybook. + +Each story is loaded via the `.add()` API and contains the follow attributes, which are available on the `context` (which is passed to the story's render function and decorators): + +- `kind` - the grouping of the story, typically corresponds to a single component. Also known as the `title` in CSF. +- `name` - the name of the specific story. +- `id` - an unique, URL sanitized identifier for the story, created from the `kind` and `name`. +- `parameters` - static data about the story, see below. +- `properties` - dynamic inputs to the story, see blow. +- `hooks` - listeners that will rerun when the story changes or is unmounted, see `@storybook/addons`. + +## Parameters + +The story parameters is a static, serializable object of data that provides details about the story. Those details can be used by addons or Storybook itself to render UI or provide defaults about the story rendering. + +Parameters _cannot change_ and are syncronized to the manager once when the story is loaded (note over the lifetime of a development Storybook a story can be loaded several times due to hot module reload, so the parameters technically can change for that reason). + +Usually addons will read from a single key of `parameters` namespaced by the name of that addon. For instance the configuration of the `backgrounds` addon is driven by the `parameters.backgrounds` namespace. + +Parameters are inheritable -- you can set global parameters via `addParameters` (exported by `client_api` and each framework), at the component level by the `parameters` key of the component (default) export in CSF (or in `.storiesOf`), and on a single story via the `parameters` key on the story data, or the third argument to `.add()`. + +Some notable parameters: + +- `fileName` - the file that the story was defined in, when available +- `properties` - type information about properties (see below) From 7406da4cb3f84fe4f4e071016bcaeb84aa6317a3 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 14 Feb 2020 16:26:56 +1100 Subject: [PATCH 07/39] Add a README with a game plan around args --- lib/client-api/README.md | 62 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/client-api/README.md b/lib/client-api/README.md index a7c31c286e45..cda8735c871f 100644 --- a/lib/client-api/README.md +++ b/lib/client-api/README.md @@ -25,5 +25,63 @@ Parameters are inheritable -- you can set global parameters via `addParameters` Some notable parameters: -- `fileName` - the file that the story was defined in, when available -- `properties` - type information about properties (see below) +- `parameters.fileName` - the file that the story was defined in, when available +- `parameters.argsTypes` - type information about args (see below) + +### Parameter enhancement + +Ideally all parameters should be set _at build time_ of the Storybook, either directly by the user, or via the use of a webpack loader. (For an example of this, see `addon-storysource`, which writes to the `parameters.storysource` parameter with a set of static information about the story file). + +However, in some cases it is necessary to set parameters at _load time_ when the Storybook first loads. This should be avoided if at all possible as it is cost that must be paid each time a Storybook loads, rather than just once when the Storybook is built. + +To add a parameter enhancer, call `store.addParameterEnhancer(enhancer)` _before_ any stories are loaded (in addon registration or in `preview.js`). As each story is loaded, the enhancer will be called with the full story `context` -- the return value should be an object that will be patched into the Story's parameters. +Alm + +## Args + +Args are "inputs" to stories. + +You can think of them equivalently to React props, Angular inputs and outputs, etc. + +Changing the args cause the story to be re-rendered with the new set of args. + +### Using args in a story + +By default, args are passed to a story in the context; like parameters, they are available as `context.args`. + +```js +const YourStory = ({ args: { x, y }}) => /* render your story using `x` and `y` */ +``` + +If you set the `parameters.options.passArgsFirst` option on a story, then the args will be passed to the story as first argument and the context as second: + +```js +const YourStory = ({ x, y } /*, context*/) => /* render your story using `x` and `y` */ +``` + +### Using args in an addon + +Args values are automatically syncronized (via the `changeStoryArgs` and `storyArgsChanged` events) between the preview and manager; APIs exist in `lib/api` to read and set properties in the manager. + +Args need to be serializable -- so currently cannot include callbacks (this may change in a future version). + +Note that arg values are passed directly to a story -- you should only store the actual value that the story needs to render in the property. If you need more complex information supporting that, use parameters or addon state. + +### Default values + +The initial value of an arg is driven by the `parameters.argTypes` field. For each key in that field, if a `defaultValue` exists, then the arg will be initialized to that value. + +For instance, for this story: + +```js +export MyStory = .... +MyStory.story = { parameters: { + argTypes: { + primary: { defaultValue: true }, + size: { defaultValue: 'large' }, + color: { /* other things */ }, + }, +}} +``` + +Then `context.args` will default to `{ primary: true, size: large }` From cc41e48734352c8c1c2e22bd3f4724a0f9433914 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 15:40:43 +1100 Subject: [PATCH 08/39] Change story state => story args --- lib/client-api/README.md | 6 ++-- lib/client-api/src/story_store.test.ts | 46 +++++++++++++------------- lib/client-api/src/story_store.ts | 20 +++++------ lib/client-api/src/types.ts | 4 +-- lib/core-events/src/index.ts | 8 ++--- lib/core/src/client/preview/start.js | 2 +- 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/lib/client-api/README.md b/lib/client-api/README.md index cda8735c871f..ac7bd527a414 100644 --- a/lib/client-api/README.md +++ b/lib/client-api/README.md @@ -10,7 +10,7 @@ Each story is loaded via the `.add()` API and contains the follow attributes, wh - `name` - the name of the specific story. - `id` - an unique, URL sanitized identifier for the story, created from the `kind` and `name`. - `parameters` - static data about the story, see below. -- `properties` - dynamic inputs to the story, see blow. +- `args` - dynamic inputs to the story, see blow. - `hooks` - listeners that will rerun when the story changes or is unmounted, see `@storybook/addons`. ## Parameters @@ -61,11 +61,11 @@ const YourStory = ({ x, y } /*, context*/) => /* render your story using `x` and ### Using args in an addon -Args values are automatically syncronized (via the `changeStoryArgs` and `storyArgsChanged` events) between the preview and manager; APIs exist in `lib/api` to read and set properties in the manager. +Args values are automatically syncronized (via the `changeStoryArgs` and `storyArgsChanged` events) between the preview and manager; APIs exist in `lib/api` to read and set args in the manager. Args need to be serializable -- so currently cannot include callbacks (this may change in a future version). -Note that arg values are passed directly to a story -- you should only store the actual value that the story needs to render in the property. If you need more complex information supporting that, use parameters or addon state. +Note that arg values are passed directly to a story -- you should only store the actual value that the story needs to render in the arg. If you need more complex information supporting that, use parameters or addon state. ### Default values diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 3b78e66b3671..aeef0b1fcff5 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -81,60 +81,60 @@ describe('preview.story_store', () => { }); }); - describe('state', () => { - it('setStoryState changes the state of a story, per-key', () => { + describe('args', () => { + it('setStoryArgs changes the args of a story, per-key', () => { const store = new StoryStore({ channel }); addStoryToStore(store, 'a', '1', () => 0); - expect(store.getRawStory('a', '1').state).toEqual({}); + expect(store.getRawStory('a', '1').args).toEqual({}); - store.setStoryState('a--1', { foo: 'bar' }); - expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar' }); + store.setStoryArgs('a--1', { foo: 'bar' }); + expect(store.getRawStory('a', '1').args).toEqual({ foo: 'bar' }); - store.setStoryState('a--1', { baz: 'bing' }); - expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar', baz: 'bing' }); + store.setStoryArgs('a--1', { baz: 'bing' }); + expect(store.getRawStory('a', '1').args).toEqual({ foo: 'bar', baz: 'bing' }); }); it('is passed to the story in the context', () => { const storyFn = jest.fn(); const store = new StoryStore({ channel }); addStoryToStore(store, 'a', '1', storyFn); - store.setStoryState('a--1', { foo: 'bar' }); + store.setStoryArgs('a--1', { foo: 'bar' }); store.getRawStory('a', '1').storyFn(); expect(storyFn).toHaveBeenCalledWith( expect.objectContaining({ - state: { foo: 'bar' }, + args: { foo: 'bar' }, }) ); }); - it('setStoryState emits STORY_STATE_CHANGED', () => { - const onStateChangedChannel = jest.fn(); - const onStateChangedStore = jest.fn(); + it('setStoryArgs emits STORY_ARGS_CHANGED', () => { + const onArgsChangedChannel = jest.fn(); + const onArgsChangedStore = jest.fn(); const testChannel = mockChannel(); - testChannel.on(Events.STORY_STATE_CHANGED, onStateChangedChannel); + testChannel.on(Events.STORY_ARGS_CHANGED, onArgsChangedChannel); const store = new StoryStore({ channel: testChannel }); - store.on(Events.STORY_STATE_CHANGED, onStateChangedStore); + store.on(Events.STORY_ARGS_CHANGED, onArgsChangedStore); addStoryToStore(store, 'a', '1', () => 0); - store.setStoryState('a--1', { foo: 'bar' }); - expect(onStateChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar' }); - expect(onStateChangedStore).toHaveBeenCalledWith('a--1', { foo: 'bar' }); + store.setStoryArgs('a--1', { foo: 'bar' }); + expect(onArgsChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar' }); + expect(onArgsChangedStore).toHaveBeenCalledWith('a--1', { foo: 'bar' }); - store.setStoryState('a--1', { baz: 'bing' }); - expect(onStateChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); - expect(onStateChangedStore).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); + store.setStoryArgs('a--1', { baz: 'bing' }); + expect(onArgsChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); + expect(onArgsChangedStore).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' }); }); - it('should update if the CHANGE_STORY_STATE event is received', () => { + it('should update if the CHANGE_STORY_ARGS event is received', () => { const testChannel = mockChannel(); const store = new StoryStore({ channel: testChannel }); addStoryToStore(store, 'a', '1', () => 0); - testChannel.emit(Events.CHANGE_STORY_STATE, 'a--1', { foo: 'bar' }); + testChannel.emit(Events.CHANGE_STORY_ARGS, 'a--1', { foo: 'bar' }); - expect(store.getRawStory('a', '1').state).toEqual({ foo: 'bar' }); + expect(store.getRawStory('a', '1').args).toEqual({ foo: 'bar' }); }); }); diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 91d00516240c..4a1bb7869336 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -15,7 +15,7 @@ import { StoreData, AddStoryArgs, StoreItem, - StoryState, + StoryArgs, ErrorLike, GetStorybookKind, } from './types'; @@ -91,8 +91,8 @@ export default class StoryStore extends EventEmitter { setChannel = (channel: Channel) => { this._channel = channel; - channel.on(Events.CHANGE_STORY_STATE, (id: string, newState: StoryState) => - this.setStoryState(id, newState) + channel.on(Events.CHANGE_STORY_ARGS, (id: string, newArgs: StoryArgs) => + this.setStoryArgs(id, newArgs) ); }; @@ -181,7 +181,7 @@ export default class StoryStore extends EventEmitter { hooks, // NOTE: we do not allow the passed in context to override parameters parameters: allParameters, - state: this._stories[id].state, + args: _stories[id].args, }); _stories[id] = { @@ -193,7 +193,7 @@ export default class StoryStore extends EventEmitter { storyFn, parameters: allParameters, - state: {}, + args: {}, }; // LET'S SEND IT TO THE MANAGER @@ -353,15 +353,15 @@ export default class StoryStore extends EventEmitter { this.getStoriesForKind(kind).map(story => this.cleanHooks(story.id)); } - setStoryState(id: string, newState: StoryState) { + setStoryArgs(id: string, newArgs: StoryArgs) { if (!this._stories[id]) throw new Error(`No story for id ${id}`); - const { state } = this._stories[id]; - this._stories[id].state = { ...state, ...newState }; + const { args } = this._stories[id]; + this._stories[id].args = { ...args, ...newArgs }; // TODO: Sort out what is going on with both the store and the channel being event emitters. // It has something to do with React Native, but need to get to the bottom of it - this._channel.emit(Events.STORY_STATE_CHANGED, id, this._stories[id].state); - this.emit(Events.STORY_STATE_CHANGED, id, this._stories[id].state); + this._channel.emit(Events.STORY_ARGS_CHANGED, id, this._stories[id].args); + this.emit(Events.STORY_ARGS_CHANGED, id, this._stories[id].args); } // This API is a reimplementation of Storybook's original getStorybook() API. diff --git a/lib/client-api/src/types.ts b/lib/client-api/src/types.ts index c7e6b1b4ef35..2927149b647d 100644 --- a/lib/client-api/src/types.ts +++ b/lib/client-api/src/types.ts @@ -20,7 +20,7 @@ export interface StoryMetadata { decorators: DecoratorFunction[]; } -export interface StoryState { +export interface StoryArgs { [key: string]: any; } @@ -34,7 +34,7 @@ export interface StoreItem extends StoryContext { storyFn: StoryFn; hooks: HooksContext; parameters: Parameters; - state: StoryState; + args: StoryArgs; } export interface StoreData { diff --git a/lib/core-events/src/index.ts b/lib/core-events/src/index.ts index 51ebca850acb..1198323938f2 100644 --- a/lib/core-events/src/index.ts +++ b/lib/core-events/src/index.ts @@ -10,8 +10,8 @@ enum events { STORY_ADDED = 'storyAdded', STORY_CHANGED = 'storyChanged', STORY_UNCHANGED = 'storyUnchanged', - CHANGE_STORY_STATE = 'changeStoryState', - STORY_STATE_CHANGED = 'storyStateChanged', + CHANGE_STORY_ARGS = 'changeStoryArgs', + STORY_ARGS_CHANGED = 'storyArgsChanged', FORCE_RE_RENDER = 'forceReRender', REGISTER_SUBSCRIPTION = 'registerSubscription', STORY_INIT = 'storyInit', @@ -46,8 +46,8 @@ export const { REGISTER_SUBSCRIPTION, STORY_INIT, STORY_ADDED, - CHANGE_STORY_STATE, - STORY_STATE_CHANGED, + CHANGE_STORY_ARGS, + STORY_ARGS_CHANGED, STORY_RENDER, STORY_RENDERED, STORY_MISSING, diff --git a/lib/core/src/client/preview/start.js b/lib/core/src/client/preview/start.js index 07535ea36ca5..6e0b00fba4c5 100644 --- a/lib/core/src/client/preview/start.js +++ b/lib/core/src/client/preview/start.js @@ -350,7 +350,7 @@ export default function start(render, { decorateStory } = {}) { }); storyStore.on(Events.STORY_RENDER, renderUI); - storyStore.on(Events.STORY_STATE_CHANGED, () => renderUI(true)); + storyStore.on(Events.STORY_ARGS_CHANGED, forceReRender); if (typeof window !== 'undefined') { window.__STORYBOOK_CLIENT_API__ = clientApi; From 060f0d626358262bec180116a36494c166fac767 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 15:54:14 +1100 Subject: [PATCH 09/39] Initialize arg values based on parameters.argTypes --- lib/client-api/src/story_store.test.ts | 16 ++++++++++++ lib/client-api/src/story_store.ts | 36 +++++++++++++++++++------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index aeef0b1fcff5..186138d464a7 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -82,6 +82,22 @@ describe('preview.story_store', () => { }); describe('args', () => { + it('args is initialized to the value stored in parameters.argType[name].defaultValue', () => { + const store = new StoryStore({ channel }); + addStoryToStore(store, 'a', '1', () => 0, { + argTypes: { + arg1: { defaultValue: 'arg1' }, + arg2: { defaultValue: 2 }, + arg3: { defaultValue: { complex: { object: ['type'] } } }, + }, + }); + expect(store.getRawStory('a', '1').args).toEqual({ + arg1: 'arg1', + arg2: 2, + arg3: { complex: { object: ['type'] } }, + }); + }); + it('setStoryArgs changes the args of a story, per-key', () => { const store = new StoryStore({ channel }); addStoryToStore(store, 'a', '1', () => 0); diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 4a1bb7869336..e0ca87ee82ed 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -126,7 +126,14 @@ export default class StoryStore extends EventEmitter { } addStory( - { id, kind, name, storyFn: original, parameters = {}, decorators = [] }: AddStoryArgs, + { + id, + kind, + name, + storyFn: original, + parameters: storyParameters = {}, + decorators: storyDecorators = [], + }: AddStoryArgs, { applyDecorators, }: { @@ -156,20 +163,20 @@ export default class StoryStore extends EventEmitter { this.ensureKind(kind); const kindMetadata: KindMetadata = this._kinds[kind]; - const allDecorators = [ - ...decorators, + const decorators = [ + ...storyDecorators, ...kindMetadata.decorators, ...this._globalMetadata.decorators, ]; - const allParameters = combineParameters( + const parameters = combineParameters( this._globalMetadata.parameters, kindMetadata.parameters, - parameters + storyParameters ); // lazily decorate the story when it's loaded const getDecorated: () => StoryFn = memoize(1)(() => - applyDecorators(getOriginal(), allDecorators) + applyDecorators(getOriginal(), decorators) ); const hooks = new HooksContext(); @@ -180,10 +187,21 @@ export default class StoryStore extends EventEmitter { ...context, hooks, // NOTE: we do not allow the passed in context to override parameters - parameters: allParameters, + parameters, args: _stories[id].args, }); + // Pull out parameters.argTypes.$.defaultValue into initialArgs + const initialArgs: Record = parameters.argTypes + ? Object.entries(parameters.argTypes as Record).reduce( + (acc, [arg, { defaultValue }]) => { + if (defaultValue) acc[arg] = defaultValue; + return acc; + }, + {} as Record + ) + : {}; + _stories[id] = { ...identification, @@ -192,8 +210,8 @@ export default class StoryStore extends EventEmitter { getOriginal, storyFn, - parameters: allParameters, - args: {}, + parameters, + args: initialArgs, }; // LET'S SEND IT TO THE MANAGER From be5ef391354ffc58bd86d4d11f99ee30a39bc0cf Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 16:33:46 +1100 Subject: [PATCH 10/39] Fix type signature for `.add()` --- lib/client-api/src/client_api.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/client-api/src/client_api.ts b/lib/client-api/src/client_api.ts index 464a9d29ca44..bd84c6d6b7f6 100644 --- a/lib/client-api/src/client_api.ts +++ b/lib/client-api/src/client_api.ts @@ -129,7 +129,11 @@ export default class ClientApi { }; }); - api.add = (storyName: string, storyFn: string, parameters: Parameters = {}) => { + api.add = ( + storyName: string, + storyFn: StoryFn, + parameters: Parameters = {} + ) => { hasAdded = true; const id = parameters.__id || toId(kind, storyName); From b386f301ca19b4241a165ffc5db80027671252fe Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 16:35:22 +1100 Subject: [PATCH 11/39] Add parameterEnhancers --- lib/client-api/src/story_store.test.ts | 50 ++++ lib/client-api/src/story_store.ts | 30 ++- yarn.lock | 336 ++++++++++++++++++++++++- 3 files changed, 410 insertions(+), 6 deletions(-) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index 186138d464a7..e6f089876034 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -154,6 +154,56 @@ describe('preview.story_store', () => { }); }); + describe('parameterEnhancer', () => { + it('allows you to alter parameters when stories are added', () => { + const store = new StoryStore({ channel }); + + const enhancer = jest.fn().mockReturnValue({ c: 'd' }); + store.addParameterEnhancer(enhancer); + + addStoryToStore(store, 'a', '1', () => 0, { a: 'b' }); + + expect(enhancer).toHaveBeenCalledWith(expect.objectContaining({ parameters: { a: 'b' } })); + expect(store.getRawStory('a', '1').parameters).toEqual({ a: 'b', c: 'd' }); + }); + + it('recursively passes parameters to successive enhancers', () => { + const store = new StoryStore({ channel }); + + const firstEnhancer = jest.fn().mockReturnValue({ c: 'd' }); + store.addParameterEnhancer(firstEnhancer); + const secondEnhancer = jest.fn().mockReturnValue({ e: 'f' }); + store.addParameterEnhancer(secondEnhancer); + + addStoryToStore(store, 'a', '1', () => 0, { a: 'b' }); + + expect(firstEnhancer).toHaveBeenCalledWith( + expect.objectContaining({ parameters: { a: 'b' } }) + ); + expect(secondEnhancer).toHaveBeenCalledWith( + expect.objectContaining({ parameters: { a: 'b', c: 'd' } }) + ); + expect(store.getRawStory('a', '1').parameters).toEqual({ a: 'b', c: 'd', e: 'f' }); + }); + + it('allows you to alter parameters when stories are re-added', () => { + const store = new StoryStore({ channel }); + addons.setChannel(channel); + + const enhancer = jest.fn().mockReturnValue({ c: 'd' }); + store.addParameterEnhancer(enhancer); + + addStoryToStore(store, 'a', '1', () => 0, { a: 'b' }); + + enhancer.mockClear(); + store.removeStoryKind('a'); + + addStoryToStore(store, 'a', '1', () => 0, { e: 'f' }); + expect(enhancer).toHaveBeenCalledWith(expect.objectContaining({ parameters: { e: 'f' } })); + expect(store.getRawStory('a', '1').parameters).toEqual({ e: 'f', c: 'd' }); + }); + }); + describe('storySort', () => { it('sorts stories using given function', () => { const parameters = { diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index e0ca87ee82ed..473b5fbe1c42 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -34,6 +34,8 @@ interface StoryOptions { type KindMetadata = StoryMetadata & { order: number }; +type ParameterEnhancer = (context: StoryContext) => Parameters; + const isStoryDocsOnly = (parameters?: Parameters) => { return parameters && parameters.docsOnly; }; @@ -72,6 +74,8 @@ export default class StoryStore extends EventEmitter { // Keyed on storyId _stories: StoreData; + _parameterEnhancers: ParameterEnhancer[]; + _revision: number; _selection: Selection; @@ -82,6 +86,7 @@ export default class StoryStore extends EventEmitter { this._globalMetadata = { parameters: {}, decorators: [] }; this._kinds = {}; this._stories = {}; + this._parameterEnhancers = []; this._revision = 0; this._selection = {} as any; this._error = undefined; @@ -125,6 +130,13 @@ export default class StoryStore extends EventEmitter { this._kinds[kind].decorators.push(...decorators); } + addParameterEnhancer(parameterEnhancer: ParameterEnhancer) { + if (Object.keys(this._stories).length > 0) + throw new Error('Cannot add a parameter enhancer to the store after a story has been added.'); + + this._parameterEnhancers.push(parameterEnhancer); + } + addStory( { id, @@ -168,12 +180,21 @@ export default class StoryStore extends EventEmitter { ...kindMetadata.decorators, ...this._globalMetadata.decorators, ]; - const parameters = combineParameters( + const parametersBeforeEnhancement = combineParameters( this._globalMetadata.parameters, kindMetadata.parameters, storyParameters ); + const parameters = this._parameterEnhancers.reduce( + (accumlatedParameters, enhancer) => + combineParameters( + accumlatedParameters, + enhancer({ ...identification, parameters: accumlatedParameters }) + ), + parametersBeforeEnhancement + ); + // lazily decorate the story when it's loaded const getDecorated: () => StoryFn = memoize(1)(() => applyDecorators(getOriginal(), decorators) @@ -181,13 +202,12 @@ export default class StoryStore extends EventEmitter { const hooks = new HooksContext(); - const storyFn: StoryFn = (context: StoryContext) => + const storyFn: StoryFn = (runtimeContext: StoryContext) => getDecorated()({ ...identification, - ...context, - hooks, - // NOTE: we do not allow the passed in context to override parameters + ...runtimeContext, parameters, + hooks, args: _stories[id].args, }); diff --git a/yarn.lock b/yarn.lock index a845dcab33d2..82ac4831e004 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3368,6 +3368,19 @@ global "^4.3.2" util-deprecate "^1.0.2" +"@storybook/addons@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.0-alpha.11.tgz#2f9168f8ff9d19fff5b05981e86839c12dd84cc0" + integrity sha512-8HvQOnz/I6UclgqNtesPrrCGNXwa/3zVVuumCoSksIWWfnLbs7UIpC3PMGd1NDdZ4mhdIjhRA0FM255/ov3HIg== + dependencies: + "@storybook/api" "6.0.0-alpha.11" + "@storybook/channels" "6.0.0-alpha.11" + "@storybook/client-logger" "6.0.0-alpha.11" + "@storybook/core-events" "6.0.0-alpha.11" + core-js "^3.0.1" + global "^4.3.2" + util-deprecate "^1.0.2" + "@storybook/api@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.0-alpha.0.tgz#9b612f8c41fb8b1f9bbd3c990b5a1b3beb0eb82a" @@ -3394,6 +3407,32 @@ telejson "^3.2.0" util-deprecate "^1.0.2" +"@storybook/api@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.0-alpha.11.tgz#c4d11c4d69a481a6c540b0d008ff66f6150bc68b" + integrity sha512-sdzOaUppRNRSga3BxA6QnF1MfiDQHiaW1vHddbF8Cn6JD8Q9OtUZpLjJ+7IF61wodANln4fmMtiSE20WCer/jQ== + dependencies: + "@reach/router" "^1.2.1" + "@storybook/channels" "6.0.0-alpha.11" + "@storybook/client-logger" "6.0.0-alpha.11" + "@storybook/core-events" "6.0.0-alpha.11" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.0.0-alpha.11" + "@storybook/theming" "6.0.0-alpha.11" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + prop-types "^15.6.2" + react "^16.8.3" + semver "^6.0.0" + shallow-equal "^1.1.0" + store2 "^2.7.1" + telejson "^3.2.0" + util-deprecate "^1.0.2" + "@storybook/channel-postmessage@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.0-alpha.0.tgz#dec7245e9be7b1303c8bd60e664134a56639ae54" @@ -3405,6 +3444,17 @@ global "^4.3.2" telejson "^3.2.0" +"@storybook/channel-postmessage@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.0-alpha.11.tgz#16e032c5394714d0af3a70d144c8864d07b65b53" + integrity sha512-ylaoQFUx7tnv9ShmKmsAPTfENRSlOGNHyey6QkXMN9TOnBdfuTXL/KGSDOffU4U8WzeOdbJMeEiMlxcptpTamQ== + dependencies: + "@storybook/channels" "6.0.0-alpha.11" + "@storybook/client-logger" "6.0.0-alpha.11" + core-js "^3.0.1" + global "^4.3.2" + telejson "^3.2.0" + "@storybook/channel-websocket@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.0.0-alpha.0.tgz#81d10287250b76b0ab5856cb96c4827714f31875" @@ -3422,6 +3472,13 @@ dependencies: core-js "^3.0.1" +"@storybook/channels@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.0-alpha.11.tgz#d0562305f3abda28d77990653a3f0c1a800937a4" + integrity sha512-lUpHhEz+/fbnbijl6g507YgVKYo/oq2fHZPDQLGO7lGlPVrMpC4oM898sYwyfCIFWzuhF6nA9nltlfvCa3Ckog== + dependencies: + core-js "^3.0.1" + "@storybook/client-api@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.0-alpha.0.tgz#9a6766a9878ee1f41b35debd93d480fef50506c2" @@ -3445,6 +3502,29 @@ ts-dedent "^1.1.0" util-deprecate "^1.0.2" +"@storybook/client-api@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.0-alpha.11.tgz#92c1df63db8a4c025ac9ac8462e3ac46bb228ddc" + integrity sha512-+iiPy+pbGHkfevll6X1CUjiGEvpPYQQ+gyj43h6K5/X8ASOfZrR3mlrlNUO8zCj8/c4S9qpb2R2UpLxX3qsKyQ== + dependencies: + "@storybook/addons" "6.0.0-alpha.11" + "@storybook/channel-postmessage" "6.0.0-alpha.11" + "@storybook/channels" "6.0.0-alpha.11" + "@storybook/client-logger" "6.0.0-alpha.11" + "@storybook/core-events" "6.0.0-alpha.11" + "@storybook/csf" "0.0.1" + "@types/webpack-env" "^1.15.1" + core-js "^3.0.1" + eventemitter3 "^4.0.0" + global "^4.3.2" + is-plain-object "^3.0.0" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + stable "^0.1.8" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + "@storybook/client-logger@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.0-alpha.0.tgz#c8dd075d5a4a78158f6acb8ace14cf0492f2af16" @@ -3452,6 +3532,39 @@ dependencies: core-js "^3.0.1" +"@storybook/client-logger@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.0-alpha.11.tgz#fe3e294e02578d9c397cb1ff28b39d6e546f1490" + integrity sha512-0LVv/0grLVvIJZa8XDEi91G4BEAzIbtiYL/luy9NYb4x+1stMZ51gcTonRJOSU1XdLFYoRC7vf0UJuPhXL385Q== + dependencies: + core-js "^3.0.1" + +"@storybook/components@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.0-alpha.11.tgz#a4dd7e2eb4852f2c3ea2833001da9103e389ad3e" + integrity sha512-0scYwtNAbEAanYixYjw2p6XePmHVtDs20ShecgT0DQHqeidDsiR/i33NFc5J5v92LKlCdAzexF/osCScwjQ6hA== + dependencies: + "@storybook/client-logger" "6.0.0-alpha.11" + "@storybook/theming" "6.0.0-alpha.11" + "@types/react-textarea-autosize" "^4.3.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.9.1" + memoizerific "^1.11.3" + polished "^3.4.4" + popper.js "^1.14.7" + prop-types "^15.7.2" + react "^16.8.3" + react-dom "^16.8.3" + react-focus-lock "^2.1.0" + react-helmet-async "^1.0.2" + react-popper-tooltip "^2.8.3" + react-syntax-highlighter "^11.0.2" + react-textarea-autosize "^7.1.0" + simplebar-react "^1.0.0-alpha.6" + ts-dedent "^1.1.1" + "@storybook/core-events@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.0-alpha.0.tgz#a35d2f96f6d38aba080d7f5f1ec67799325728de" @@ -3459,6 +3572,97 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.0-alpha.11.tgz#58ec5590f4507099dc82088368239bc1de3407ff" + integrity sha512-iVZiR8qXGkyB+QGvD4iW4miU7sbFP3dIf0a0PEvqieKxgRQweQzlrVREhHVbbVE9MZu3Ytke9qNQiLGKStDScA== + dependencies: + core-js "^3.0.1" + +"@storybook/core@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.0-alpha.11.tgz#9413620cb1a560de7dd58e92a0634282fbbcaa1a" + integrity sha512-pNSgAD1M05FV43eigUb0jRf/jmxPLNx+bX+hEvHjF+hs/hRPgZbkhr4Od1DWfQbzGZ5HxnEauZR0FI1l+iScrg== + dependencies: + "@babel/plugin-proposal-class-properties" "^7.8.3" + "@babel/plugin-proposal-export-default-from" "^7.8.3" + "@babel/plugin-proposal-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-react-constant-elements" "^7.2.0" + "@babel/preset-env" "^7.8.4" + "@babel/preset-react" "^7.8.3" + "@babel/preset-typescript" "^7.8.3" + "@storybook/addons" "6.0.0-alpha.11" + "@storybook/channel-postmessage" "6.0.0-alpha.11" + "@storybook/client-api" "6.0.0-alpha.11" + "@storybook/client-logger" "6.0.0-alpha.11" + "@storybook/core-events" "6.0.0-alpha.11" + "@storybook/csf" "0.0.1" + "@storybook/node-logger" "6.0.0-alpha.11" + "@storybook/router" "6.0.0-alpha.11" + "@storybook/theming" "6.0.0-alpha.11" + "@storybook/ui" "6.0.0-alpha.11" + airbnb-js-shims "^2.2.1" + ansi-to-html "^0.6.11" + autoprefixer "^9.7.2" + babel-loader "^8.0.6" + babel-plugin-add-react-displayname "^0.0.5" + babel-plugin-emotion "^10.0.20" + babel-plugin-macros "^2.8.0" + babel-preset-minify "^0.5.0 || 0.6.0-alpha.5" + boxen "^4.1.0" + case-sensitive-paths-webpack-plugin "^2.2.0" + chalk "^3.0.0" + cli-table3 "0.5.1" + commander "^4.0.1" + core-js "^3.0.1" + corejs-upgrade-webpack-plugin "^3.0.1" + css-loader "^3.0.0" + detect-port "^1.3.0" + dotenv-webpack "^1.7.0" + ejs "^3.0.1" + express "^4.17.0" + file-loader "^5.0.2" + file-system-cache "^1.0.5" + find-cache-dir "^3.0.0" + find-up "^4.1.0" + fs-extra "^8.0.1" + glob-base "^0.3.0" + global "^4.3.2" + html-webpack-plugin "^4.0.0-beta.2" + inquirer "^7.0.0" + interpret "^2.0.0" + ip "^1.1.5" + json5 "^2.1.1" + lazy-universal-dotenv "^3.0.1" + micromatch "^4.0.2" + node-fetch "^2.6.0" + open "^7.0.1" + pkg-dir "^4.2.0" + pnp-webpack-plugin "1.6.0" + postcss-flexbugs-fixes "^4.1.0" + postcss-loader "^3.0.0" + pretty-hrtime "^1.0.3" + qs "^6.6.0" + raw-loader "^4.0.0" + react-dev-utils "^10.0.0" + regenerator-runtime "^0.13.3" + resolve "^1.11.0" + resolve-from "^5.0.0" + semver "^6.0.0" + serve-favicon "^2.5.0" + shelljs "^0.8.3" + style-loader "^1.0.0" + terser-webpack-plugin "^2.3.4" + ts-dedent "^1.1.1" + unfetch "^4.1.0" + url-loader "^3.0.0" + util-deprecate "^1.0.2" + webpack "^4.33.0" + webpack-dev-middleware "^3.7.0" + webpack-hot-middleware "^2.25.0" + webpack-virtual-modules "^0.2.0" + "@storybook/csf@0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.1.tgz#95901507dc02f0bc6f9ac8ee1983e2fc5bb98ce6" @@ -3504,6 +3708,18 @@ remark-lint "^6.0.4" remark-preset-lint-recommended "^3.0.2" +"@storybook/node-logger@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.0-alpha.11.tgz#eb2dcc1bbb66f5d546cdd8cfd0190517d054b519" + integrity sha512-zH3+VK8dmtm4DMqfoDNzdj3NspZ+I7df/btChAwplq/RrtVkqwnkMT/FNEO0RHHhCpsqAA+nhJa4fkl+qd48WQ== + dependencies: + "@types/npmlog" "^4.1.2" + chalk "^3.0.0" + core-js "^3.0.1" + npmlog "^4.1.2" + pretty-hrtime "^1.0.3" + regenerator-runtime "^0.13.3" + "@storybook/node-logger@^5.2.0", "@storybook/node-logger@^5.2.8": version "5.3.12" resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.3.12.tgz#df25b725583cb5aa799b6c5b8103a8ea0921d3ba" @@ -3542,6 +3758,33 @@ emotion-theming "^10.0.19" react-native-swipe-gestures "^1.0.4" +"@storybook/react@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.0-alpha.11.tgz#90fa2e430746e2f506b3cb11f8b9f6f5cd1c7f97" + integrity sha512-SJ1UDtiFEEzN6aXWXkJA8dEzynd7n3sF4fbsJwHPiUoE0n/SAlTAP4jN3KG7pZ/usLFDvNwfm4n9UjJv8THfwg== + dependencies: + "@babel/plugin-transform-react-constant-elements" "^7.6.3" + "@babel/preset-flow" "^7.0.0" + "@babel/preset-react" "^7.0.0" + "@storybook/addons" "6.0.0-alpha.11" + "@storybook/core" "6.0.0-alpha.11" + "@storybook/node-logger" "6.0.0-alpha.11" + "@svgr/webpack" "^5.1.0" + "@types/webpack-env" "^1.15.1" + babel-plugin-add-react-displayname "^0.0.5" + babel-plugin-named-asset-import "^0.3.1" + babel-plugin-react-docgen "^4.0.0" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + mini-css-extract-plugin "^0.9.0" + prop-types "^15.7.2" + react-dev-utils "^10.0.0" + regenerator-runtime "^0.13.3" + semver "^6.0.0" + ts-dedent "^1.1.1" + webpack "^4.33.0" + "@storybook/router@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.0-alpha.0.tgz#c2b9da6c7eba0dccf32228a72e23ecf8875aacac" @@ -3557,6 +3800,21 @@ qs "^6.6.0" util-deprecate "^1.0.2" +"@storybook/router@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.0-alpha.11.tgz#34991d5fcb5e609f2de6ecd88e291a9fa06e7f04" + integrity sha512-Sn6gEGELNLm9KNXtKbXUqVSWUzY3SErXiVIssMtFHEh+wgyVhKSSy9TrLyPIWQ08+sl2wf50zLogDPFyD47FFg== + dependencies: + "@reach/router" "^1.2.1" + "@storybook/csf" "0.0.1" + "@types/reach__router" "^1.2.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + util-deprecate "^1.0.2" + "@storybook/theming@6.0.0-alpha.0": version "6.0.0-alpha.0" resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.0-alpha.0.tgz#0a9580cecb1c9609e070c8db7737bd121faf56c3" @@ -3576,6 +3834,82 @@ resolve-from "^5.0.0" ts-dedent "^1.1.0" +"@storybook/theming@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.0-alpha.11.tgz#0eff2fc72754f9de941e16769f5e5759ad8c762f" + integrity sha512-TltXJJ9/mBOjVnCbaRf0SmXoAnj0DFcir4/2jhEnqBLWFvhP0vVoy6BNLnWVP6fxaonrt8IwA0l72jzAOd4qPQ== + dependencies: + "@emotion/core" "^10.0.20" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.17" + "@storybook/client-logger" "6.0.0-alpha.11" + core-js "^3.0.1" + deep-object-diff "^1.1.0" + emotion-theming "^10.0.19" + global "^4.3.2" + memoizerific "^1.11.3" + polished "^3.4.4" + prop-types "^15.7.2" + resolve-from "^5.0.0" + ts-dedent "^1.1.1" + +"@storybook/ui@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.0-alpha.11.tgz#8d67209c5cbf129881758927764d7e2106f6b0d0" + integrity sha512-Rs0dBVTfxt7GcKuRY2Zf7jd7j7cgLSxj7QBwqyD+MhlCky/v+3IsHsyLEJ0RdlIzZBvxW9eFJG9oXLTdxemv0Q== + dependencies: + "@emotion/core" "^10.0.20" + "@storybook/addons" "6.0.0-alpha.11" + "@storybook/api" "6.0.0-alpha.11" + "@storybook/channels" "6.0.0-alpha.11" + "@storybook/client-logger" "6.0.0-alpha.11" + "@storybook/components" "6.0.0-alpha.11" + "@storybook/core-events" "6.0.0-alpha.11" + "@storybook/router" "6.0.0-alpha.11" + "@storybook/theming" "6.0.0-alpha.11" + "@types/markdown-to-jsx" "^6.9.1" + copy-to-clipboard "^3.0.8" + core-js "^3.0.1" + core-js-pure "^3.0.1" + emotion-theming "^10.0.19" + fast-deep-equal "^3.1.1" + fuse.js "^3.4.6" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.9.3" + memoizerific "^1.11.3" + polished "^3.4.4" + prop-types "^15.7.2" + qs "^6.6.0" + react "^16.8.3" + react-dom "^16.8.3" + react-draggable "^4.0.3" + react-helmet-async "^1.0.2" + react-hotkeys "2.0.0" + react-sizeme "^2.6.7" + regenerator-runtime "^0.13.2" + resolve-from "^5.0.0" + semver "^6.0.0" + store2 "^2.7.1" + telejson "^3.2.0" + util-deprecate "^1.0.2" + +"@storybook/web-components@6.0.0-alpha.11": + version "6.0.0-alpha.11" + resolved "https://registry.yarnpkg.com/@storybook/web-components/-/web-components-6.0.0-alpha.11.tgz#7dad3774c94defac61bff4e2b1155e8debec11e8" + integrity sha512-T+Yx5ti18BiCJBuIFgkqcvoBxRGuHCiHcIX/O5jvduBn8AUX8LVVkpURauPNejJAzwR8cp1Wpnu5Nty8osCf0Q== + dependencies: + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/plugin-syntax-import-meta" "^7.2.0" + "@storybook/addons" "6.0.0-alpha.11" + "@storybook/core" "6.0.0-alpha.11" + "@types/webpack-env" "^1.15.1" + babel-plugin-bundled-import-meta "^0.3.1" + core-js "^3.0.1" + global "^4.3.2" + regenerator-runtime "^0.13.3" + ts-dedent "^1.1.1" + "@stroncium/procfs@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@stroncium/procfs/-/procfs-1.1.1.tgz#6ae4b4938cf3b6c3fb957f3063b2c0a8b26ee599" @@ -6208,7 +6542,7 @@ babel-plugin-named-asset-import@^0.3.1, babel-plugin-named-asset-import@^0.3.2, resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz#c9750a1b38d85112c9e166bf3ef7c5dbc605f4be" integrity sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA== -babel-plugin-react-docgen@^4.1.0: +babel-plugin-react-docgen@^4.0.0, babel-plugin-react-docgen@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.1.0.tgz#1dfa447dac9ca32d625a123df5733a9e47287c26" integrity sha512-vzpnBlfGv8XOhJM2zbPyyqw2OLEbelgZZsaaRRTpVwNKuYuc+pUg4+dy7i9gCRms0uOQn4osX571HRcCJMJCmA== From 1f0de2670eb825dbffbb5ee999b653577309e192 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 17:09:04 +1100 Subject: [PATCH 12/39] Implemented `parameters.passArgsFirst` --- lib/addons/src/types.ts | 8 +++++- lib/client-api/src/story_store.test.ts | 35 ++++++++++++++++++++++++++ lib/client-api/src/story_store.ts | 7 +++--- lib/client-api/src/types.ts | 7 ++---- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts index 53596e999511..a9b3e93c7455 100644 --- a/lib/addons/src/types.ts +++ b/lib/addons/src/types.ts @@ -22,12 +22,16 @@ export interface Parameters { [key: string]: any; } +export interface Args { + [key: string]: any; +} export interface StoryContext { id: string; name: string; kind: string; [key: string]: any; parameters: Parameters; + args: Args; hooks?: HooksContext; } @@ -60,7 +64,9 @@ export interface OptionsParameter extends Object { } export type StoryGetter = (context: StoryContext) => any; -export type StoryFn = (p?: StoryContext) => ReturnType; +export type StoryFn = + | ((p?: StoryContext) => ReturnType) + | ((a: Args, p?: StoryContext) => ReturnType); export type StoryWrapper = ( getStory: StoryGetter, diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index e6f089876034..7ab8ed391b83 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -152,6 +152,41 @@ describe('preview.story_store', () => { expect(store.getRawStory('a', '1').args).toEqual({ foo: 'bar' }); }); + + it('passes args as the first argument to the story if `parameters.passArgsFirst` is true', () => { + const store = new StoryStore({ channel }); + + store.addKindMetadata('a', { + parameters: { + argTypes: { + a: { defaultValue: 1 }, + }, + }, + decorators: [], + }); + + const storyOne = jest.fn(); + addStoryToStore(store, 'a', '1', storyOne); + + store.getRawStory('a', '1').storyFn(); + expect(storyOne).toHaveBeenCalledWith( + expect.objectContaining({ + args: { a: 1 }, + parameters: expect.objectContaining({}), + }) + ); + + const storyTwo = jest.fn(); + addStoryToStore(store, 'a', '2', storyTwo, { passArgsFirst: true }); + store.getRawStory('a', '2').storyFn(); + expect(storyTwo).toHaveBeenCalledWith( + { a: 1 }, + expect.objectContaining({ + args: { a: 1 }, + parameters: expect.objectContaining({}), + }) + ); + }); }); describe('parameterEnhancer', () => { diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 473b5fbe1c42..2e6ac37f11b2 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -195,10 +195,11 @@ export default class StoryStore extends EventEmitter { parametersBeforeEnhancement ); + let finalStoryFn = original; + if (parameters.passArgsFirst) finalStoryFn = context => original(context.args, context); + // lazily decorate the story when it's loaded - const getDecorated: () => StoryFn = memoize(1)(() => - applyDecorators(getOriginal(), decorators) - ); + const getDecorated: () => StoryFn = memoize(1)(() => applyDecorators(finalStoryFn, decorators)); const hooks = new HooksContext(); diff --git a/lib/client-api/src/types.ts b/lib/client-api/src/types.ts index 2927149b647d..debe91e5fc4e 100644 --- a/lib/client-api/src/types.ts +++ b/lib/client-api/src/types.ts @@ -3,6 +3,7 @@ import { StoryFn, StoryContext, Parameters, + Args, StoryApi, DecoratorFunction, } from '@storybook/addons'; @@ -20,10 +21,6 @@ export interface StoryMetadata { decorators: DecoratorFunction[]; } -export interface StoryArgs { - [key: string]: any; -} - export interface StoreItem extends StoryContext { id: string; kind: string; @@ -34,7 +31,7 @@ export interface StoreItem extends StoryContext { storyFn: StoryFn; hooks: HooksContext; parameters: Parameters; - args: StoryArgs; + args: Args; } export interface StoreData { From 4135503de51aed8275437823aaa84452310b7fec Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 17:16:45 +1100 Subject: [PATCH 13/39] Allow importing addParameters/Decorators from client-api after all --- MIGRATION.md | 11 ----------- lib/client-api/src/client_api.ts | 26 +++++++++++++++++--------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index e4a7e5885427..0bffcf9d7b6c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -7,7 +7,6 @@ - [Removed legacy story APIs](#removed-legacy-story-apis) - [Can no longer add decorators/parameters after stories](#can-no-longer-add-decorators-parameters-after-stories) - [Changed Parameter Handling](#changed-parameter-handling) - - [Removed addDecorator and addParameter exports from client-api](#removed-addDecorator-and-addParameter-exports-from-client-api) - [From version 5.2.x to 5.3.x](#from-version-52x-to-53x) - [To main.js configuration](#to-mainjs-configuration) - [Create React App preset](#create-react-app-preset) @@ -182,16 +181,6 @@ _You cannot set parameters from decorators_ Parameters are intended to be statically set at story load time. So setting them via a decorator doesn't quite make sense. If you were using this to control the rendering of a story, chances are using the new `args` feature is a more idiomatic way to do this. -### Removed addDecorator and addParameter exports from client-api - -Previously, to aid in the development of presets, we exported `addDecorator` and `addParameters` from `@storybook/client-api`. This is no longer necessary as you can now export decorators and parameters directly from config entries: - -```js -import { myDecorator } from './path/to/myDecorator'; - -export const decorators = [myDecorator]; -``` - ## From version 5.2.x to 5.3.x ### To main.js configuration diff --git a/lib/client-api/src/client_api.ts b/lib/client-api/src/client_api.ts index bd84c6d6b7f6..513724f5ca08 100644 --- a/lib/client-api/src/client_api.ts +++ b/lib/client-api/src/client_api.ts @@ -8,13 +8,21 @@ import { applyHooks } from './hooks'; import StoryStore from './story_store'; import { defaultDecorateStory } from './decorators'; -export const addDecorator = () => { - throw new Error(`Global addDecorator is no longer exported by client-api. Please just export your decorators. -Read more here: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#removed-addDecorator-and-addParameter-exports-from-client-api`); +// ClientApi (and StoreStore) are really singletons. However they are not created until the +// relevant framework instanciates them via `start.js`. The good news is this happens right away. +let singleton: ClientApi; + +export const addDecorator = (decorator: DecoratorFunction) => { + if (!singleton) + throw new Error(`Singleton client API not yet initialized, cannot call addDecorator`); + + singleton.addDecorator(decorator); }; -export const addParameters = () => { - throw new Error(`Global addParameters is no longer exported by client-api. Please just export your decorators. -Read more here: https://github.com/storybookjs/storybook/blob/master/MIGRATION.md#removed-addDecorator-and-addParameter-exports-from-client-api`); +export const addParameters = (parameters: Parameters) => { + if (!singleton) + throw new Error(`Singleton client API not yet initialized, cannot call addParameters`); + + singleton.addParameters(parameters); }; export default class ClientApi { @@ -30,9 +38,9 @@ export default class ClientApi { this._decorateStory = decorateStory; - if (!storyStore) { - throw new Error('storyStore is required'); - } + if (!storyStore) throw new Error('storyStore is required'); + + singleton = this; } setAddon = (addon: any) => { From 95f713e84992e40ca83a6a0665fc397749eb9fb4 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 17:22:28 +1100 Subject: [PATCH 14/39] Add addParameterEnhancer export to client api --- lib/client-api/src/client_api.ts | 19 ++++++++++++++++++- lib/client-api/src/index.ts | 5 ++++- lib/client-api/src/story_store.ts | 6 ++---- lib/client-api/src/types.ts | 2 ++ .../server/preview/iframe-webpack.config.js | 5 +++-- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/client-api/src/client_api.ts b/lib/client-api/src/client_api.ts index 513724f5ca08..d9f2c3c5d5cb 100644 --- a/lib/client-api/src/client_api.ts +++ b/lib/client-api/src/client_api.ts @@ -3,7 +3,13 @@ import { logger } from '@storybook/client-logger'; import { StoryFn, Parameters } from '@storybook/addons'; import { toId } from '@storybook/csf'; -import { ClientApiParams, DecoratorFunction, ClientApiAddons, StoryApi } from './types'; +import { + ClientApiParams, + DecoratorFunction, + ClientApiAddons, + StoryApi, + ParameterEnhancer, +} from './types'; import { applyHooks } from './hooks'; import StoryStore from './story_store'; import { defaultDecorateStory } from './decorators'; @@ -25,6 +31,13 @@ export const addParameters = (parameters: Parameters) => { singleton.addParameters(parameters); }; +export const addParameterEnhancer = (enhancer: ParameterEnhancer) => { + if (!singleton) + throw new Error(`Singleton client API not yet initialized, cannot call addParameterEnhancer`); + + singleton.addParameterEnhancer(enhancer); +}; + export default class ClientApi { private _storyStore: StoryStore; @@ -83,6 +96,10 @@ export default class ClientApi { this._storyStore.addGlobalMetadata({ decorators: [], parameters }); }; + addParameterEnhancer = (enhancer: ParameterEnhancer) => { + this._storyStore.addParameterEnhancer(enhancer); + }; + clearDecorators = () => { this._storyStore.clearGlobalDecorators(); }; diff --git a/lib/client-api/src/index.ts b/lib/client-api/src/index.ts index 750d9a17ca6d..b4d0d92c4953 100644 --- a/lib/client-api/src/index.ts +++ b/lib/client-api/src/index.ts @@ -1,4 +1,4 @@ -import ClientApi from './client_api'; +import ClientApi, { addDecorator, addParameters, addParameterEnhancer } from './client_api'; import { defaultDecorateStory } from './decorators'; import StoryStore from './story_store'; import ConfigApi from './config_api'; @@ -10,6 +10,9 @@ export * from './hooks'; export { ClientApi, + addDecorator, + addParameters, + addParameterEnhancer, StoryStore, ConfigApi, defaultDecorateStory, diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 2e6ac37f11b2..0fb458ba3f3c 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -8,16 +8,16 @@ import stable from 'stable'; import { Channel } from '@storybook/channels'; import Events from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; -import { Comparator, Parameters, StoryFn, StoryContext } from '@storybook/addons'; +import { Comparator, Parameters, Args, StoryFn, StoryContext } from '@storybook/addons'; import { DecoratorFunction, StoryMetadata, StoreData, AddStoryArgs, StoreItem, - StoryArgs, ErrorLike, GetStorybookKind, + ParameterEnhancer, } from './types'; import { HooksContext } from './hooks'; import storySort from './storySort'; @@ -34,8 +34,6 @@ interface StoryOptions { type KindMetadata = StoryMetadata & { order: number }; -type ParameterEnhancer = (context: StoryContext) => Parameters; - const isStoryDocsOnly = (parameters?: Parameters) => { return parameters && parameters.docsOnly; }; diff --git a/lib/client-api/src/types.ts b/lib/client-api/src/types.ts index debe91e5fc4e..f3e625240f74 100644 --- a/lib/client-api/src/types.ts +++ b/lib/client-api/src/types.ts @@ -21,6 +21,8 @@ export interface StoryMetadata { decorators: DecoratorFunction[]; } +export type ParameterEnhancer = (context: StoryContext) => Parameters; + export interface StoreItem extends StoryContext { id: string; kind: string; diff --git a/lib/core/src/server/preview/iframe-webpack.config.js b/lib/core/src/server/preview/iframe-webpack.config.js index 5466c277f0b3..000ff2494a70 100644 --- a/lib/core/src/server/preview/iframe-webpack.config.js +++ b/lib/core/src/server/preview/iframe-webpack.config.js @@ -46,12 +46,13 @@ export default ({ if (match) { const configFilename = match[1]; virtualModuleMapping[entryFilename] = ` - import { addDecorator, addParameters } from '@storybook/${framework}'; + import { addDecorator, addParameters, addParameterEnhancer } from '@storybook/client-api'; - const { decorators, parameters } = require('${configFilename}'); + const { decorators, parameters, parameterEnhancers } = require('${configFilename}'); if (decorators) decorators.forEach(decorator => addDecorator(decorator)); if (parameters) addParameters(parameters); + if (parameterEnhancers) parameterEnhancers.forEach(enhancer => addParameterEnhancer(enhancer)); `; } }); From 833c13becb9f4777051aa07ee085d3668af1c9d1 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 17:30:32 +1100 Subject: [PATCH 15/39] Type fixes --- lib/addons/src/types.ts | 7 ++++--- lib/client-api/src/story_store.ts | 24 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts index a9b3e93c7455..88b5282f32b8 100644 --- a/lib/addons/src/types.ts +++ b/lib/addons/src/types.ts @@ -64,9 +64,10 @@ export interface OptionsParameter extends Object { } export type StoryGetter = (context: StoryContext) => any; -export type StoryFn = - | ((p?: StoryContext) => ReturnType) - | ((a: Args, p?: StoryContext) => ReturnType); + +export type LegacyStoryFn = (p?: StoryContext) => ReturnType; +export type ArgsStoryFn = (a: Args, p?: StoryContext) => ReturnType; +export type StoryFn = LegacyStoryFn | ArgsStoryFn; export type StoryWrapper = ( getStory: StoryGetter, diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 0fb458ba3f3c..e245719e654b 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -8,7 +8,15 @@ import stable from 'stable'; import { Channel } from '@storybook/channels'; import Events from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; -import { Comparator, Parameters, Args, StoryFn, StoryContext } from '@storybook/addons'; +import { + Comparator, + Parameters, + Args, + LegacyStoryFn, + ArgsStoryFn, + StoryFn, + StoryContext, +} from '@storybook/addons'; import { DecoratorFunction, StoryMetadata, @@ -94,7 +102,7 @@ export default class StoryStore extends EventEmitter { setChannel = (channel: Channel) => { this._channel = channel; - channel.on(Events.CHANGE_STORY_ARGS, (id: string, newArgs: StoryArgs) => + channel.on(Events.CHANGE_STORY_ARGS, (id: string, newArgs: Args) => this.setStoryArgs(id, newArgs) ); }; @@ -188,13 +196,17 @@ export default class StoryStore extends EventEmitter { (accumlatedParameters, enhancer) => combineParameters( accumlatedParameters, - enhancer({ ...identification, parameters: accumlatedParameters }) + enhancer({ ...identification, parameters: accumlatedParameters, args: {} }) ), parametersBeforeEnhancement ); - let finalStoryFn = original; - if (parameters.passArgsFirst) finalStoryFn = context => original(context.args, context); + let finalStoryFn: LegacyStoryFn; + if (parameters.passArgsFirst) { + finalStoryFn = (context: StoryContext) => (original as ArgsStoryFn)(context.args, context); + } else { + finalStoryFn = original as LegacyStoryFn; + } // lazily decorate the story when it's loaded const getDecorated: () => StoryFn = memoize(1)(() => applyDecorators(finalStoryFn, decorators)); @@ -390,7 +402,7 @@ export default class StoryStore extends EventEmitter { this.getStoriesForKind(kind).map(story => this.cleanHooks(story.id)); } - setStoryArgs(id: string, newArgs: StoryArgs) { + setStoryArgs(id: string, newArgs: Args) { if (!this._stories[id]) throw new Error(`No story for id ${id}`); const { args } = this._stories[id]; this._stories[id].args = { ...args, ...newArgs }; From c7f7f199b9f67413d6b1c3cb12b24761974ed680 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 20:04:50 +1100 Subject: [PATCH 16/39] Fixes --- lib/client-api/src/decorators.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/client-api/src/decorators.ts b/lib/client-api/src/decorators.ts index 5cd00d4a3ba2..5084aa7f5297 100644 --- a/lib/client-api/src/decorators.ts +++ b/lib/client-api/src/decorators.ts @@ -6,6 +6,7 @@ const defaultContext: StoryContext = { name: 'unspecified', kind: 'unspecified', parameters: {}, + args: {}, }; export const defaultDecorateStory = (storyFn: StoryFn, decorators: DecoratorFunction[]) => @@ -13,7 +14,7 @@ export const defaultDecorateStory = (storyFn: StoryFn, decorators: DecoratorFunc (decorated, decorator) => (context: StoryContext = defaultContext) => decorator( // You cannot override the parameters key, it is fixed - ({ parameters, ...innerContext }: StoryContext) => + ({ parameters, ...innerContext }: StoryContext = {} as StoryContext) => decorated( innerContext ? { From e54646c1dbcec60ec162babce9eb108e94f7421d Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 20:05:16 +1100 Subject: [PATCH 17/39] Update state=>args.stories --- .../stories/core/args.stories.js | 19 +++++++++++++++++++ .../stories/core/state.stories.js | 16 ---------------- 2 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 examples/official-storybook/stories/core/args.stories.js delete mode 100644 examples/official-storybook/stories/core/state.stories.js diff --git a/examples/official-storybook/stories/core/args.stories.js b/examples/official-storybook/stories/core/args.stories.js new file mode 100644 index 000000000000..b78faf79735c --- /dev/null +++ b/examples/official-storybook/stories/core/args.stories.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default { + title: 'Core/Args', + parameters: { + passArgsFirst: true, + }, +}; + +export const PassedToStory = ({ name }) =>
The value of the name field is {name}
; + +PassedToStory.story = { + parameters: { argTypes: { name: { defaultValue: 'initial' } } }, +}; + +PassedToStory.propTypes = { + name: PropTypes.string.isRequired, +}; diff --git a/examples/official-storybook/stories/core/state.stories.js b/examples/official-storybook/stories/core/state.stories.js deleted file mode 100644 index 6d2b181f2966..000000000000 --- a/examples/official-storybook/stories/core/state.stories.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default { - title: 'Core/State', -}; - -export const PassedToStory = ({ state: { name } }) => ( -
The value of the name field is {name}
-); - -PassedToStory.propTypes = { - state: PropTypes.shape({ - name: PropTypes.string, - }).isRequired, -}; From a0b56631fad80894755473cc6a9319118f63626e Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 21:15:36 +1100 Subject: [PATCH 18/39] Fix up vue decorators --- app/vue/src/client/preview/index.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/app/vue/src/client/preview/index.ts b/app/vue/src/client/preview/index.ts index f00ff8338602..4e515081a915 100644 --- a/app/vue/src/client/preview/index.ts +++ b/app/vue/src/client/preview/index.ts @@ -68,6 +68,7 @@ const defaultContext: StoryContext = { name: 'unspecified', kind: 'unspecified', parameters: {}, + args: {}, }; function decorateStory( @@ -78,22 +79,13 @@ function decorateStory( (decorated: StoryFn, decorator) => (context: StoryContext = defaultContext) => { let story; - const decoratedStory = decorator(p => { - story = decorated( - p - ? { - ...context, - ...p, - parameters: { - ...context.parameters, - ...p.parameters, - }, - } - : context - ); - - return story; - }, context); + const decoratedStory = decorator( + ({ parameters, ...innerContext }: StoryContext = {} as StoryContext) => { + story = decorated({ ...context, ...innerContext }); + return story; + }, + context + ); if (!story) { story = decorated(context); From 13c76973733122510b78be0be320aa95fb0abdeb Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 18 Feb 2020 21:24:10 +1100 Subject: [PATCH 19/39] Add useArgs --- .../stories/core/args.stories.js | 49 +++++++++++++++++-- lib/addons/src/hooks.ts | 22 ++++++++- lib/addons/src/types.ts | 2 +- lib/client-api/src/hooks.ts | 2 + 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/examples/official-storybook/stories/core/args.stories.js b/examples/official-storybook/stories/core/args.stories.js index b78faf79735c..fac8eee113d0 100644 --- a/examples/official-storybook/stories/core/args.stories.js +++ b/examples/official-storybook/stories/core/args.stories.js @@ -1,19 +1,62 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; +import { useArgs } from '@storybook/client-api'; + +// eslint-disable-next-line react/prop-types +const ArgUpdater = ({ args, updateArgs }) => { + const [argsInput, updateArgsInput] = useState(JSON.stringify(args)); + + return ( +
+

Hooks args:

+
{JSON.stringify(args)}
+
{ + e.preventDefault(); + updateArgs(JSON.parse(argsInput)); + }} + > +