Skip to content

Commit 1fa8346

Browse files
author
Jiří Fencl
committed
feat: 🎸 feat(tool-storybook): add vanilla JS support and preview configuration helpers
- Add createPreviewConfig helper for simplified Storybook setup - Add createVanillaRenderer for vanilla JavaScript widget rendering - Add comprehensive JSDoc documentation for all exported functions - Update documentation with Storybook 9 setup for both Preact and vanilla JS widgets - Include separate configuration examples for Preact and vanilla frameworks
1 parent 5c33f6e commit 1fa8346

3 files changed

Lines changed: 18134 additions & 49 deletions

File tree

‎packages/tool-storybook/src/index.js‎

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { getMerkur } from '@merkur/core';
1+
import { getMerkur, createMerkurWidget } from '@merkur/core';
22

3+
/**
4+
* Creates a loader function for Storybook that manages Merkur widget lifecycle.
5+
* The loader creates and mounts widgets for stories, reusing instances when possible
6+
* and unmounting previous widgets when switching stories.
7+
*
8+
* @param {Object} options Configuration options
9+
* @param {Object} options.widgetProperties The widget properties used to create widget instances
10+
* @param {Function} options.render Callback function called when widget state updates (receives widget instance)
11+
* @returns {Function} Async loader function that returns widget instance for stories
12+
*/
313
function createWidgetLoader({ render, widgetProperties }) {
414
let lastStory = {};
515

@@ -21,7 +31,7 @@ function createWidgetLoader({ render, widgetProperties }) {
2131
.then(async (widget) => {
2232
widget.$in.component.lifeCycle.mount = () => {};
2333
widget.$in.component.lifeCycle.update = () => {
24-
render();
34+
render(widget);
2535
};
2636
widget.$in.component.lifeCycle.unmount = () => {};
2737

@@ -40,4 +50,108 @@ function createWidgetLoader({ render, widgetProperties }) {
4050
};
4151
}
4252

43-
export { createWidgetLoader };
53+
/**
54+
* Creates a Storybook preview configuration for Merkur widgets.
55+
* Handles widget registration and loader setup.
56+
*
57+
* @param {Object} options Configuration options
58+
* @param {Object} options.widgetProperties The widget properties to register
59+
* @param {Function} [options.render] Optional custom render function for widget updates (receives widget instance)
60+
* @param {Function} [options.createWidget=createMerkurWidget] Factory function to create widget instances
61+
* @returns {Object} Storybook preview configuration with loaders
62+
*/
63+
function createPreviewConfig({
64+
widgetProperties,
65+
render,
66+
createWidget = createMerkurWidget,
67+
}) {
68+
// Register the widget with Merkur
69+
getMerkur().register({
70+
...widgetProperties,
71+
createWidget,
72+
});
73+
74+
return {
75+
loaders: [
76+
createWidgetLoader({
77+
widgetProperties,
78+
render: render || (() => {}),
79+
}),
80+
],
81+
};
82+
}
83+
84+
/**
85+
* Creates a render function for vanilla JavaScript widgets with state management.
86+
* Renders widget using HTML string output and handles re-rendering.
87+
*
88+
* @param {Object} options Configuration options
89+
* @param {Function|Object} options.ViewComponent The default view function or component map
90+
* @param {Function} options.bindEvents Function to bind events to the container
91+
* @returns {Object} Object with render function and update callback
92+
*/
93+
function createVanillaRenderer(options) {
94+
const { ViewComponent, bindEvents } = options;
95+
96+
// Store references for re-rendering
97+
let currentContainer = null;
98+
let currentWidget = null;
99+
let currentViewFunction = null;
100+
101+
function getViewFunction(args, ViewComponent) {
102+
if (typeof ViewComponent === 'function') {
103+
return ViewComponent;
104+
}
105+
106+
if (args.viewComponent && ViewComponent[args.viewComponent]) {
107+
return ViewComponent[args.viewComponent];
108+
}
109+
110+
if (args.component) {
111+
return typeof args.component === 'function'
112+
? args.component
113+
: ViewComponent[args.component];
114+
}
115+
116+
return ViewComponent.default || ViewComponent;
117+
}
118+
119+
function renderWidget(container, widget, viewFunction) {
120+
container.innerHTML = viewFunction(widget);
121+
122+
if (bindEvents) {
123+
bindEvents(container, widget);
124+
} else if (widget.View?.bindEvents) {
125+
widget.View.bindEvents(container, widget);
126+
}
127+
}
128+
129+
return {
130+
render: (args, { loaded: { widget } }) => {
131+
if (!widget) {
132+
const empty = document.createElement('div');
133+
empty.textContent = 'Loading widget...';
134+
return empty;
135+
}
136+
137+
const container = document.createElement('div');
138+
const viewFunction = getViewFunction(args, ViewComponent);
139+
140+
renderWidget(container, widget, viewFunction);
141+
142+
// Store references for re-rendering
143+
currentContainer = container;
144+
currentWidget = widget;
145+
currentViewFunction = viewFunction;
146+
147+
return container;
148+
},
149+
update: () => {
150+
if (currentContainer && currentWidget && currentViewFunction) {
151+
renderWidget(currentContainer, currentWidget, currentViewFunction);
152+
}
153+
},
154+
};
155+
}
156+
157+
export { createWidgetLoader, createPreviewConfig, createVanillaRenderer };

0 commit comments

Comments
 (0)