Skip to content

Commit

Permalink
Merge pull request #16333 from storybookjs/16239-side-loading
Browse files Browse the repository at this point in the history
Args: Add ability to specific argType "targets"
  • Loading branch information
shilman authored Nov 8, 2021
2 parents 66cc58b + c7912a8 commit 8a8f446
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 17 deletions.
4 changes: 3 additions & 1 deletion app/react/src/client/preview/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import { ReactFramework } from './types-6-0';

const { FRAMEWORK_OPTIONS } = global;

export const render: ArgsStoryFn<ReactFramework> = (args, { id, component: Component }) => {
export const render: ArgsStoryFn<ReactFramework> = (args, context) => {
const { id, component: Component } = context;
if (!Component) {
throw new Error(
`Unable to render story ${id} as the component annotation is missing from the default export`
);
}

return <Component {...args} />;
};

Expand Down
32 changes: 32 additions & 0 deletions examples/official-storybook/stories/core/sideloaded.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

const Component = (props: Record<string, number>) => <pre>{JSON.stringify(props)}</pre>;

export default {
component: Component,
argTypes: {
a: { target: 'somewhere' },
},
parameters: {
argTypeTarget: true,
},
};

export const StoryOne = {
args: {
a: 1,
b: 2,
c: 3,
},
};

export const StoryTwo = {
args: {
a: 1,
b: 2,
c: 3,
},
argTypes: {
c: { target: 'somewhere' },
},
};
5 changes: 5 additions & 0 deletions lib/core-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ export interface StorybookConfig {
* Use Storybook 7.0 babel config scheme
*/
babelModeV7?: boolean;

/**
* Filter args with a "target" on the type from the render function (EXPERIMENTAL)
*/
argTypeTargetsV7?: boolean;
};

/**
Expand Down
42 changes: 41 additions & 1 deletion lib/store/src/args.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { once } from '@storybook/client-logger';
import { combineArgs, mapArgsToTypes, validateOptions } from './args';
import {
combineArgs,
groupArgsByTarget,
mapArgsToTypes,
NO_TARGET_NAME,
validateOptions,
} from './args';

const stringType = { name: 'string' };
const numberType = { name: 'number' };
Expand Down Expand Up @@ -238,3 +244,37 @@ describe('validateOptions', () => {
);
});
});

describe('groupArgsByTarget', () => {
it('groups targeted args', () => {
const groups = groupArgsByTarget({
args: { a: 1, b: 2, c: 3 },
argTypes: { a: { target: 'group1' }, b: { target: 'group2' }, c: { target: 'group2' } },
} as any);
expect(groups).toEqual({
group1: {
a: 1,
},
group2: {
b: 2,
c: 3,
},
});
});

it('groups non-targetted args into a group with no name', () => {
const groups = groupArgsByTarget({
args: { a: 1, b: 2, c: 3 },
argTypes: { b: { name: 'b', target: 'group2' }, c: {} },
} as any);
expect(groups).toEqual({
[NO_TARGET_NAME]: {
a: 1,
c: 3,
},
group2: {
b: 2,
},
});
});
});
21 changes: 20 additions & 1 deletion lib/store/src/args.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import deepEqual from 'fast-deep-equal';
import { SBType, Args, ArgTypes } from '@storybook/csf';
import { SBType, Args, ArgTypes, StoryContext, AnyFramework } from '@storybook/csf';
import { once } from '@storybook/client-logger';
import isPlainObject from 'lodash/isPlainObject';
import dedent from 'ts-dedent';
Expand Down Expand Up @@ -137,3 +137,22 @@ export const deepDiff = (value: any, update: any): any => {
}
return update;
};

export const NO_TARGET_NAME = '';
export function groupArgsByTarget<TArgs = Args>({
args,
argTypes,
}: StoryContext<AnyFramework, TArgs>) {
const groupedArgs: Record<string, Partial<TArgs>> = {};
(Object.entries(args) as [keyof TArgs, any][]).forEach(([name, value]) => {
const { target = NO_TARGET_NAME } = (argTypes[name] || {}) as { target: string };

groupedArgs[target] = groupedArgs[target] || {};
groupedArgs[target][name] = value;
});
return groupedArgs;
}

export function noTargetArgs<TArgs = Args>(context: StoryContext<AnyFramework, TArgs>) {
return groupArgsByTarget(context)[NO_TARGET_NAME];
}
81 changes: 70 additions & 11 deletions lib/store/src/prepareStory.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import global from 'global';
import addons, { HooksContext } from '@storybook/addons';
import {
AnyFramework,
Expand All @@ -6,6 +7,7 @@ import {
SBScalarType,
StoryContext,
} from '@storybook/csf';
import { NO_TARGET_NAME } from './args';
import { prepareStory } from './prepareStory';

jest.mock('global', () => ({
Expand Down Expand Up @@ -38,6 +40,10 @@ const complexType: SBObjectType = {
},
};

beforeEach(() => {
global.FEATURES = { breakingChangesV7: true };
});

describe('prepareStory', () => {
describe('parameters', () => {
it('are combined in the right order', () => {
Expand Down Expand Up @@ -470,18 +476,71 @@ describe('prepareStory', () => {
});
});

describe('playFunction', () => {
it('awaits play if defined', async () => {
const inner = jest.fn();
const play = jest.fn(async () => {
await new Promise((r) => setTimeout(r, 0)); // Ensure this puts an async boundary in
inner();
});
const { playFunction } = prepareStory({ id, name, play }, { id, title }, { render });
describe('with `FEATURES.argTypeTargetsV7`', () => {
beforeEach(() => {
global.FEATURES = { breakingChangesV7: true, argTypeTargetsV7: true };
});
it('filters out targeted args', () => {
const renderMock = jest.fn();
const firstStory = prepareStory(
{
id,
name,
args: { a: 1, b: 2 },
argTypes: { b: { name: 'b', target: 'foo' } },
},
{ id, title },
{ render: renderMock }
);

firstStory.unboundStoryFn({
args: firstStory.initialArgs,
hooks: new HooksContext(),
...firstStory,
} as any);
expect(renderMock).toHaveBeenCalledWith(
{ a: 1 },
expect.objectContaining({ args: { a: 1 }, allArgs: { a: 1, b: 2 } })
);
});

await playFunction({} as StoryContext<AnyFramework>);
expect(play).toHaveBeenCalled();
expect(inner).toHaveBeenCalled();
it('adds argsByTarget to context', () => {
const renderMock = jest.fn();
const firstStory = prepareStory(
{
id,
name,
args: { a: 1, b: 2 },
argTypes: { b: { name: 'b', target: 'foo' } },
},
{ id, title },
{ render: renderMock }
);

firstStory.unboundStoryFn({
args: firstStory.initialArgs,
hooks: new HooksContext(),
...firstStory,
} as any);
expect(renderMock).toHaveBeenCalledWith(
{ a: 1 },
expect.objectContaining({ argsByTarget: { [NO_TARGET_NAME]: { a: 1 }, foo: { b: 2 } } })
);
});
});
});

describe('playFunction', () => {
it('awaits play if defined', async () => {
const inner = jest.fn();
const play = jest.fn(async () => {
await new Promise((r) => setTimeout(r, 0)); // Ensure this puts an async boundary in
inner();
});
const { playFunction } = prepareStory({ id, name, play }, { id, title }, { render });

await playFunction({} as StoryContext<AnyFramework>);
expect(play).toHaveBeenCalled();
expect(inner).toHaveBeenCalled();
});
});
21 changes: 18 additions & 3 deletions lib/store/src/prepareStory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { combineParameters } from './parameters';
import { applyHooks } from './hooks';
import { defaultDecorateStory } from './decorators';
import { groupArgsByTarget, NO_TARGET_NAME } from './args';

const argTypeDefaultValueWarning = deprecate(
() => {},
Expand Down Expand Up @@ -170,14 +171,28 @@ export function prepareStory<TFramework extends AnyFramework>(
acc[key] = mapping && val in mapping ? mapping[val] : val;
return acc;
}, {} as Args);

const mappedContext = { ...context, args: mappedArgs };

const { passArgsFirst: renderTimePassArgsFirst = true } = context.parameters;
return renderTimePassArgsFirst
? (render as ArgsStoryFn<TFramework>)(mappedArgs, mappedContext)
? (render as ArgsStoryFn<TFramework>)(mappedContext.args, mappedContext)
: (render as LegacyStoryFn<TFramework>)(mappedContext);
};
const unboundStoryFn = applyHooks<TFramework>(applyDecorators)(undecoratedStoryFn, decorators);
const decoratedStoryFn = applyHooks<TFramework>(applyDecorators)(undecoratedStoryFn, decorators);
const unboundStoryFn = (context: StoryContext<TFramework>) => {
let finalContext: StoryContext<TFramework> = context;
if (global.FEATURES?.argTypeTargetsV7) {
const argsByTarget = groupArgsByTarget({ args: context.args, ...context });
finalContext = {
...context,
allArgs: context.args,
argsByTarget,
args: argsByTarget[NO_TARGET_NAME],
};
}

return decoratedStoryFn(finalContext);
};
const playFunction = storyAnnotations.play;

return Object.freeze({
Expand Down

0 comments on commit 8a8f446

Please sign in to comment.