Skip to content

Commit

Permalink
Refactor DocsRender/Context
Browse files Browse the repository at this point in the history
  • Loading branch information
tmeasday committed Jul 5, 2022
1 parent 68ea524 commit e203c0c
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 320 deletions.
3 changes: 1 addition & 2 deletions lib/blocks/src/blocks/ArgsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION, PRIMARY_STORY } from './types';
import { getComponentName } from './utils';
import { lookupStoryId } from './Story';
import { useStory } from './useStory';

interface BaseProps {
Expand Down Expand Up @@ -173,7 +172,7 @@ export const StoryTable: FC<
break;
}
default: {
storyId = lookupStoryId(storyName, context);
storyId = context.storyIdByName(storyName);
}
}

Expand Down
7 changes: 2 additions & 5 deletions lib/blocks/src/blocks/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const getPreviewProps = (
docsContext: DocsContextProps<AnyFramework>,
sourceContext: SourceContextProps
) => {
const { mdxComponentAnnotations, mdxStoryNameToKey } = docsContext;
const { storyIdByName } = docsContext;
let sourceState = withSource;
let isLoading = false;
if (sourceState === SourceState.NONE) {
Expand All @@ -48,10 +48,7 @@ const getPreviewProps = (
if (id) return id;
if (of) return docsContext.storyIdByModuleExport(of);

return toId(
mdxComponentAnnotations.id || mdxComponentAnnotations.title,
storyNameFromExport(mdxStoryNameToKey[name])
);
return storyIdByName(name);
});

const sourceProps = getSourceProps({ ids: targetIds }, docsContext, sourceContext);
Expand Down
8 changes: 5 additions & 3 deletions lib/blocks/src/blocks/DocsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,22 @@ const warnOptionsTheme = deprecate(
);

export const DocsContainer: FunctionComponent<DocsContainerProps> = ({ context, children }) => {
const { id: storyId, type, storyById } = context;
const { id: storyId, storyById } = context;
const allComponents = { ...defaultComponents };
let theme = ensureTheme(null);
if (type === 'legacy') {
try {
const {
parameters: { options = {}, docs = {} },
} = storyById(storyId);
} = storyById();
let themeVars = docs.theme;
if (!themeVars && options.theme) {
warnOptionsTheme();
themeVars = options.theme;
}
theme = ensureTheme(themeVars);
Object.assign(allComponents, docs.components);
} catch (err) {
// No primary story, ie. standalone docs
}

useEffect(() => {
Expand Down
9 changes: 6 additions & 3 deletions lib/blocks/src/blocks/ExternalDocsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { ThemeProvider, themes, ensure } from '@storybook/theming';
import { DocsContextProps } from '@storybook/preview-web';
import { ModuleExport, ModuleExports, Story } from '@storybook/store';
import { AnyFramework, StoryId } from '@storybook/csf';
import { AnyFramework, StoryId, StoryName } from '@storybook/csf';

import { DocsContext } from './DocsContext';
import { ExternalPreview } from './ExternalPreview';
Expand All @@ -22,8 +22,6 @@ export const ExternalDocsContainer: React.FC<{ projectAnnotations: any }> = ({
};

const docsContext: DocsContextProps = {
type: 'external',

id: 'external-docs',
title: 'External',
name: 'Docs',
Expand All @@ -32,6 +30,11 @@ export const ExternalDocsContainer: React.FC<{ projectAnnotations: any }> = ({
return preview.storyIdByModuleExport(storyExport, metaExport || pageMeta);
},

storyIdByName: (name: StoryName) => {
// TODO
throw new Error('not implemented');
},

storyById: (id: StoryId) => {
return preview.storyById(id);
},
Expand Down
3 changes: 0 additions & 3 deletions lib/blocks/src/blocks/Meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ function getFirstStoryId(docsContext: DocsContextProps): string {

function renderAnchor() {
const context = useContext(DocsContext);
if (context.type === 'external') {
return null;
}
const anchorId = getFirstStoryId(context) || context.id;

return <Anchor storyId={anchorId} />;
Expand Down
14 changes: 2 additions & 12 deletions lib/blocks/src/blocks/Story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ type StoryImportProps = {

export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & CommonProps;

export const lookupStoryId = (
storyName: string,
{ mdxStoryNameToKey, mdxComponentAnnotations }: DocsContextProps
) =>
toId(
mdxComponentAnnotations.id || mdxComponentAnnotations.title,
storyNameFromExport(mdxStoryNameToKey[storyName])
);

export const getStoryId = (props: StoryProps, context: DocsContextProps): StoryId => {
const { id, of, meta } = props as StoryRefProps;

Expand All @@ -63,7 +54,7 @@ export const getStoryId = (props: StoryProps, context: DocsContextProps): StoryI

const { name } = props as StoryDefProps;
const inputId = id === CURRENT_SELECTION ? context.id : id;
return inputId || lookupStoryId(name, context);
return inputId || context.storyIdByName(name);
};

export const getStoryProps = <TFramework extends AnyFramework>(
Expand Down Expand Up @@ -118,8 +109,7 @@ const Story: FunctionComponent<StoryProps> = (props) => {
return null;
}

const inline = context.type === 'external' || storyProps.inline;
if (inline) {
if (storyProps.inline) {
// We do this so React doesn't complain when we replace the span in a secondary render
const htmlContents = `<span></span>`;

Expand Down
8 changes: 6 additions & 2 deletions lib/preview-web/src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
} from '@storybook/store';

import { StoryRender } from './render/StoryRender';
import { AbstractDocsRender } from './render/AbstractDocsRender';
import { TemplateDocsRender } from './render/TemplateDocsRender';
import { StandaloneDocsRender } from './render/StandaloneDocsRender';

const { fetch } = global;

Expand Down Expand Up @@ -324,7 +325,10 @@ export class Preview<TFramework extends AnyFramework> {
}

async teardownRender(
render: StoryRender<TFramework> | AbstractDocsRender<TFramework>,
render:
| StoryRender<TFramework>
| TemplateDocsRender<TFramework>
| StandaloneDocsRender<TFramework>,
{ viewModeChanged }: { viewModeChanged?: boolean } = {}
) {
this.storyRenders = this.storyRenders.filter((r) => r !== render);
Expand Down
4 changes: 2 additions & 2 deletions lib/preview-web/src/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,9 @@ describe('PreviewWeb', () => {
document.location.search = '?id=component-one--docs&viewMode=docs';
await createAndRenderPreview();

const { componentStories } = docsRenderer.render.mock.calls[0][0];
const context = docsRenderer.render.mock.calls[0][0];

expect(componentStories().map((s) => s.id)).toEqual([
expect(context.componentStories().map((s) => s.id)).toEqual([
'component-one--a',
'component-one--b',
'component-one--e',
Expand Down
27 changes: 0 additions & 27 deletions lib/preview-web/src/PreviewWeb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,33 +389,6 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
}
}

// Used by docs' modernInlineRender to render a story to a given element
// Note this short-circuits the `prepare()` phase of the StoryRender,
// main to be consistent with the previous behaviour. In the future,
// we will change it to go ahead and load the story, which will end up being
// "instant", although async.
renderStoryToElement(story: Story<TFramework>, element: HTMLElement) {
if (!this.renderToDOM)
throw new Error(`Cannot call renderStoryToElement before initialization`);

const render = new StoryRender<TFramework>(
this.channel,
this.storyStore,
this.renderToDOM,
this.inlineStoryCallbacks(story.id),
story.id,
'docs',
story
);
render.renderToElement(element);

this.storyRenders.push(render);

return async () => {
await this.teardownRender(render);
};
}

async teardownRender(
render: PossibleRender<TFramework>,
{ viewModeChanged = false }: { viewModeChanged?: boolean } = {}
Expand Down
89 changes: 89 additions & 0 deletions lib/preview-web/src/docs-context/DocsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
AnyFramework,
ComponentTitle,
StoryContextForLoaders,
StoryId,
StoryName,
} from '@storybook/csf';
import { CSFFile, ModuleExport, Story, StoryStore } from '@storybook/store';
import { PreviewWeb } from '../PreviewWeb';

import { DocsContextProps } from './DocsContextProps';

export class DocsContext<TFramework extends AnyFramework> implements DocsContextProps<TFramework> {
private componentStoriesValue: Story<TFramework>[];

private storyIdToCSFFile: Map<StoryId, CSFFile<TFramework>>;

private exportToStoryId: Map<ModuleExport, StoryId>;

private nameToStoryId: Map<StoryName, StoryId>;

constructor(
public readonly id: StoryId,
public readonly title: ComponentTitle,
public readonly name: StoryName,
protected store: StoryStore<TFramework>,
/** The CSF files known (via the index) to be refererenced by this docs file */
public renderStoryToElement: PreviewWeb<TFramework>['renderStoryToElement'],
csfFiles: CSFFile<TFramework>[],
componentStoriesFromAllCsfFiles = true
) {
this.storyIdToCSFFile = new Map();
this.exportToStoryId = new Map();
this.nameToStoryId = new Map();
this.componentStoriesValue = [];

csfFiles.forEach((csfFile, index) => {
Object.values(csfFile.stories).forEach((annotation) => {
this.storyIdToCSFFile.set(annotation.id, csfFile);
this.exportToStoryId.set(annotation.moduleExport, annotation.id);
this.nameToStoryId.set(annotation.name, annotation.id);

if (componentStoriesFromAllCsfFiles || index === 0)
this.componentStoriesValue.push(this.storyById(annotation.id));
});
});
}

setMeta() {
// Do nothing
}

storyIdByModuleExport = (storyExport: ModuleExport) => {
const storyId = this.exportToStoryId.get(storyExport);
if (storyId) return storyId;

throw new Error(`No story found with that export: ${storyExport}`);
};

storyIdByName = (storyName: StoryName) => {
const storyId = this.nameToStoryId.get(storyName);
if (storyId) return storyId;

throw new Error(`No story found with that name: ${storyName}`);
};

componentStories = () => {
return this.componentStoriesValue;
};

storyById = (inputStoryId?: StoryId) => {
const storyId = inputStoryId || this.id;
const csfFile = this.storyIdToCSFFile.get(storyId);
if (!csfFile)
throw new Error(`Called \`storyById\` for story that was never loaded: ${storyId}`);
return this.store.storyFromCSFFile({ storyId, csfFile });
};

getStoryContext = (story: Story<TFramework>) => {
return {
...this.store.getStoryContext(story),
viewMode: 'docs',
} as StoryContextForLoaders<TFramework>;
};

loadStory = (id: StoryId) => {
return this.store.loadStory({ storyId: id });
};
}
58 changes: 58 additions & 0 deletions lib/preview-web/src/docs-context/DocsContextProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {
StoryId,
StoryName,
AnyFramework,
StoryContextForLoaders,
ComponentTitle,
} from '@storybook/csf';
import type { ModuleExport, ModuleExports, Story } from '@storybook/store';

export interface DocsContextProps<TFramework extends AnyFramework = AnyFramework> {
/**
* These fields represent the docs entry that is being rendered to the screen
*/
id: StoryId;
title: ComponentTitle;
name: StoryName;

/**
* Register the CSF file that this docs entry represents.
* Used by the `<Meta of={} />` block.
*/
setMeta: (metaExports: ModuleExports) => void;

/**
* Find a story's id from the direct export from the CSF file.
* This is primarily used by the `<Story of={} /> block.
*/
storyIdByModuleExport: (storyExport: ModuleExport, metaExports?: ModuleExports) => StoryId;
/**
* Find a story's id from the name of the story.
* This is primarily used by the `<Story name={} /> block.
* Note that the story must be part of the primary CSF file of the docs entry.
*/
storyIdByName: (storyName: StoryName) => StoryId;
/**
* Syncronously find a story by id (if the id is not provided, this will look up the primary
* story in the CSF file, if such a file exists).
*/
storyById: (id?: StoryId) => Story<TFramework>;
/**
* Syncronously find all stories of the component referenced by the CSF file.
*/
componentStories: () => Story<TFramework>[];

/**
* Get the story context of the referenced story.
*/
getStoryContext: (story: Story<TFramework>) => StoryContextForLoaders<TFramework>;
/**
* Asyncronously load an arbitrary story by id.
*/
loadStory: (id: StoryId) => Promise<Story<TFramework>>;

/**
* Render a story to a given HTML element and keep it up to date across context changes
*/
renderStoryToElement: (story: Story<TFramework>, element: HTMLElement) => () => Promise<void>;
}
9 changes: 9 additions & 0 deletions lib/preview-web/src/docs-context/DocsRenderFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { AnyFramework, Parameters } from '@storybook/csf';
import { DocsContextProps } from './DocsContextProps';

export type DocsRenderFunction<TFramework extends AnyFramework> = (
docsContext: DocsContextProps<TFramework>,
docsParameters: Parameters,
element: HTMLElement,
callback: () => void
) => void;
3 changes: 2 additions & 1 deletion lib/preview-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export { PreviewWeb } from './PreviewWeb';

export { simulatePageLoad, simulateDOMContentLoaded } from './simulate-pageload';

export * from './types';
export type { DocsContextProps } from './docs-context/DocsContextProps';
export type { DocsRenderFunction } from './docs-context/DocsRenderFunction';
Loading

0 comments on commit e203c0c

Please sign in to comment.