From d75b4e09a04870382a5d6de8bdad311b6c75a955 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 27 Feb 2020 13:27:27 +1100 Subject: [PATCH 01/19] Added basic global args mechanism to story_store --- lib/client-api/src/story_store.test.ts | 107 ++++++++++++++++++++++++- lib/client-api/src/story_store.ts | 38 ++++++--- lib/client-api/src/types.ts | 4 + lib/core-events/src/index.ts | 5 ++ 4 files changed, 143 insertions(+), 11 deletions(-) diff --git a/lib/client-api/src/story_store.test.ts b/lib/client-api/src/story_store.test.ts index e77f1bca9bc4..b2cd79933d56 100644 --- a/lib/client-api/src/story_store.test.ts +++ b/lib/client-api/src/story_store.test.ts @@ -85,7 +85,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: { @@ -201,6 +201,111 @@ describe('preview.story_store', () => { }); }); + describe('globalArgs', () => { + it.skip('is initialized to the value stored in parameters.globalArgTypes[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('setGlobalArgs changes the global args', () => { + const store = new StoryStore({ channel }); + addStoryToStore(store, 'a', '1', () => 0); + expect(store.getRawStory('a', '1').globalArgs).toEqual({}); + + store.setGlobalArgs({ foo: 'bar' }); + expect(store.getRawStory('a', '1').globalArgs).toEqual({ foo: 'bar' }); + + store.setGlobalArgs({ 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.setGlobalArgs({ foo: 'bar' }); + addStoryToStore(store, 'a', '1', storyFn); + store.getRawStory('a', '1').storyFn(); + + expect(storyFn).toHaveBeenCalledWith( + expect.objectContaining({ + globalArgs: { foo: 'bar' }, + }) + ); + + store.setGlobalArgs({ baz: 'bing' }); + store.getRawStory('a', '1').storyFn(); + + expect(storyFn).toHaveBeenCalledWith( + expect.objectContaining({ + globalArgs: { foo: 'bar', baz: 'bing' }, + }) + ); + }); + + it('setGlobalArgs emits GLOBAL_ARGS_CHANGED', () => { + const onGlobalArgsChangedChannel = jest.fn(); + const testChannel = mockChannel(); + testChannel.on(Events.GLOBAL_ARGS_CHANGED, onGlobalArgsChangedChannel); + + const store = new StoryStore({ channel: testChannel }); + addStoryToStore(store, 'a', '1', () => 0); + + store.setGlobalArgs({ foo: 'bar' }); + expect(onGlobalArgsChangedChannel).toHaveBeenCalledWith({ foo: 'bar' }); + + store.setGlobalArgs({ baz: 'bing' }); + expect(onGlobalArgsChangedChannel).toHaveBeenCalledWith({ foo: 'bar', baz: 'bing' }); + }); + + it('should update if the CHANGE_GLOBAL_ARGS event is received', () => { + const testChannel = mockChannel(); + const store = new StoryStore({ channel: testChannel }); + addStoryToStore(store, 'a', '1', () => 0); + + testChannel.emit(Events.CHANGE_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.setGlobalArgs({ 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 5039e6cd449a..bd4cf36faa31 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.CHANGE_STORY_ARGS, (id: string, newArgs: Args) => this.setStoryArgs(id, newArgs) ); + + this._channel.on(Events.CHANGE_GLOBAL_ARGS, (newGlobalArgs: Args) => + this.setGlobalArgs(newGlobalArgs) + ); } startConfiguring() { @@ -247,6 +256,7 @@ export default class StoryStore { parameters, hooks, args: _stories[id].args, + globalArgs: this._globalArgs, }); // Pull out parameters.args.$ || .argTypes.$.defaultValue into initialArgs @@ -306,7 +316,20 @@ export default class StoryStore { }, {}); } - fromId = (id: string): StoreItem | null => { + setGlobalArgs(newGlobalArgs: Args) { + this._globalArgs = { ...this._globalArgs, ...newGlobalArgs }; + this._channel.emit(Events.GLOBAL_ARGS_CHANGED, this._globalArgs); + } + + 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 }; + + this._channel.emit(Events.STORY_ARGS_CHANGED, id, this._stories[id].args); + } + + fromId = (id: string): PublishedStoreItem | null => { try { const data = this._stories[id as string]; @@ -314,7 +337,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 +460,6 @@ export default class StoryStore { this.getStoriesForKind(kind).map(story => this.cleanHooks(story.id)); } - 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 }; - - this._channel.emit(Events.STORY_ARGS_CHANGED, 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 b9c9f6ed25e5..f25b2f5c327f 100644 --- a/lib/core-events/src/index.ts +++ b/lib/core-events/src/index.ts @@ -23,6 +23,9 @@ enum events { CHANGE_STORY_ARGS = 'changeStoryArgs', // The values of a stories args just changed STORY_ARGS_CHANGED = 'storyArgsChanged', + // As above + CHANGE_GLOBAL_ARGS = 'changeGlobalArgs', + GLOBAL_ARGS_CHANGED = 'globalArgsChanged', 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, CHANGE_STORY_ARGS, STORY_ARGS_CHANGED, + CHANGE_GLOBAL_ARGS, + GLOBAL_ARGS_CHANGED, REGISTER_SUBSCRIPTION, PREVIEW_KEYDOWN, SELECT_STORY, From 71876af37d4fe48b40ba7544983cf240f303dc87 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 27 Feb 2020 13:52:15 +1100 Subject: [PATCH 02/19] Re-render when global args change --- .../stories/core/globalArgs.stories.js | 68 +++++++++++++++++++ lib/addons/src/hooks.ts | 14 ++++ lib/client-api/src/hooks.ts | 2 + lib/core/src/client/preview/StoryRenderer.tsx | 1 + 4 files changed, 85 insertions(+) create mode 100644 examples/official-storybook/stories/core/globalArgs.stories.js 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)}
+
{ + e.preventDefault(); + updateArgs(JSON.parse(argsInput)); + }} + > +