mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-17 00:05:59 +08:00
123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
/* eslint-disable no-param-reassign */
|
|
import { dedent } from 'ts-dedent';
|
|
import { createApp, h, reactive } from 'vue';
|
|
import type { RenderContext, ArgsStoryFn } from '@storybook/types';
|
|
|
|
import type { Args, StoryContext } from '@storybook/csf';
|
|
import type { StoryFnVueReturnType, VueRenderer } from './types';
|
|
|
|
export const render: ArgsStoryFn<VueRenderer> = (props, 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 h(Component, props, getSlots(props, context));
|
|
};
|
|
|
|
let setupFunction = (_app: any) => {};
|
|
export const setup = (fn: (app: any) => void) => {
|
|
setupFunction = fn;
|
|
};
|
|
|
|
const map = new Map<
|
|
VueRenderer['canvasElement'],
|
|
{ vueApp: ReturnType<typeof createApp>; reactiveArgs: any }
|
|
>();
|
|
|
|
export function renderToCanvas(
|
|
{
|
|
storyFn,
|
|
name,
|
|
showMain,
|
|
showError,
|
|
showException,
|
|
id,
|
|
title,
|
|
forceRemount,
|
|
storyContext,
|
|
}: RenderContext<VueRenderer>,
|
|
canvasElement: VueRenderer['canvasElement']
|
|
) {
|
|
// TODO: explain cyclical nature of these app => story => mount
|
|
const element: StoryFnVueReturnType = storyFn();
|
|
|
|
if (!element) {
|
|
showError({
|
|
title: `Expecting a Vue component from the story: "${name}" of "${title}".`,
|
|
description: dedent`
|
|
Did you forget to return the Vue component from the story?
|
|
Use "() => ({ template: '<my-comp></my-comp>' })" or "() => ({ components: MyComp, template: '<my-comp></my-comp>' })" when defining the story.
|
|
`,
|
|
});
|
|
return () => {};
|
|
}
|
|
|
|
const storyArgs = element.props || (element as any).render?.().props || storyContext.args || {};
|
|
|
|
const existingApp = map.get(canvasElement);
|
|
|
|
if (existingApp && !forceRemount) {
|
|
updateArgs(existingApp.reactiveArgs, storyArgs);
|
|
return () => {
|
|
teardown(existingApp.vueApp, canvasElement);
|
|
};
|
|
}
|
|
|
|
const reactiveArgs = reactive(storyArgs) as Args;
|
|
const storybookApp = createApp({
|
|
render() {
|
|
map.set(canvasElement, { vueApp: storybookApp, reactiveArgs });
|
|
return h(element, reactiveArgs);
|
|
},
|
|
});
|
|
|
|
storybookApp.config.errorHandler = (e: unknown) => showException(e as Error);
|
|
setupFunction(storybookApp);
|
|
storybookApp.mount(canvasElement);
|
|
|
|
showMain();
|
|
return () => {
|
|
teardown(storybookApp, canvasElement);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* get the slots as functions to be rendered
|
|
* @param props
|
|
* @param context
|
|
*/
|
|
|
|
function getSlots(props: Args, context: StoryContext<VueRenderer, Args>) {
|
|
const { argTypes } = context;
|
|
const slots = Object.entries(props)
|
|
.filter(([key, value]) => argTypes[key]?.table?.category === 'slots')
|
|
.map(([key, value]) => [key, () => h('span', JSON.stringify(value))]);
|
|
|
|
return Object.fromEntries(slots);
|
|
}
|
|
|
|
/**
|
|
* update the reactive args
|
|
* @param reactiveArgs
|
|
* @param nextArgs
|
|
* @returns
|
|
*/
|
|
function updateArgs(reactiveArgs: Args, nextArgs: Args) {
|
|
if (!nextArgs) return;
|
|
Object.keys(reactiveArgs).forEach((key) => {
|
|
delete reactiveArgs[key];
|
|
});
|
|
Object.assign(reactiveArgs, nextArgs);
|
|
}
|
|
|
|
function teardown(
|
|
storybookApp: ReturnType<typeof createApp>,
|
|
canvasElement: VueRenderer['canvasElement']
|
|
) {
|
|
storybookApp?.unmount();
|
|
if (map.has(canvasElement)) map.delete(canvasElement);
|
|
}
|