-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
render.ts
147 lines (128 loc) · 4.93 KB
/
render.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/* eslint-disable no-param-reassign */
import type { App } from 'vue';
import { createApp, h, reactive, isVNode, isReactive } from 'vue';
import type { ArgsStoryFn, RenderContext } from '@storybook/types';
import type { Args, StoryContext } from '@storybook/csf';
import type { StoryFnVueReturnType, StoryID, 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));
};
// set of setup functions that will be called when story is created
const setupFunctions = new Set<(app: App, storyContext?: StoryContext<VueRenderer>) => void>();
/** add a setup function to set that will be call when story is created a d
*
* @param fn
*/
export const setup = (fn: (app: App, storyContext?: StoryContext<VueRenderer>) => void) => {
setupFunctions.add(fn);
};
const runSetupFunctions = (app: App, storyContext: StoryContext<VueRenderer>) => {
setupFunctions.forEach((fn) => fn(app, storyContext));
};
const map = new Map<
VueRenderer['canvasElement'] | StoryID,
{
vueApp: ReturnType<typeof createApp>;
reactiveArgs: Args;
}
>();
export function renderToCanvas(
{ storyFn, forceRemount, showMain, showException, storyContext, id }: RenderContext<VueRenderer>,
canvasElement: VueRenderer['canvasElement']
) {
const existingApp = map.get(canvasElement);
// if the story is already rendered and we are not forcing a remount, we just update the reactive args
if (existingApp && !forceRemount) {
// normally storyFn should be call once only in setup function,but because the nature of react and how storybook rendering the decorators
// we need to call here to run the decorators again
// i may wrap each decorator in memoized function to avoid calling it if the args are not changed
const element = storyFn(); // call the story function to get the root element with all the decorators
const args = getArgs(element, storyContext); // get args in case they are altered by decorators otherwise use the args from the context
updateArgs(existingApp.reactiveArgs, args);
return () => {
teardown(existingApp.vueApp, canvasElement);
};
}
if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement);
// create vue app for the story
const vueApp = createApp({
setup() {
storyContext.args = reactive(storyContext.args);
const rootElement = storyFn(); // call the story function to get the root element with all the decorators
const args = getArgs(rootElement, storyContext); // get args in case they are altered by decorators otherwise use the args from the context
const appState = {
vueApp,
reactiveArgs: reactive(args),
};
map.set(canvasElement, appState);
return () => {
// not passing args here as props
// treat the rootElement as a component without props
return h(rootElement);
};
},
});
vueApp.config.errorHandler = (e: unknown) => showException(e as Error);
runSetupFunctions(vueApp, storyContext);
vueApp.mount(canvasElement);
showMain();
return () => {
teardown(vueApp, canvasElement);
};
}
/**
* generate slots for default story without render function template
*/
function getSlots(props: Args, context: StoryContext<VueRenderer, Args>) {
const { argTypes } = context;
const slots = Object.entries(props)
.filter(([key]) => argTypes[key]?.table?.category === 'slots')
.map(([key, value]) => [key, typeof value === 'function' ? value : () => value]);
return Object.fromEntries(slots);
}
/**
* get the args from the root element props if it is a vnode otherwise from the context
* @param element is the root element of the story
* @param storyContext is the story context
*/
function getArgs(element: StoryFnVueReturnType, storyContext: StoryContext<VueRenderer, Args>) {
return element.props && isVNode(element) ? element.props : storyContext.args;
}
/**
* update the reactive args
* @param reactiveArgs
* @param nextArgs
* @returns
*/
export function updateArgs(reactiveArgs: Args, nextArgs: Args) {
if (Object.keys(nextArgs).length === 0) return;
const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs);
// delete all args in currentArgs that are not in nextArgs
Object.keys(currentArgs).forEach((key) => {
if (!(key in nextArgs)) {
delete currentArgs[key];
}
});
// update currentArgs with nextArgs
Object.assign(currentArgs, nextArgs);
}
/**
* unmount the vue app
* @param storybookApp
* @param canvasElement
* @returns void
* @private
* */
function teardown(
storybookApp: ReturnType<typeof createApp>,
canvasElement: VueRenderer['canvasElement']
) {
storybookApp?.unmount();
if (map.has(canvasElement)) map.delete(canvasElement);
}