diff --git a/app/vue/src/client/preview/index.ts b/app/vue/src/client/preview/index.ts
index b3188e051a28..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(
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..fac8eee113d0
--- /dev/null
+++ b/examples/official-storybook/stories/core/args.stories.js
@@ -0,0 +1,62 @@
+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)}
+
+
+ );
+};
+
+export default {
+ title: 'Core/Args',
+ parameters: {
+ passArgsFirst: true,
+ },
+ decorators: [
+ story => {
+ const [args, updateArgs] = useArgs();
+
+ return (
+ <>
+ {story()}
+
+ >
+ );
+ },
+ ],
+};
+
+export const PassedToStory = inputArgs => {
+ return (
+
+
Input args:
+
{JSON.stringify(inputArgs)}
+
+ );
+};
+
+PassedToStory.story = {
+ parameters: { argTypes: { name: { defaultValue: 'initial' } } },
+};
+
+PassedToStory.propTypes = {
+ args: PropTypes.shape({}).isRequired,
+};
diff --git a/examples/vue-kitchen-sink/src/stories/__snapshots__/custom-decorators.stories.storyshot b/examples/vue-kitchen-sink/src/stories/__snapshots__/custom-decorators.stories.storyshot
index 50d60b7918c6..8488aec4425e 100644
--- a/examples/vue-kitchen-sink/src/stories/__snapshots__/custom-decorators.stories.storyshot
+++ b/examples/vue-kitchen-sink/src/stories/__snapshots__/custom-decorators.stories.storyshot
@@ -56,6 +56,7 @@ exports[`Storyshots Custom/Decorator for Vue With Data 1`] = `
"framework": "vue",
"__id": "custom-decorator-for-vue--with-data"
},
+ "args": {},
"customContext": 52
}
diff --git a/lib/addons/src/hooks.ts b/lib/addons/src/hooks.ts
index ebf64215b6bd..8ad5787b4698 100644
--- a/lib/addons/src/hooks.ts
+++ b/lib/addons/src/hooks.ts
@@ -1,8 +1,13 @@
import window from 'global';
import { logger } from '@storybook/client-logger';
-import { FORCE_RE_RENDER, STORY_RENDERED, DOCS_RENDERED } from '@storybook/core-events';
+import {
+ FORCE_RE_RENDER,
+ STORY_RENDERED,
+ DOCS_RENDERED,
+ UPDATE_STORY_ARGS,
+} from '@storybook/core-events';
import { addons } from './index';
-import { StoryGetter, StoryContext } from './types';
+import { StoryGetter, StoryContext, Args } from './types';
interface StoryStore {
fromId: (
@@ -409,3 +414,16 @@ export function useParameter(parameterKey: string, defaultValue?: S): S | und
}
return undefined;
}
+
+/* Returns current value of story args */
+export function useArgs(): [Args, (newArgs: Args) => void] {
+ const channel = addons.getChannel();
+ const { id: storyId, args } = useStoryContext();
+
+ const updateArgs = useCallback(
+ (newArgs: Args) => channel.emit(UPDATE_STORY_ARGS, storyId, newArgs),
+ [channel, storyId]
+ );
+
+ return [args, updateArgs];
+}
diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts
index 4409a500403a..1bb5282b4125 100644
--- a/lib/addons/src/types.ts
+++ b/lib/addons/src/types.ts
@@ -27,6 +27,12 @@ export interface Parameters {
[key: string]: any;
}
+// This is duplicated in @storybook/api because there is no common place to put types (manager/preview)
+// We cannot import from @storybook/api here because it will lead to manager code (i.e. emotion) imported in the preview
+export interface Args {
+ [key: string]: any;
+}
+
export interface StoryIdentifier {
id: StoryId;
kind: StoryKind;
@@ -36,6 +42,7 @@ export interface StoryIdentifier {
export interface StoryContext extends StoryIdentifier {
[key: string]: any;
parameters: Parameters;
+ args: Args;
hooks?: HooksContext;
}
@@ -68,7 +75,10 @@ export interface OptionsParameter extends Object {
}
export type StoryGetter = (context: StoryContext) => any;
-export type StoryFn = (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/api/src/index.tsx b/lib/api/src/index.tsx
index f7fc885edc83..919a3bc66a99 100644
--- a/lib/api/src/index.tsx
+++ b/lib/api/src/index.tsx
@@ -119,6 +119,10 @@ interface Children {
children: ReactNode | ((props: Combo) => ReactNode);
}
+export interface Args {
+ [key: string]: any;
+}
+
type StatePartial = Partial;
export type ManagerProviderProps = Children & RouterData & ProviderData & DocsModeData;
@@ -423,3 +427,13 @@ export function useStoryState(defaultState?: S) {
const { storyId } = useStorybookState();
return useSharedState(`story-state-${storyId}`, defaultState);
}
+
+export function useArgs(): [Args, (newArgs: Args) => void] {
+ const {
+ api: { getCurrentStoryData, updateStoryArgs },
+ } = useStorybookApi();
+
+ const { id, args } = getCurrentStoryData();
+
+ return [args, (newArgs: Args) => updateStoryArgs(id, newArgs)];
+}
diff --git a/lib/api/src/lib/stories.ts b/lib/api/src/lib/stories.ts
index 4b0db52fd0a6..9fb835e5f6ff 100644
--- a/lib/api/src/lib/stories.ts
+++ b/lib/api/src/lib/stories.ts
@@ -1,6 +1,8 @@
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import { sanitize, parseKind } from '@storybook/csf';
+
+import { Args } from '../index';
import merge from './merge';
import { Provider } from '../init-provider-api';
@@ -58,6 +60,7 @@ export interface Story {
docsOnly?: boolean;
[k: string]: any;
};
+ args: Args;
}
export interface StoryInput {
diff --git a/lib/api/src/modules/stories.ts b/lib/api/src/modules/stories.ts
index 5f9d388eb14c..922fb64f83c9 100644
--- a/lib/api/src/modules/stories.ts
+++ b/lib/api/src/modules/stories.ts
@@ -1,5 +1,6 @@
import { DOCS_MODE } from 'global';
import { toId, sanitize } from '@storybook/csf';
+import { UPDATE_STORY_ARGS, STORY_ARGS_UPDATED } from '@storybook/core-events';
import {
transformStoriesRawToStoriesHash,
@@ -11,7 +12,7 @@ import {
isStory,
} from '../lib/stories';
-import { Module } from '../index';
+import { Module, API, Args } from '../index';
type Direction = -1 | 1;
type ParameterName = string;
@@ -46,6 +47,8 @@ const initStoriesApi = ({
storyId: initialStoryId,
viewMode: initialViewMode,
}: Module) => {
+ let fullApi: API;
+
const getData = (storyId: StoryId) => {
const { storiesHash } = store.getState();
@@ -235,6 +238,24 @@ const initStoriesApi = ({
}
};
+ const storyArgsChanged = (id: StoryId, args: Args) => {
+ const { storiesHash } = store.getState();
+ (storiesHash[id] as Story).args = args;
+ store.setState({ storiesHash });
+ };
+
+ const updateStoryArgs = (id: StoryId, newArgs: Args) => {
+ if (!fullApi) throw new Error('Cannot set story args until api has been initialized');
+
+ fullApi.emit(UPDATE_STORY_ARGS, id, newArgs);
+ };
+
+ function init({ api: inputFullApi }: { api: API }) {
+ fullApi = inputFullApi;
+
+ fullApi.on(STORY_ARGS_UPDATED, (id: StoryId, args: Args) => storyArgsChanged(id, args));
+ }
+
return {
api: {
storyId: toId,
@@ -246,6 +267,7 @@ const initStoriesApi = ({
getData,
getParameters,
getCurrentParameter,
+ updateStoryArgs,
},
state: {
storiesHash: {},
@@ -253,6 +275,7 @@ const initStoriesApi = ({
viewMode: initialViewMode,
storiesConfigured: false,
},
+ init,
};
};
export default initStoriesApi;
diff --git a/lib/api/src/tests/stories.test.js b/lib/api/src/tests/stories.test.js
index de238f1a64e2..ccfbc38e40b1 100644
--- a/lib/api/src/tests/stories.test.js
+++ b/lib/api/src/tests/stories.test.js
@@ -1,3 +1,6 @@
+import EventEmitter from 'event-emitter';
+import { STORY_ARGS_UPDATED, UPDATE_STORY_ARGS } from '@storybook/core-events';
+
import initStories from '../modules/stories';
function createMockStore() {
@@ -32,14 +35,15 @@ describe('stories API', () => {
});
const parameters = {};
const storiesHash = {
- 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1' },
- 'a--2': { kind: 'a', name: '2', parameters, path: 'a--2', id: 'a--2' },
+ 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1', args: {} },
+ 'a--2': { kind: 'a', name: '2', parameters, path: 'a--2', id: 'a--2', args: {} },
'b-c--1': {
kind: 'b/c',
name: '1',
parameters,
path: 'b-c--1',
id: 'b-c--1',
+ args: {},
},
'b-d--1': {
kind: 'b/d',
@@ -47,6 +51,7 @@ describe('stories API', () => {
parameters,
path: 'b-d--1',
id: 'b-d--1',
+ args: {},
},
'b-d--2': {
kind: 'b/d',
@@ -54,6 +59,7 @@ describe('stories API', () => {
parameters,
path: 'b-d--2',
id: 'b-d--2',
+ args: { a: 'b' },
},
'custom-id--1': {
kind: 'b/e',
@@ -61,6 +67,7 @@ describe('stories API', () => {
parameters,
path: 'custom-id--1',
id: 'custom-id--1',
+ args: {},
},
};
describe('setStories', () => {
@@ -103,6 +110,7 @@ describe('stories API', () => {
kind: 'a',
name: '1',
parameters,
+ args: {},
});
expect(storedStoriesHash['a--2']).toMatchObject({
@@ -111,6 +119,7 @@ describe('stories API', () => {
kind: 'a',
name: '2',
parameters,
+ args: {},
});
expect(storedStoriesHash.b).toMatchObject({
@@ -134,6 +143,7 @@ describe('stories API', () => {
kind: 'b/c',
name: '1',
parameters,
+ args: {},
});
expect(storedStoriesHash['b-d']).toMatchObject({
@@ -150,6 +160,7 @@ describe('stories API', () => {
kind: 'b/d',
name: '1',
parameters,
+ args: {},
});
expect(storedStoriesHash['b-d--2']).toMatchObject({
@@ -158,6 +169,7 @@ describe('stories API', () => {
kind: 'b/d',
name: '2',
parameters,
+ args: { a: 'b' },
});
expect(storedStoriesHash['b-e']).toMatchObject({
@@ -174,6 +186,7 @@ describe('stories API', () => {
kind: 'b/e',
name: '1',
parameters,
+ args: {},
});
});
@@ -193,6 +206,7 @@ describe('stories API', () => {
parameters: showRootsParameters,
path: 'a-b--1',
id: 'a-b--1',
+ args: {},
},
});
@@ -219,6 +233,7 @@ describe('stories API', () => {
kind: 'a/b',
name: '1',
parameters: showRootsParameters,
+ args: {},
});
});
@@ -238,6 +253,7 @@ describe('stories API', () => {
parameters: showRootsParameters,
path: 'a--1',
id: 'a--1',
+ args: {},
},
});
@@ -257,6 +273,7 @@ describe('stories API', () => {
kind: 'a',
name: '1',
parameters: showRootsParameters,
+ args: {},
});
});
@@ -269,14 +286,15 @@ describe('stories API', () => {
} = initStories({ store, navigate, provider });
setStories({
- 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1' },
- 'a--2': { kind: 'a', name: '2', parameters, path: 'a--2', id: 'a--2' },
+ 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1', args: {} },
+ 'a--2': { kind: 'a', name: '2', parameters, path: 'a--2', id: 'a--2', args: {} },
'b-c--1': {
kind: 'b|c',
name: '1',
parameters,
path: 'b-c--1',
id: 'b-c--1',
+ args: {},
},
'b-d--1': {
kind: 'b|d',
@@ -284,6 +302,7 @@ describe('stories API', () => {
parameters,
path: 'b-d--1',
id: 'b-d--1',
+ args: {},
},
'b-d--2': {
kind: 'b|d',
@@ -291,6 +310,7 @@ describe('stories API', () => {
parameters,
path: 'b-d--2',
id: 'b-d--2',
+ args: {},
},
});
const { storiesHash: storedStoriesHash } = store.getState();
@@ -328,6 +348,7 @@ describe('stories API', () => {
kind: 'b|c',
name: '1',
parameters,
+ args: {},
});
expect(storedStoriesHash['b-d--1']).toMatchObject({
@@ -336,6 +357,7 @@ describe('stories API', () => {
kind: 'b|d',
name: '1',
parameters,
+ args: {},
});
expect(storedStoriesHash['b-d--2']).toMatchObject({
@@ -344,6 +366,7 @@ describe('stories API', () => {
kind: 'b|d',
name: '2',
parameters,
+ args: {},
});
});
@@ -358,9 +381,9 @@ describe('stories API', () => {
} = initStories({ store, navigate, provider });
setStories({
- 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1' },
- 'b--1': { kind: 'b', name: '1', parameters, path: 'b--1', id: 'b--1' },
- 'a--2': { kind: 'a', name: '2', parameters, path: 'a--2', id: 'a--2' },
+ 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1', args: {} },
+ 'b--1': { kind: 'b', name: '1', parameters, path: 'b--1', id: 'b--1', args: {} },
+ 'a--2': { kind: 'a', name: '2', parameters, path: 'a--2', id: 'a--2', args: {} },
});
const { storiesHash: storedStoriesHash } = store.getState();
@@ -489,6 +512,78 @@ describe('stories API', () => {
);
});
+ describe('args handling', () => {
+ it('changes args properly, per story when receiving STORY_ARGS_UPDATED', () => {
+ const navigate = jest.fn();
+ const store = createMockStore();
+
+ const {
+ api: { setStories },
+ init,
+ } = initStories({ store, navigate, provider });
+
+ setStories({
+ 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1', args: { a: 'b' } },
+ 'b--1': { kind: 'b', name: '1', parameters, path: 'b--1', id: 'b--1', args: { x: 'y' } },
+ });
+
+ const { storiesHash: initialStoriesHash } = store.getState();
+ expect(initialStoriesHash['a--1'].args).toEqual({ a: 'b' });
+ expect(initialStoriesHash['b--1'].args).toEqual({ x: 'y' });
+
+ const api = new EventEmitter();
+ init({ api });
+
+ api.emit(STORY_ARGS_UPDATED, 'a--1', { foo: 'bar' });
+ const { storiesHash: changedStoriesHash } = store.getState();
+ expect(changedStoriesHash['a--1']).toEqual({
+ isLeaf: true,
+ parent: 'a',
+ kind: 'a',
+ name: '1',
+ parameters: {},
+ path: 'a--1',
+ id: 'a--1',
+ args: { foo: 'bar' },
+ });
+ expect(changedStoriesHash['b--1']).toEqual({
+ isLeaf: true,
+ parent: 'b',
+ kind: 'b',
+ name: '1',
+ parameters: {},
+ path: 'b--1',
+ id: 'b--1',
+ args: { x: 'y' },
+ });
+ });
+
+ it('updateStoryArgs emits UPDATE_STORY_ARGS and does not change anything', () => {
+ const navigate = jest.fn();
+ const store = createMockStore();
+
+ const {
+ api: { setStories, updateStoryArgs },
+ init,
+ } = initStories({ store, navigate, provider });
+
+ setStories({
+ 'a--1': { kind: 'a', name: '1', parameters, path: 'a--1', id: 'a--1', args: { a: 'b' } },
+ 'b--1': { kind: 'b', name: '1', parameters, path: 'b--1', id: 'b--1', args: { x: 'y' } },
+ });
+
+ const emit = jest.fn();
+ init({ api: { emit, on: jest.fn() } });
+
+ updateStoryArgs('a--1', { foo: 'bar' });
+ expect(emit).toHaveBeenCalledWith(UPDATE_STORY_ARGS, 'a--1', { foo: 'bar' });
+
+ const { storiesHash: changedStoriesHash } = store.getState();
+ expect(changedStoriesHash['a--1'].args).toEqual({ a: 'b' });
+ expect(changedStoriesHash['b--1'].args).toEqual({ x: 'y' });
+ });
+ });
+
describe('jumpToStory', () => {
it('works forward', () => {
const navigate = jest.fn();
diff --git a/lib/client-api/README.md b/lib/client-api/README.md
new file mode 100644
index 000000000000..e6a03570cc4e
--- /dev/null
+++ b/lib/client-api/README.md
@@ -0,0 +1,102 @@
+# `@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.
+- `args` - dynamic inputs to the story, see below.
+- `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:
+
+- `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.
+
+## 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` */
+```
+
+### Arg types and values
+
+Arg types are used by the docs addon to populate the props table and are documented there. They are controlled by `parameters.argTypes` and can (sometimes) be automatically inferred from type information about the story or the component rendered by the story.
+
+A story can set initial values of its args with the `parameters.args` parameter. If you set an initial value for an arg that doesn't have a type a simple type will be inferred from the value.
+
+The initial value for an arg named "X" will be either `parameters.args.X` (if set) or `parameters.argTypes.X.defaultValue`. If an arg doesn't have a default value or an initial value, it will start unset, although it can still be set later via user interaction.
+
+For instance, for this story:
+
+```js
+export MyStory = ....
+MyStory.story = { parameters: {
+ argTypes: {
+ primary: { defaultValue: true, /* other things */ },
+ size: { /* other things */ },
+ color: { /* other things */ },
+ },
+ args: {
+ size: 'large',
+ extra: 'prop',
+ }
+}}
+```
+
+Then `context.args` will default to `{ primary: true, size: 'large', extra: 'prop' }`.
+
+### 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 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 arg. If you need more complex information supporting that, use parameters or addon state.
+
+Both `@storybook/client-api` (preview) and `@storybook/api` (manager) export a `useArgs()` hook that you can use to access args in decorators or addon panels. The API is as follows:
+
+```js
+// `args` is the args of the currently rendered story
+// `updateArgs` will update its args. You can pass a subset of the args; other args will not be changed.
+const [args, updateArgs] = useArgs();
+```
diff --git a/lib/client-api/src/client_api.ts b/lib/client-api/src/client_api.ts
index 245665a097e1..b0b55260a91b 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, DecorateStoryFunction } 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;
@@ -92,6 +105,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/decorators.ts b/lib/client-api/src/decorators.ts
index 2bcf99508df3..7620af698ec7 100644
--- a/lib/client-api/src/decorators.ts
+++ b/lib/client-api/src/decorators.ts
@@ -10,6 +10,7 @@ const defaultContext: StoryContext = {
name: 'unspecified',
kind: 'unspecified',
parameters: {},
+ args: {},
};
export const defaultDecorateStory = (storyFn: StoryFn, decorators: DecoratorFunction[]) =>
diff --git a/lib/client-api/src/hooks.test.js b/lib/client-api/src/hooks.test.js
index d567035276ac..80edecf49554 100644
--- a/lib/client-api/src/hooks.test.js
+++ b/lib/client-api/src/hooks.test.js
@@ -1,4 +1,4 @@
-import { FORCE_RE_RENDER, STORY_RENDERED } from '@storybook/core-events';
+import { FORCE_RE_RENDER, STORY_RENDERED, UPDATE_STORY_ARGS } from '@storybook/core-events';
import addons from '@storybook/addons';
import { defaultDecorateStory } from './decorators';
import {
@@ -13,6 +13,7 @@ import {
useParameter,
useStoryContext,
HooksContext,
+ useArgs,
} from './hooks';
jest.mock('@storybook/client-logger', () => ({
@@ -492,4 +493,31 @@ describe('Preview hooks', () => {
expect(state).toBe(1);
});
});
+ describe('useArgs', () => {
+ it('will pull args from context', () => {
+ run(
+ () => {},
+ [
+ storyFn => {
+ expect(useArgs()[0]).toEqual({ a: 'b' });
+ return storyFn();
+ },
+ ],
+ { args: { a: 'b' } }
+ );
+ });
+ it('will emit UPDATE_STORY_ARGS when called', () => {
+ run(
+ () => {},
+ [
+ storyFn => {
+ useArgs()[1]({ a: 'b' });
+ expect(mockChannel.emit).toHaveBeenCalledWith(UPDATE_STORY_ARGS, '1', { a: 'b' });
+ return storyFn();
+ },
+ ],
+ { id: '1', args: {} }
+ );
+ });
+ });
});
diff --git a/lib/client-api/src/hooks.ts b/lib/client-api/src/hooks.ts
index 651928660d67..9d5f8c777a8f 100644
--- a/lib/client-api/src/hooks.ts
+++ b/lib/client-api/src/hooks.ts
@@ -13,6 +13,7 @@ import {
useChannel,
useStoryContext,
useParameter,
+ useArgs,
} from '@storybook/addons';
export {
@@ -27,6 +28,7 @@ export {
useChannel,
useStoryContext,
useParameter,
+ useArgs,
};
export function useSharedState(sharedId: string, defaultState?: S): [S, (s: S) => void] {
diff --git a/lib/client-api/src/index.ts b/lib/client-api/src/index.ts
index 4fe25c1a9ff1..cee9d206fcc3 100644
--- a/lib/client-api/src/index.ts
+++ b/lib/client-api/src/index.ts
@@ -1,4 +1,4 @@
-import ClientApi, { addDecorator, addParameters } 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';
@@ -13,6 +13,7 @@ export {
ClientApi,
addDecorator,
addParameters,
+ addParameterEnhancer,
StoryStore,
ConfigApi,
defaultDecorateStory,
diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts
index 2553f0d0ff60..01132c8664e1 100644
--- a/lib/client-api/src/story_store.test.ts
+++ b/lib/client-api/src/story_store.test.ts
@@ -1,7 +1,8 @@
+// foo
import createChannel from '@storybook/channel-postmessage';
import { toId } from '@storybook/csf';
-import addons from '@storybook/addons';
-import { SET_STORIES, RENDER_CURRENT_STORY } from '@storybook/core-events';
+import addons, { mockChannel } from '@storybook/addons';
+import Events from '@storybook/core-events';
import StoryStore, { ErrorLike } from './story_store';
import { defaultDecorateStory } from './decorators';
@@ -84,6 +85,188 @@ describe('preview.story_store', () => {
});
});
+ describe('args', () => {
+ it('args is initialized to the value stored in parameters.args[name] || 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'] } } },
+ arg4: {},
+ arg5: {},
+ },
+ args: {
+ arg2: 3,
+ arg4: 'foo',
+ arg6: false,
+ },
+ });
+ expect(store.getRawStory('a', '1').args).toEqual({
+ arg1: 'arg1',
+ arg2: 3,
+ arg3: { complex: { object: ['type'] } },
+ arg4: 'foo',
+ arg6: false,
+ });
+ });
+
+ it('updateStoryArgs changes the args of a story, per-key', () => {
+ const store = new StoryStore({ channel });
+ addStoryToStore(store, 'a', '1', () => 0);
+ expect(store.getRawStory('a', '1').args).toEqual({});
+
+ store.updateStoryArgs('a--1', { foo: 'bar' });
+ expect(store.getRawStory('a', '1').args).toEqual({ foo: 'bar' });
+
+ store.updateStoryArgs('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.updateStoryArgs('a--1', { foo: 'bar' });
+ store.getRawStory('a', '1').storyFn();
+
+ expect(storyFn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ args: { foo: 'bar' },
+ })
+ );
+ });
+
+ it('updateStoryArgs emits STORY_ARGS_UPDATED', () => {
+ const onArgsChangedChannel = jest.fn();
+ const testChannel = mockChannel();
+ testChannel.on(Events.STORY_ARGS_UPDATED, onArgsChangedChannel);
+
+ const store = new StoryStore({ channel: testChannel });
+ addStoryToStore(store, 'a', '1', () => 0);
+
+ store.updateStoryArgs('a--1', { foo: 'bar' });
+ expect(onArgsChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar' });
+
+ store.updateStoryArgs('a--1', { baz: 'bing' });
+ expect(onArgsChangedChannel).toHaveBeenCalledWith('a--1', { foo: 'bar', baz: 'bing' });
+ });
+
+ it('should update if the UPDATE_STORY_ARGS event is received', () => {
+ const testChannel = mockChannel();
+ const store = new StoryStore({ channel: testChannel });
+ addStoryToStore(store, 'a', '1', () => 0);
+
+ testChannel.emit(Events.UPDATE_STORY_ARGS, 'a--1', { foo: 'bar' });
+
+ 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', () => {
+ 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('does not merge subkeys of parameter enhancer results', () => {
+ const store = new StoryStore({ channel });
+
+ const firstEnhancer = jest.fn().mockReturnValue({ ns: { c: 'd' } });
+ store.addParameterEnhancer(firstEnhancer);
+ const secondEnhancer = jest.fn().mockReturnValue({ ns: { e: 'f' } });
+ store.addParameterEnhancer(secondEnhancer);
+
+ addStoryToStore(store, 'a', '1', () => 0, { ns: { a: 'b' } });
+
+ expect(firstEnhancer).toHaveBeenCalledWith(
+ expect.objectContaining({ parameters: { ns: { a: 'b' } } })
+ );
+ expect(secondEnhancer).toHaveBeenCalledWith(
+ expect.objectContaining({ parameters: { ns: { c: 'd' } } })
+ );
+ expect(store.getRawStory('a', '1').parameters).toEqual({ ns: { 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 = {
@@ -262,7 +445,7 @@ describe('preview.story_store', () => {
it('waits for configuration to be over before emitting SET_STORIES', () => {
const onSetStories = jest.fn();
- channel.on(SET_STORIES, onSetStories);
+ channel.on(Events.SET_STORIES, onSetStories);
const store = new StoryStore({ channel });
addStoryToStore(store, 'a', '1', () => 0);
@@ -280,7 +463,7 @@ describe('preview.story_store', () => {
it('emits an empty SET_STORIES if no stories were added during configuration', () => {
const onSetStories = jest.fn();
- channel.on(SET_STORIES, onSetStories);
+ channel.on(Events.SET_STORIES, onSetStories);
const store = new StoryStore({ channel });
store.finishConfiguring();
@@ -289,7 +472,7 @@ describe('preview.story_store', () => {
it('allows configuration as second time (HMR)', () => {
const onSetStories = jest.fn();
- channel.on(SET_STORIES, onSetStories);
+ channel.on(Events.SET_STORIES, onSetStories);
const store = new StoryStore({ channel });
store.finishConfiguring();
@@ -311,7 +494,7 @@ describe('preview.story_store', () => {
describe('HMR behaviour', () => {
it('emits the right things after removing a story', () => {
const onSetStories = jest.fn();
- channel.on(SET_STORIES, onSetStories);
+ channel.on(Events.SET_STORIES, onSetStories);
const store = new StoryStore({ channel });
// For hooks
@@ -341,7 +524,7 @@ describe('preview.story_store', () => {
it('emits the right things after removing a kind', () => {
const onSetStories = jest.fn();
- channel.on(SET_STORIES, onSetStories);
+ channel.on(Events.SET_STORIES, onSetStories);
const store = new StoryStore({ channel });
// For hooks
@@ -384,7 +567,7 @@ describe('preview.story_store', () => {
describe('RENDER_CURRENT_STORY', () => {
it('is emitted when setError is called', () => {
const onRenderCurrentStory = jest.fn();
- channel.on(RENDER_CURRENT_STORY, onRenderCurrentStory);
+ channel.on(Events.RENDER_CURRENT_STORY, onRenderCurrentStory);
const store = new StoryStore({ channel });
store.setError(new Error('Something is bad!') as ErrorLike);
@@ -393,7 +576,7 @@ describe('preview.story_store', () => {
it('is NOT emitted when setSelection is called during configuration', () => {
const onRenderCurrentStory = jest.fn();
- channel.on(RENDER_CURRENT_STORY, onRenderCurrentStory);
+ channel.on(Events.RENDER_CURRENT_STORY, onRenderCurrentStory);
const store = new StoryStore({ channel });
store.setSelection({ storyId: 'a--1', viewMode: 'story' });
@@ -402,7 +585,7 @@ describe('preview.story_store', () => {
it('is emitted when configuration ends', () => {
const onRenderCurrentStory = jest.fn();
- channel.on(RENDER_CURRENT_STORY, onRenderCurrentStory);
+ channel.on(Events.RENDER_CURRENT_STORY, onRenderCurrentStory);
const store = new StoryStore({ channel });
store.finishConfiguring();
@@ -411,7 +594,7 @@ describe('preview.story_store', () => {
it('is emitted when setSelection is called outside of configuration', () => {
const onRenderCurrentStory = jest.fn();
- channel.on(RENDER_CURRENT_STORY, onRenderCurrentStory);
+ channel.on(Events.RENDER_CURRENT_STORY, onRenderCurrentStory);
const store = new StoryStore({ channel });
store.finishConfiguring();
diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts
index 4eaab68d4282..bbf2807bc72d 100644
--- a/lib/client-api/src/story_store.ts
+++ b/lib/client-api/src/story_store.ts
@@ -1,5 +1,4 @@
/* eslint no-underscore-dangle: 0 */
-import EventEmitter from 'eventemitter3';
import memoize from 'memoizerific';
import dedent from 'ts-dedent';
import stable from 'stable';
@@ -12,7 +11,9 @@ import {
ViewMode,
Comparator,
Parameters,
- StoryFn,
+ Args,
+ LegacyStoryFn,
+ ArgsStoryFn,
StoryContext,
} from '@storybook/addons';
import {
@@ -23,6 +24,7 @@ import {
StoreItem,
ErrorLike,
GetStorybookKind,
+ ParameterEnhancer,
} from './types';
import { HooksContext } from './hooks';
import storySort from './storySort';
@@ -66,7 +68,7 @@ const toExtracted = (obj: T) =>
return Object.assign(acc, { [key]: value });
}, {});
-export default class StoryStore extends EventEmitter {
+export default class StoryStore {
_error?: ErrorLike;
_channel: Channel;
@@ -81,22 +83,23 @@ export default class StoryStore extends EventEmitter {
// Keyed on storyId
_stories: StoreData;
+ _parameterEnhancers: ParameterEnhancer[];
+
_revision: number;
_selection: Selection;
constructor(params: { channel: Channel }) {
- super();
-
// Assume we are configuring until we hear otherwise
this._configuring = true;
this._globalMetadata = { parameters: {}, decorators: [] };
this._kinds = {};
this._stories = {};
+ this._parameterEnhancers = [];
this._revision = 0;
this._selection = {} as any;
- this._channel = params.channel;
this._error = undefined;
+ this._channel = params.channel;
this.setupListeners();
}
@@ -108,6 +111,10 @@ export default class StoryStore extends EventEmitter {
this._channel.on(Events.SET_CURRENT_STORY, ({ storyId, viewMode }) =>
this.setSelection({ storyId, viewMode })
);
+
+ this._channel.on(Events.UPDATE_STORY_ARGS, (id: string, newArgs: Args) =>
+ this.updateStoryArgs(id, newArgs)
+ );
}
startConfiguring() {
@@ -149,13 +156,27 @@ 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, kind, name, storyFn: original, parameters = {}, decorators = [] }: AddStoryArgs,
+ {
+ id,
+ kind,
+ name,
+ storyFn: original,
+ parameters: storyParameters = {},
+ decorators: storyDecorators = [],
+ }: AddStoryArgs,
{
applyDecorators,
allowUnsafe = false,
}: {
- applyDecorators: (fn: StoryFn, decorators: DecoratorFunction[]) => any;
+ applyDecorators: (fn: LegacyStoryFn, decorators: DecoratorFunction[]) => any;
} & AllowUnsafeOption
) {
if (!this._configuring && !allowUnsafe)
@@ -186,33 +207,60 @@ 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 parametersBeforeEnhancement = combineParameters(
this._globalMetadata.parameters,
kindMetadata.parameters,
- parameters
+ storyParameters
);
+ const parameters = this._parameterEnhancers.reduce(
+ (accumlatedParameters, enhancer) => ({
+ ...accumlatedParameters,
+ ...enhancer({ ...identification, parameters: accumlatedParameters, args: {} }),
+ }),
+ parametersBeforeEnhancement
+ );
+
+ 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(getOriginal(), allDecorators)
+ const getDecorated: () => LegacyStoryFn = memoize(1)(() =>
+ applyDecorators(finalStoryFn, decorators)
);
const hooks = new HooksContext();
- const storyFn: StoryFn = (context: StoryContext) =>
+ const storyFn: LegacyStoryFn = (runtimeContext: StoryContext) =>
getDecorated()({
...identification,
- ...context,
+ ...runtimeContext,
+ parameters,
hooks,
- // NOTE: we do not allow the passed in context to override parameters
- parameters: allParameters,
+ args: _stories[id].args,
});
+ // Pull out parameters.args.$ || .argTypes.$.defaultValue into initialArgs
+ const initialArgs: Args = parameters.args || {};
+ const defaultArgs: Args = parameters.argTypes
+ ? Object.entries(parameters.argTypes as Record).reduce(
+ (acc, [arg, { defaultValue }]) => {
+ if (defaultValue) acc[arg] = defaultValue;
+ return acc;
+ },
+ {} as Args
+ )
+ : {};
+
_stories[id] = {
...identification,
@@ -221,7 +269,8 @@ export default class StoryStore extends EventEmitter {
getOriginal,
storyFn,
- parameters: allParameters,
+ parameters,
+ args: { ...defaultArgs, ...initialArgs },
};
}
@@ -385,6 +434,14 @@ export default class StoryStore extends EventEmitter {
this.getStoriesForKind(kind).map(story => this.cleanHooks(story.id));
}
+ updateStoryArgs(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 };
+
+ this._channel.emit(Events.STORY_ARGS_UPDATED, id, this._stories[id].args);
+ }
+
// 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 38478c0d8262..d201b3dcfa76 100644
--- a/lib/client-api/src/types.ts
+++ b/lib/client-api/src/types.ts
@@ -3,9 +3,11 @@ import {
StoryIdentifier,
StoryFn,
Parameters,
+ Args,
StoryApi,
DecoratorFunction,
DecorateStoryFunction,
+ StoryContext,
} from '@storybook/addons';
import StoryStore from './story_store';
import { HooksContext } from './hooks';
@@ -20,6 +22,7 @@ export interface StoryMetadata {
parameters: Parameters;
decorators: DecoratorFunction[];
}
+export type ParameterEnhancer = (context: StoryContext) => Parameters;
export type AddStoryArgs = StoryIdentifier & {
storyFn: StoryFn;
@@ -33,6 +36,7 @@ export type StoreItem = StoryIdentifier & {
getOriginal: () => StoryFn;
storyFn: StoryFn;
hooks: HooksContext;
+ args: Args;
};
export interface StoreData {
diff --git a/lib/core-events/src/index.ts b/lib/core-events/src/index.ts
index 38af71d7a0b6..c237594043e2 100644
--- a/lib/core-events/src/index.ts
+++ b/lib/core-events/src/index.ts
@@ -19,6 +19,10 @@ enum events {
STORY_MISSING = 'storyMissing',
STORY_ERRORED = 'storyErrored',
STORY_THREW_EXCEPTION = 'storyThrewException',
+ // Tell the story store to update (a subset of) a stories arg values
+ UPDATE_STORY_ARGS = 'updateStoryArgs',
+ // The values of a stories args just changed
+ STORY_ARGS_UPDATED = 'storyArgsChanged',
REGISTER_SUBSCRIPTION = 'registerSubscription',
// Tell the manager that the user pressed a key in the preview
PREVIEW_KEYDOWN = 'previewKeydown',
@@ -50,6 +54,8 @@ export const {
STORY_MISSING,
STORY_ERRORED,
STORY_THREW_EXCEPTION,
+ UPDATE_STORY_ARGS,
+ STORY_ARGS_UPDATED,
REGISTER_SUBSCRIPTION,
PREVIEW_KEYDOWN,
SELECT_STORY,
diff --git a/lib/core/src/client/preview/StoryRenderer.tsx b/lib/core/src/client/preview/StoryRenderer.tsx
index e31d577a951f..e52d58cfa369 100644
--- a/lib/core/src/client/preview/StoryRenderer.tsx
+++ b/lib/core/src/client/preview/StoryRenderer.tsx
@@ -68,8 +68,9 @@ export class StoryRenderer {
setupListeners() {
// Channel can be null in StoryShots
if (this.channel) {
- this.channel.on(Events.FORCE_RE_RENDER, () => this.forceReRender());
this.channel.on(Events.RENDER_CURRENT_STORY, () => this.renderCurrentStory(false));
+ this.channel.on(Events.STORY_ARGS_UPDATED, () => this.forceReRender());
+ this.channel.on(Events.FORCE_RE_RENDER, () => this.forceReRender());
}
}
diff --git a/lib/core/src/server/preview/iframe-webpack.config.js b/lib/core/src/server/preview/iframe-webpack.config.js
index c5cfe62766c1..9d290648432c 100644
--- a/lib/core/src/server/preview/iframe-webpack.config.js
+++ b/lib/core/src/server/preview/iframe-webpack.config.js
@@ -53,12 +53,15 @@ export default ({
if (match) {
const configFilename = match[1];
virtualModuleMapping[entryFilename] = `
- import { addDecorator, addParameters } from '@storybook/client-api';
+ import { addDecorator, addParameters, addParameterEnhancer } from '@storybook/client-api';
- const { decorators, parameters } = require(${JSON.stringify(configFilename)});
+ const { decorators, parameters,parameterEnhancers } = require(${JSON.stringify(
+ configFilename
+ )});
if (decorators) decorators.forEach(decorator => addDecorator(decorator));
if (parameters) addParameters(parameters);
+ if (parameterEnhancers) parameterEnhancers.forEach(enhancer => addParameterEnhancer(enhancer));
`;
}
});
diff --git a/lib/ui/src/components/sidebar/treeview/treeview.mockdata.ts b/lib/ui/src/components/sidebar/treeview/treeview.mockdata.ts
index 5401de6796ef..fd2d2b0aaa1e 100644
--- a/lib/ui/src/components/sidebar/treeview/treeview.mockdata.ts
+++ b/lib/ui/src/components/sidebar/treeview/treeview.mockdata.ts
@@ -41,6 +41,7 @@ export const mockDataset: MockDataSet = {
depth: 2,
name: 'GrandChild A1.1',
kind: '',
+ args: {},
},
'1-12-122': {
isRoot: false,
@@ -51,6 +52,7 @@ export const mockDataset: MockDataSet = {
depth: 2,
name: 'GrandChild A1.2',
kind: '',
+ args: {},
},
'1-12': {
isRoot: false,
@@ -71,6 +73,7 @@ export const mockDataset: MockDataSet = {
name: 'Child B1',
parent: '2',
kind: '',
+ args: {},
},
'2-22': {
isRoot: false,
@@ -81,6 +84,7 @@ export const mockDataset: MockDataSet = {
name: 'Child B2',
parent: '2',
kind: '',
+ args: {},
},
'3': {
isRoot: true,
@@ -100,6 +104,7 @@ export const mockDataset: MockDataSet = {
name: 'Child A1',
parent: '3',
kind: '',
+ args: {},
},
'3-32': {
isRoot: false,
@@ -120,6 +125,7 @@ export const mockDataset: MockDataSet = {
name: 'GrandChild A1.1',
parent: '3-32',
kind: '',
+ args: {},
},
'3-32-322': {
isRoot: false,
@@ -130,6 +136,7 @@ export const mockDataset: MockDataSet = {
name: 'GrandChild A1.2',
parent: '3-32',
kind: '',
+ args: {},
},
},
noRoot: {
@@ -160,6 +167,7 @@ export const mockDataset: MockDataSet = {
isRoot: false,
parent: '1',
kind: '',
+ args: {},
},
'1-12-121': {
id: '1-12-121',
@@ -170,6 +178,7 @@ export const mockDataset: MockDataSet = {
isRoot: false,
parent: '1-12',
kind: '',
+ args: {},
},
'1-12-122': {
id: '1-12-122',
@@ -180,6 +189,7 @@ export const mockDataset: MockDataSet = {
isRoot: false,
parent: '1-12',
kind: '',
+ args: {},
},
'1-12': {
id: '1-12',
@@ -200,6 +210,7 @@ export const mockDataset: MockDataSet = {
isRoot: false,
parent: '2',
kind: '',
+ args: {},
},
'2-22': {
id: '2-22',
@@ -210,6 +221,7 @@ export const mockDataset: MockDataSet = {
isRoot: false,
parent: '2',
kind: '',
+ args: {},
},
},
};