Skip to content

Commit

Permalink
Add a modernInlineRender that renders the story using the framework…
Browse files Browse the repository at this point in the history
…'s `renderToDOM`

Replaces #14911
  • Loading branch information
tmeasday committed Aug 6, 2021
1 parent 3b69025 commit f8f3dba
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 41 deletions.
110 changes: 81 additions & 29 deletions addons/docs/src/blocks/Story.tsx
@@ -1,8 +1,18 @@
import React, { FunctionComponent, ReactNode, ElementType, ComponentProps } from 'react';
import React, {
FunctionComponent,
ReactNode,
ElementType,
ComponentProps,
useContext,
useRef,
useEffect,
} from 'react';
import { MDXProvider } from '@mdx-js/react';
import { resetComponents, Story as PureStory } from '@storybook/components';
import { toId, storyNameFromExport } from '@storybook/csf';
import { Args, BaseAnnotations } from '@storybook/addons';
import { Story as StoryType } from '@storybook/client-api/dist/ts3.9/new/types';
import global from 'global';
import { CURRENT_SELECTION } from './types';

import { DocsContext, DocsContextProps } from './DocsContext';
Expand Down Expand Up @@ -41,18 +51,32 @@ export const lookupStoryId = (
storyNameFromExport(mdxStoryNameToKey[storyName])
);

export const getStoryProps = (
props: StoryProps,
context: DocsContextProps<any>
): PureStoryProps => {
// TODO -- this can be async
export const getStory = (props: StoryProps, context: DocsContextProps<any>): StoryType<any> => {
const { id } = props as StoryRefProps;
const { name } = props as StoryDefProps;
const inputId = id === CURRENT_SELECTION ? context.id : id;
const previewId = inputId || lookupStoryId(name, context);
const story = context.storyById(previewId);
return context.storyById(previewId);
};

export const getStoryProps = (
{ height, inline }: StoryProps,
story: StoryType<any>,
context: DocsContextProps<any>
): PureStoryProps => {
const defaultIframeHeight = 100;

if (!story) {
return {
id: story.id,
inline: false,
height: height || defaultIframeHeight.toString(),
title: undefined,
};
}

const { height, inline } = props;
const { storyFn, name: storyName, parameters } = story;
const { name: storyName, parameters } = story;
const { docs = {} } = parameters;

if (docs.disable) {
Expand All @@ -72,32 +96,60 @@ export const getStoryProps = (
return {
parameters,
inline: storyIsInline,
id: previewId,
// TODO -- how can `storyFn` be undefined?
storyFn:
prepareForInline && boundStoryFn ? () => prepareForInline(boundStoryFn, story) : boundStoryFn,
id: story.id,
storyFn: prepareForInline ? () => prepareForInline(boundStoryFn, story) : boundStoryFn,
height: height || (storyIsInline ? undefined : iframeHeight),
title: storyName,
};
};

const Story: FunctionComponent<StoryProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const storyProps = getStoryProps(props, context);
if (!storyProps) {
return null;
}
return (
<div id={storyBlockIdFromId(storyProps.id)}>
<MDXProvider components={resetComponents}>
<PureStory {...storyProps} />
</MDXProvider>
</div>
);
}}
</DocsContext.Consumer>
);
const Story: FunctionComponent<StoryProps> = (props) => {
const context = useContext(DocsContext);
const ref = useRef();
const story = getStory(props, context);
const { id, title, name } = story;
const renderContext = {
id,
title,
kind: title,
name,
story: name,
// TODO -- shouldn't this be true sometimes? How to react to arg changes
forceRender: false,
// TODO what to do when these fail?
showMain: () => {},
showError: () => {},
showException: () => {},
};
useEffect(() => {
if (story && ref.current) {
setTimeout(() => {
context.renderStoryToElement({ story, renderContext, element: ref.current as Element });
}, 1000);
}
return () => story?.cleanup();
}, [story]);

if (global?.FEATURES.modernInlineRender) {
return (
<div ref={ref} data-name={story.name}>
<span data-is-loading-indicator="true">loading story...</span>
</div>
);
}

const storyProps = getStoryProps(props, story, context);
if (!storyProps) {
return null;
}
return (
<div id={storyBlockIdFromId(storyProps.id)}>
<MDXProvider components={resetComponents}>
<PureStory {...storyProps} />
</MDXProvider>
</div>
);
};

Story.defaultProps = {
children: null,
Expand Down
17 changes: 6 additions & 11 deletions app/react/src/client/preview/render.tsx
Expand Up @@ -6,8 +6,6 @@ import { StoryContext, RenderContext } from './types';

const { document, FRAMEWORK_OPTIONS } = global;

const rootEl = document ? document.getElementById('root') : null;

const render = (node: ReactElement, el: Element) =>
new Promise((resolve) => {
ReactDOM.render(node, el, resolve);
Expand Down Expand Up @@ -47,13 +45,10 @@ class ErrorBoundary extends Component<{

const Wrapper = FRAMEWORK_OPTIONS?.strictMode ? StrictMode : Fragment;

export default async function renderMain({
storyContext,
unboundStoryFn,
showMain,
showException,
forceRender,
}: RenderContext) {
export default async function renderMain(
{ storyContext, unboundStoryFn, showMain, showException, forceRender }: RenderContext,
domElement: Element
) {
const Story = unboundStoryFn as FunctionComponent<StoryContext>;

const content = (
Expand All @@ -71,8 +66,8 @@ export default async function renderMain({
// https://github.com/storybookjs/react-storybook/issues/81
// But forceRender means that it's the same story, so we want too keep the state in that case.
if (!forceRender) {
ReactDOM.unmountComponentAtNode(rootEl);
ReactDOM.unmountComponentAtNode(domElement);
}

await render(element, rootEl);
await render(element, domElement);
}
1 change: 1 addition & 0 deletions examples/react-ts/main.ts
Expand Up @@ -23,6 +23,7 @@ const config: StorybookConfig = {
postcss: false,
previewCsfV3: true,
buildStoriesJson: true,
modernInlineRender: true,
},
};

Expand Down
6 changes: 5 additions & 1 deletion lib/client-api/src/new/types.ts
Expand Up @@ -171,7 +171,11 @@ export interface DocsContextProps<StoryFnReturnType> {
name: string;
storyById: (id: StoryId) => Story<StoryFnReturnType>;
componentStories: () => Story<StoryFnReturnType>[];
renderStoryToElement: (story: Story<StoryFnReturnType>) => void;
renderStoryToElement: (args: {
story: Story<StoryFnReturnType>;
renderContext: RenderContextWithoutStoryContext;
element: Element;
}) => void;

// TODO -- we need this for the `prepareForInline` docs approach
bindStoryFn: (story: Story<StoryFnReturnType>) => LegacyStoryFn<StoryFnReturnType>;
Expand Down
5 changes: 5 additions & 0 deletions lib/core-common/src/types.ts
Expand Up @@ -264,6 +264,11 @@ export interface StorybookConfig {
* Activate preview of CSF v3.0
*/
previewCsfV3?: boolean;

/**
* Activate modern inline rendering
*/
modernInlineRender?: boolean;
};
/**
* Tells Storybook where to find stories.
Expand Down

0 comments on commit f8f3dba

Please sign in to comment.