diff --git a/app/vue/src/client/preview/index.ts b/app/vue/src/client/preview/index.ts
index 4e515081a915..09b69690dbcd 100644
--- a/app/vue/src/client/preview/index.ts
+++ b/app/vue/src/client/preview/index.ts
@@ -69,6 +69,7 @@ const defaultContext: StoryContext = {
kind: 'unspecified',
parameters: {},
args: {},
+ globalArgs: {},
};
function decorateStory(
diff --git a/examples/dev-kits/manager.js b/examples/dev-kits/manager.js
index cf2f252bacec..7254179364b3 100644
--- a/examples/dev-kits/manager.js
+++ b/examples/dev-kits/manager.js
@@ -1,8 +1,8 @@
-import React from 'react';
+import React, { useState } from 'react';
import { PropTypes } from 'prop-types';
import { Button } from '@storybook/react/demo';
import { addons } from '@storybook/addons';
-import { useAddonState, useStoryState } from '@storybook/api';
+import { useAddonState, useStoryState, useGlobalArgs } from '@storybook/api';
import { themes } from '@storybook/theming';
import { AddonPanel } from '@storybook/components';
@@ -59,3 +59,37 @@ addons.addPanel('useAddonState', {
title: 'useAddonState',
render: StatePanel,
});
+
+const GlobalArgsPanel = ({ active, key }) => {
+ const [globalArgs, updateGlobalArgs] = useGlobalArgs();
+ const [globalArgsInput, updateGlobalArgsInput] = useState(JSON.stringify(globalArgs));
+ return (
+
+
+
Global Args
+
+
+
+
+ );
+};
+
+GlobalArgsPanel.propTypes = {
+ active: PropTypes.bool.isRequired,
+ key: PropTypes.string.isRequired,
+};
+
+addons.addPanel('useGlobalArgs', {
+ id: 'useGlobalArgs',
+ title: 'useGlobalArgs',
+ render: GlobalArgsPanel,
+});
diff --git a/examples/dev-kits/stories/addon-useglobalargs.js b/examples/dev-kits/stories/addon-useglobalargs.js
new file mode 100644
index 000000000000..b9cbed9e3af2
--- /dev/null
+++ b/examples/dev-kits/stories/addon-useglobalargs.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default {
+ title: 'addons/useGlobalArgs',
+};
+
+export const PassedToStory = ({ globalArgs }) => {
+ return (
+
+
Global args:
+
{JSON.stringify(globalArgs)}
+
+ );
+};
+
+PassedToStory.propTypes = {
+ globalArgs: PropTypes.shape({}).isRequired,
+};
+
+export const SecondStory = ({ globalArgs }) => {
+ return (
+
+
Global args (2):
+
{JSON.stringify(globalArgs)}
+
+ );
+};
+
+SecondStory.propTypes = {
+ globalArgs: PropTypes.shape({}).isRequired,
+};
diff --git a/examples/official-storybook/stories/core/globalArgs.stories.js b/examples/official-storybook/stories/core/globalArgs.stories.js
new file mode 100644
index 000000000000..dbd845be8ab4
--- /dev/null
+++ b/examples/official-storybook/stories/core/globalArgs.stories.js
@@ -0,0 +1,68 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { useGlobalArgs } 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/Global Args',
+ decorators: [
+ story => {
+ const [globalArgs, updateGlobalArgs] = useGlobalArgs();
+
+ return (
+ <>
+ {story()}
+
+ >
+ );
+ },
+ ],
+};
+
+export const PassedToStory = ({ globalArgs }) => {
+ return (
+
+
Global args:
+
{JSON.stringify(globalArgs)}
+
+ );
+};
+
+PassedToStory.propTypes = {
+ globalArgs: PropTypes.shape({}).isRequired,
+};
+
+export const SecondStory = ({ globalArgs }) => {
+ return (
+
+
Global args (2):
+
{JSON.stringify(globalArgs)}
+
+ );
+};
+
+SecondStory.propTypes = {
+ globalArgs: PropTypes.shape({}).isRequired,
+};
diff --git a/lib/addons/src/hooks.ts b/lib/addons/src/hooks.ts
index 8ad5787b4698..7d61b9d83ecc 100644
--- a/lib/addons/src/hooks.ts
+++ b/lib/addons/src/hooks.ts
@@ -5,6 +5,7 @@ import {
STORY_RENDERED,
DOCS_RENDERED,
UPDATE_STORY_ARGS,
+ UPDATE_GLOBAL_ARGS,
} from '@storybook/core-events';
import { addons } from './index';
import { StoryGetter, StoryContext, Args } from './types';
@@ -427,3 +428,16 @@ export function useArgs(): [Args, (newArgs: Args) => void] {
return [args, updateArgs];
}
+
+/* Returns current value of global args */
+export function useGlobalArgs(): [Args, (newGlobalArgs: Args) => void] {
+ const channel = addons.getChannel();
+ const { globalArgs } = useStoryContext();
+
+ const updateGlobalArgs = useCallback(
+ (newGlobalArgs: Args) => channel.emit(UPDATE_GLOBAL_ARGS, newGlobalArgs),
+ [channel]
+ );
+
+ return [globalArgs, updateGlobalArgs];
+}
diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts
index 1bb5282b4125..19d01c75d190 100644
--- a/lib/addons/src/types.ts
+++ b/lib/addons/src/types.ts
@@ -39,12 +39,13 @@ export interface StoryIdentifier {
name: StoryName;
}
-export interface StoryContext extends StoryIdentifier {
+export type StoryContext = StoryIdentifier & {
[key: string]: any;
parameters: Parameters;
args: Args;
+ globalArgs: Args;
hooks?: HooksContext;
-}
+};
export interface WrapperSettings {
options: OptionsParameter;
diff --git a/lib/api/src/index.tsx b/lib/api/src/index.tsx
index 94d8f5ee7ca2..72b4fa773740 100644
--- a/lib/api/src/index.tsx
+++ b/lib/api/src/index.tsx
@@ -46,6 +46,10 @@ import initVersions, {
SubState as VersionsSubState,
SubAPI as VersionsAPI,
} from './modules/versions';
+import initGlobalArgs, {
+ SubState as GlobalArgsSubState,
+ SubAPI as GlobalArgsAPI,
+} from './modules/globalArgs';
export { Options as StoreOptions, Listener as ChannelListener };
export { ActiveTabs };
@@ -66,7 +70,8 @@ export type State = Other &
NotificationState &
VersionsSubState &
RouterData &
- ShortcutsSubState;
+ ShortcutsSubState &
+ GlobalArgsSubState;
export type API = AddonsAPI &
ChannelAPI &
@@ -77,6 +82,7 @@ export type API = AddonsAPI &
ShortcutsAPI &
VersionsAPI &
UrlAPI &
+ GlobalArgsAPI &
OtherAPI;
interface OtherAPI {
@@ -172,6 +178,7 @@ class ManagerProvider extends Component {
initStories,
initURL,
initVersions,
+ initGlobalArgs,
].map(initModule =>
initModule({ ...routeData, ...apiData, state: this.state, fullAPI: this.api })
);
@@ -405,3 +412,12 @@ export function useArgs(): [Args, (newArgs: Args) => void] {
return [args, (newArgs: Args) => updateStoryArgs(id, newArgs)];
}
+
+export function useGlobalArgs(): [Args, (newGlobalArgs: Args) => void] {
+ const {
+ state: { globalArgs },
+ api: { updateGlobalArgs },
+ } = useContext(ManagerContext);
+
+ return [globalArgs, updateGlobalArgs];
+}
diff --git a/lib/api/src/modules/globalArgs.ts b/lib/api/src/modules/globalArgs.ts
new file mode 100644
index 000000000000..9fb65a8f1756
--- /dev/null
+++ b/lib/api/src/modules/globalArgs.ts
@@ -0,0 +1,41 @@
+import { UPDATE_GLOBAL_ARGS, GLOBAL_ARGS_UPDATED } from '@storybook/core-events';
+import { Args, Module, API } from '../index';
+
+export interface SubState {
+ globalArgs: Args;
+}
+
+export interface SubAPI {
+ updateGlobalArgs: (newGlobalArgs: Args) => void;
+}
+
+const initGlobalArgsApi = ({ store }: Module) => {
+ let fullApi: API;
+ const updateGlobalArgs = (newGlobalArgs: Args) => {
+ if (!fullApi) throw new Error('Cannot set global args until api has been initialized');
+
+ fullApi.emit(UPDATE_GLOBAL_ARGS, newGlobalArgs);
+ };
+
+ const api: SubAPI = {
+ updateGlobalArgs,
+ };
+
+ const state: SubState = {
+ // Currently global args always start empty. TODO -- should this be set on the channel at init time?
+ globalArgs: {},
+ };
+
+ const init = ({ api: inputApi }: { api: API }) => {
+ fullApi = inputApi;
+ fullApi.on(GLOBAL_ARGS_UPDATED, (globalArgs: Args) => store.setState({ globalArgs }));
+ };
+
+ return {
+ api,
+ state,
+ init,
+ };
+};
+
+export default initGlobalArgsApi;
diff --git a/lib/api/src/tests/globalArgs.test.ts b/lib/api/src/tests/globalArgs.test.ts
new file mode 100644
index 000000000000..90757b5bf9c5
--- /dev/null
+++ b/lib/api/src/tests/globalArgs.test.ts
@@ -0,0 +1,59 @@
+import EventEmitter from 'event-emitter';
+import { UPDATE_GLOBAL_ARGS, GLOBAL_ARGS_UPDATED } from '@storybook/core-events';
+
+import { Module, API } from '../index';
+import initGlobalArgs from '../modules/globalArgs';
+
+function createMockStore() {
+ let state = {};
+ return {
+ getState: jest.fn().mockImplementation(() => state),
+ setState: jest.fn().mockImplementation(s => {
+ state = { ...state, ...s };
+ }),
+ };
+}
+
+function createMockModule() {
+ // This mock module doesn't have all the fields but we don't use them all in this sub-module
+ return ({ store: createMockStore() } as unknown) as Module;
+}
+
+describe('stories API', () => {
+ it('sets a sensible initialState', () => {
+ const { state } = initGlobalArgs(createMockModule());
+
+ expect(state).toEqual({
+ globalArgs: {},
+ });
+ });
+
+ it('updates the state when the preview emits GLOBAL_ARGS_UPDATED', () => {
+ const mod = createMockModule();
+ const { state, init } = initGlobalArgs(mod);
+ mod.store.setState(state);
+
+ const api = new EventEmitter() as API;
+ init({ api });
+
+ api.emit(GLOBAL_ARGS_UPDATED, { a: 'b' });
+ expect(mod.store.getState()).toEqual({ globalArgs: { a: 'b' } });
+
+ api.emit(GLOBAL_ARGS_UPDATED, { a: 'c' });
+ expect(mod.store.getState()).toEqual({ globalArgs: { a: 'c' } });
+
+ // SHOULD NOT merge global args
+ api.emit(GLOBAL_ARGS_UPDATED, { d: 'e' });
+ expect(mod.store.getState()).toEqual({ globalArgs: { d: 'e' } });
+ });
+
+ it('emits UPDATE_GLOBAL_ARGS when updateGlobalArgs is called', () => {
+ const { init, api } = initGlobalArgs({} as Module);
+
+ const fullApi = ({ emit: jest.fn(), on: jest.fn() } as unknown) as API;
+ init({ api: fullApi });
+
+ api.updateGlobalArgs({ a: 'b' });
+ expect(fullApi.emit).toHaveBeenCalledWith(UPDATE_GLOBAL_ARGS, { a: 'b' });
+ });
+});
diff --git a/lib/client-api/README.md b/lib/client-api/README.md
index e6a03570cc4e..0fecc849017e 100644
--- a/lib/client-api/README.md
+++ b/lib/client-api/README.md
@@ -96,7 +96,27 @@ Note that arg values are passed directly to a story -- you should only store the
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
+import { useArgs } from '@storybook/client-api'; // or '@storybook/api'
+
// `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();
```
+
+## Global Args
+
+Global args are args that are "global" across all stories. They are used for things like themes and internationalization (i18n) in stories, where you want Storybook to "remember" your setting as you browse between stories.
+
+### Initial values of global args
+
+To set initial values of global args, set the `parameters.globalArgs` parameters. Addons can use parameter enhancers (see above) to do this.
+
+### Using global args in an addon
+
+Similar to args, global args are syncronized to the manager and can be accessed via the `useGlobalArgs` hook.
+
+```js
+import { useGlobalArgs } from '@storybook/client-api'; // or '@storybook/api'
+
+const [globalArgs, updateGlobalArgs] = useGlobalArgs();
+```
diff --git a/lib/client-api/src/decorators.ts b/lib/client-api/src/decorators.ts
index 7620af698ec7..1547f64366f4 100644
--- a/lib/client-api/src/decorators.ts
+++ b/lib/client-api/src/decorators.ts
@@ -11,6 +11,7 @@ const defaultContext: StoryContext = {
kind: 'unspecified',
parameters: {},
args: {},
+ globalArgs: {},
};
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 80edecf49554..057e49ffb686 100644
--- a/lib/client-api/src/hooks.test.js
+++ b/lib/client-api/src/hooks.test.js
@@ -1,4 +1,9 @@
-import { FORCE_RE_RENDER, STORY_RENDERED, UPDATE_STORY_ARGS } from '@storybook/core-events';
+import {
+ FORCE_RE_RENDER,
+ STORY_RENDERED,
+ UPDATE_STORY_ARGS,
+ UPDATE_GLOBAL_ARGS,
+} from '@storybook/core-events';
import addons from '@storybook/addons';
import { defaultDecorateStory } from './decorators';
import {
@@ -14,6 +19,7 @@ import {
useStoryContext,
HooksContext,
useArgs,
+ useGlobalArgs,
} from './hooks';
jest.mock('@storybook/client-logger', () => ({
@@ -520,4 +526,32 @@ describe('Preview hooks', () => {
);
});
});
+
+ describe('useGlobalArgs', () => {
+ it('will pull globalArgs from context', () => {
+ run(
+ () => {},
+ [
+ storyFn => {
+ expect(useGlobalArgs()[0]).toEqual({ a: 'b' });
+ return storyFn();
+ },
+ ],
+ { globalArgs: { a: 'b' } }
+ );
+ });
+ it('will emit UPDATE_GLOBAL_ARGS when called', () => {
+ run(
+ () => {},
+ [
+ storyFn => {
+ useGlobalArgs()[1]({ a: 'b' });
+ expect(mockChannel.emit).toHaveBeenCalledWith(UPDATE_GLOBAL_ARGS, { a: 'b' });
+ return storyFn();
+ },
+ ],
+ { globalArgs: {} }
+ );
+ });
+ });
});
diff --git a/lib/client-api/src/hooks.ts b/lib/client-api/src/hooks.ts
index 9d5f8c777a8f..5837e24c1970 100644
--- a/lib/client-api/src/hooks.ts
+++ b/lib/client-api/src/hooks.ts
@@ -14,6 +14,7 @@ import {
useStoryContext,
useParameter,
useArgs,
+ useGlobalArgs,
} from '@storybook/addons';
export {
@@ -29,6 +30,7 @@ export {
useStoryContext,
useParameter,
useArgs,
+ useGlobalArgs,
};
export function useSharedState(sharedId: string, defaultState?: S): [S, (s: S) => void] {
diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts
index 01132c8664e1..23803cddf833 100644
--- a/lib/client-api/src/story_store.test.ts
+++ b/lib/client-api/src/story_store.test.ts
@@ -86,7 +86,7 @@ describe('preview.story_store', () => {
});
describe('args', () => {
- it('args is initialized to the value stored in parameters.args[name] || parameters.argType[name].defaultValue', () => {
+ it('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: {
@@ -198,6 +198,145 @@ describe('preview.story_store', () => {
});
});
+ describe('globalArgs', () => {
+ it('is initialized to the value stored in parameters.globalArgs on the first story', () => {
+ const store = new StoryStore({ channel });
+ addStoryToStore(store, 'a', '1', () => 0, {
+ globalArgs: {
+ arg1: 'arg1',
+ arg2: 2,
+ arg3: { complex: { object: ['type'] } },
+ },
+ });
+ store.finishConfiguring();
+ expect(store.getRawStory('a', '1').globalArgs).toEqual({
+ arg1: 'arg1',
+ arg2: 2,
+ arg3: { complex: { object: ['type'] } },
+ });
+ });
+
+ it('on HMR it sensibly re-initializes with memory', () => {
+ const store = new StoryStore({ channel });
+ addons.setChannel(channel);
+ addStoryToStore(store, 'a', '1', () => 0, {
+ globalArgs: {
+ arg1: 'arg1',
+ arg2: 2,
+ arg3: { complex: { object: ['type'] } },
+ },
+ });
+ store.finishConfiguring();
+
+ // HMR
+ store.startConfiguring();
+ store.removeStoryKind('a');
+ addStoryToStore(store, 'a', '1', () => 0, {
+ globalArgs: {
+ arg2: 2,
+ // Although we have changed the default there is no way to tell that the user didn't change
+ // it themselves
+ arg3: { complex: { object: ['changed'] } },
+ arg4: 'new',
+ },
+ });
+ store.finishConfiguring();
+
+ expect(store.getRawStory('a', '1').globalArgs).toEqual({
+ arg2: 2,
+ arg3: { complex: { object: ['type'] } },
+ arg4: 'new',
+ });
+ });
+
+ it('updateGlobalArgs changes the global args', () => {
+ const store = new StoryStore({ channel });
+ addStoryToStore(store, 'a', '1', () => 0);
+ expect(store.getRawStory('a', '1').globalArgs).toEqual({});
+
+ store.updateGlobalArgs({ foo: 'bar' });
+ expect(store.getRawStory('a', '1').globalArgs).toEqual({ foo: 'bar' });
+
+ store.updateGlobalArgs({ baz: 'bing' });
+ expect(store.getRawStory('a', '1').globalArgs).toEqual({ foo: 'bar', baz: 'bing' });
+ });
+
+ it('is passed to the story in the context', () => {
+ const storyFn = jest.fn();
+ const store = new StoryStore({ channel });
+
+ store.updateGlobalArgs({ foo: 'bar' });
+ addStoryToStore(store, 'a', '1', storyFn);
+ store.getRawStory('a', '1').storyFn();
+
+ expect(storyFn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ globalArgs: { foo: 'bar' },
+ })
+ );
+
+ store.updateGlobalArgs({ baz: 'bing' });
+ store.getRawStory('a', '1').storyFn();
+
+ expect(storyFn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ globalArgs: { foo: 'bar', baz: 'bing' },
+ })
+ );
+ });
+
+ it('updateGlobalArgs emits GLOBAL_ARGS_UPDATED', () => {
+ const onGlobalArgsChangedChannel = jest.fn();
+ const testChannel = mockChannel();
+ testChannel.on(Events.GLOBAL_ARGS_UPDATED, onGlobalArgsChangedChannel);
+
+ const store = new StoryStore({ channel: testChannel });
+ addStoryToStore(store, 'a', '1', () => 0);
+
+ store.updateGlobalArgs({ foo: 'bar' });
+ expect(onGlobalArgsChangedChannel).toHaveBeenCalledWith({ foo: 'bar' });
+
+ store.updateGlobalArgs({ baz: 'bing' });
+ expect(onGlobalArgsChangedChannel).toHaveBeenCalledWith({ foo: 'bar', baz: 'bing' });
+ });
+
+ it('should update if the UPDATE_GLOBAL_ARGS event is received', () => {
+ const testChannel = mockChannel();
+ const store = new StoryStore({ channel: testChannel });
+ addStoryToStore(store, 'a', '1', () => 0);
+
+ testChannel.emit(Events.UPDATE_GLOBAL_ARGS, { foo: 'bar' });
+
+ expect(store.getRawStory('a', '1').globalArgs).toEqual({ foo: 'bar' });
+ });
+
+ it('DOES NOT pass globalArgs as the first argument to the story if `parameters.passArgsFirst` is true', () => {
+ const store = new StoryStore({ channel });
+
+ const storyOne = jest.fn();
+ addStoryToStore(store, 'a', '1', storyOne);
+
+ store.updateGlobalArgs({ foo: 'bar' });
+
+ store.getRawStory('a', '1').storyFn();
+ expect(storyOne).toHaveBeenCalledWith(
+ expect.objectContaining({
+ globalArgs: { foo: 'bar' },
+ })
+ );
+
+ const storyTwo = jest.fn();
+ addStoryToStore(store, 'a', '2', storyTwo, { passArgsFirst: true });
+ store.getRawStory('a', '2').storyFn();
+ expect(storyTwo).toHaveBeenCalledWith(
+ {},
+ expect.objectContaining({
+ globalArgs: { foo: 'bar' },
+ })
+ );
+ });
+ });
+
describe('parameterEnhancer', () => {
it('allows you to alter parameters when stories are added', () => {
const store = new StoryStore({ channel });
diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts
index bbf2807bc72d..8ce243386764 100644
--- a/lib/client-api/src/story_store.ts
+++ b/lib/client-api/src/story_store.ts
@@ -22,6 +22,7 @@ import {
StoreData,
AddStoryArgs,
StoreItem,
+ PublishedStoreItem,
ErrorLike,
GetStorybookKind,
ParameterEnhancer,
@@ -75,6 +76,8 @@ export default class StoryStore {
_configuring: boolean;
+ _globalArgs: Args;
+
_globalMetadata: StoryMetadata;
// Keyed on kind name
@@ -92,6 +95,8 @@ export default class StoryStore {
constructor(params: { channel: Channel }) {
// Assume we are configuring until we hear otherwise
this._configuring = true;
+
+ this._globalArgs = {};
this._globalMetadata = { parameters: {}, decorators: [] };
this._kinds = {};
this._stories = {};
@@ -115,6 +120,10 @@ export default class StoryStore {
this._channel.on(Events.UPDATE_STORY_ARGS, (id: string, newArgs: Args) =>
this.updateStoryArgs(id, newArgs)
);
+
+ this._channel.on(Events.UPDATE_GLOBAL_ARGS, (newGlobalArgs: Args) =>
+ this.updateGlobalArgs(newGlobalArgs)
+ );
}
startConfiguring() {
@@ -125,6 +134,26 @@ export default class StoryStore {
this._configuring = false;
this.pushToManager();
if (this._channel) this._channel.emit(Events.RENDER_CURRENT_STORY);
+
+ const storyIds = Object.keys(this._stories);
+ if (storyIds.length) {
+ const {
+ parameters: { globalArgs },
+ } = this.fromId(storyIds[0]);
+
+ // To deal with HMR, we consider the previous value of global args, and:
+ // 1. Remove any keys that are not in the new parameter
+ // 2. Preference any keys that were already set
+ // 3. Use any new keys from the new parameter
+ this._globalArgs = Object.entries(this._globalArgs || {}).reduce(
+ (acc, [key, previousValue]) => {
+ if (acc[key]) acc[key] = previousValue;
+
+ return acc;
+ },
+ globalArgs
+ );
+ }
}
addGlobalMetadata({ parameters, decorators }: StoryMetadata) {
@@ -221,7 +250,12 @@ export default class StoryStore {
const parameters = this._parameterEnhancers.reduce(
(accumlatedParameters, enhancer) => ({
...accumlatedParameters,
- ...enhancer({ ...identification, parameters: accumlatedParameters, args: {} }),
+ ...enhancer({
+ ...identification,
+ parameters: accumlatedParameters,
+ args: {},
+ globalArgs: {},
+ }),
}),
parametersBeforeEnhancement
);
@@ -247,6 +281,7 @@ export default class StoryStore {
parameters,
hooks,
args: _stories[id].args,
+ globalArgs: this._globalArgs,
});
// Pull out parameters.args.$ || .argTypes.$.defaultValue into initialArgs
@@ -306,7 +341,20 @@ export default class StoryStore {
}, {});
}
- fromId = (id: string): StoreItem | null => {
+ updateGlobalArgs(newGlobalArgs: Args) {
+ this._globalArgs = { ...this._globalArgs, ...newGlobalArgs };
+ this._channel.emit(Events.GLOBAL_ARGS_UPDATED, this._globalArgs);
+ }
+
+ 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);
+ }
+
+ fromId = (id: string): PublishedStoreItem | null => {
try {
const data = this._stories[id as string];
@@ -314,7 +362,10 @@ export default class StoryStore {
return null;
}
- return data;
+ return {
+ ...data,
+ globalArgs: this._globalArgs,
+ };
} catch (e) {
logger.warn('failed to get story:', this._stories);
logger.error(e);
@@ -434,14 +485,6 @@ export default class StoryStore {
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 d201b3dcfa76..4b7a8f2ac852 100644
--- a/lib/client-api/src/types.ts
+++ b/lib/client-api/src/types.ts
@@ -39,6 +39,10 @@ export type StoreItem = StoryIdentifier & {
args: Args;
};
+export type PublishedStoreItem = StoreItem & {
+ globalArgs: Args;
+};
+
export interface StoreData {
[key: string]: StoreItem;
}
diff --git a/lib/core-events/src/index.ts b/lib/core-events/src/index.ts
index c237594043e2..ec02c974b75b 100644
--- a/lib/core-events/src/index.ts
+++ b/lib/core-events/src/index.ts
@@ -22,7 +22,10 @@ enum events {
// 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',
+ STORY_ARGS_UPDATED = 'storyArgsUpdated',
+ // As above
+ UPDATE_GLOBAL_ARGS = 'updateGlobalArgs',
+ GLOBAL_ARGS_UPDATED = 'globalArgsUpdated',
REGISTER_SUBSCRIPTION = 'registerSubscription',
// Tell the manager that the user pressed a key in the preview
PREVIEW_KEYDOWN = 'previewKeydown',
@@ -56,6 +59,8 @@ export const {
STORY_THREW_EXCEPTION,
UPDATE_STORY_ARGS,
STORY_ARGS_UPDATED,
+ UPDATE_GLOBAL_ARGS,
+ GLOBAL_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 89bb545bdbbf..273447146eae 100644
--- a/lib/core/src/client/preview/StoryRenderer.tsx
+++ b/lib/core/src/client/preview/StoryRenderer.tsx
@@ -70,6 +70,7 @@ export class StoryRenderer {
if (this.channel) {
this.channel.on(Events.RENDER_CURRENT_STORY, () => this.renderCurrentStory(false));
this.channel.on(Events.STORY_ARGS_UPDATED, () => this.forceReRender());
+ this.channel.on(Events.GLOBAL_ARGS_UPDATED, () => this.forceReRender());
this.channel.on(Events.FORCE_RE_RENDER, () => this.forceReRender());
}
}