Skip to content

Commit 3faf46d

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 a02d483 commit 3faf46d

3 files changed

Lines changed: 17976 additions & 50 deletions

File tree

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

Lines changed: 70 additions & 2 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
11+
* @returns {Function} Async loader function that returns widget instance for stories
12+
*/
313
function createWidgetLoader({ render, widgetProperties }) {
414
let lastStory = {};
515

@@ -40,4 +50,62 @@ 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
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.
86+
* Renders widget using HTML string output.
87+
*
88+
* @param {Function|Object} ViewComponent The view function or component map
89+
* @returns {Function} Storybook render function
90+
*/
91+
function createVanillaRenderer(ViewComponent) {
92+
// eslint-disable-next-line no-unused-vars
93+
return (args, { loaded: { widget }, viewMode }) => {
94+
if (!widget) {
95+
return document.createElement('div');
96+
}
97+
98+
const container = document.createElement('div');
99+
const viewFunction =
100+
typeof ViewComponent === 'function'
101+
? ViewComponent
102+
: args.viewComponent && ViewComponent[args.viewComponent]
103+
? ViewComponent[args.viewComponent]
104+
: ViewComponent.default || ViewComponent;
105+
106+
container.innerHTML = viewFunction(widget);
107+
return container;
108+
};
109+
}
110+
111+
export { createWidgetLoader, createPreviewConfig, createVanillaRenderer };

‎website/docs/storybook-integration-into-merkur.md‎

Lines changed: 202 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,72 +8,149 @@ description: Learn how to integrate Storybook with your Merkur widget
88

99
[Storybook](https://storybook.js.org/) is an open source tool for developing UI components in isolation for React, Vue, Angular, and more. It makes building stunning UIs organized and efficient.
1010

11+
This guide requires Storybook version 9 or higher. Lower versions need a different setup, which is covered in previous versions of the documentation.
12+
1113
## Installation
1214

13-
At first we must install storybook to our Merkur project. The best way is using the [Storybook CLI](https://storybook.js.org/docs/react/get-started/install) to install it in a single command.
15+
Since Preact and vanilla JavaScript are not in the automatic framework selection, you need to manually install Storybook packages.
16+
17+
### For Preact Widgets
1418

1519
```bash
16-
npx storybook@latest init
20+
npm i -D @storybook/preact-vite storybook
1721
```
1822

19-
After that we install merkur module for easy integration.
23+
### For Vanilla JavaScript Widgets
2024

2125
```bash
22-
npm i @merkur/tool-storybook --save-dev
26+
npm i -D @storybook/html-vite storybook
2327
```
2428

25-
Now we must update storybook file `./storybook/preview.js` similarly to the example below.
29+
### Merkur Integration Package
30+
31+
Then install the Merkur module for easy integration (required for both):
32+
33+
```bash
34+
npm i -D @merkur/tool-storybook
35+
```
36+
37+
## Configuration
38+
39+
### Preact Widget
40+
41+
For Preact widgets, configure Storybook to use the Preact framework and JSX transformation.
42+
43+
#### `.storybook/main.mjs`
2644

2745
```javascript
28-
// ./storybook/preview.js
29-
import { createPreactWidget } from '@merkur/preact/client';
46+
const config = {
47+
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
48+
framework: {
49+
name: '@storybook/preact-vite',
50+
},
51+
// Configure Vite to transform JSX to Preact's `h` function instead of React's `React.createElement`
52+
// This automatically injects Preact imports so you don't need to import h and Fragment in every file
53+
async viteFinal(config) {
54+
config.esbuild = {
55+
jsxFactory: 'h',
56+
jsxFragment: 'Fragment',
57+
jsxInject: `import { h, Fragment } from 'preact'`,
58+
};
59+
return config;
60+
},
61+
};
62+
63+
export default config;
64+
```
3065

31-
// helper method for creating storybook loader, which async creates our widget instance.
32-
import { createWidgetLoader } from '@merkur/tool-storybook';
66+
#### `.storybook/preview.mjs`
3367

34-
// Imports for updating storybook playground.
35-
import { FORCE_RE_RENDER } from '@storybook/core-events';
36-
import { addons } from '@storybook/preview-api';
68+
```javascript
69+
import { h, render } from 'preact';
70+
import { createPreviewConfig } from '@merkur/tool-storybook';
71+
import widgetProperties from '../src/widget.js';
72+
import View from '../src/views/View.jsx';
73+
import HeadlineSlot from '../src/slots/HeadlineSlot.jsx';
3774

38-
import WidgetContext from '../src/components/WidgetContext';
39-
// receive widget properties for creating our Merkur widget instance
40-
import widgetProperties from '../src/widget';
75+
// Create preview configuration with Merkur widget support
76+
const preview = {
77+
...createPreviewConfig({
78+
widgetProperties,
79+
}),
80+
// Preact render function
81+
render: (args, { loaded: { widget }, viewMode }) => {
82+
if (!widget) {
83+
return document.createElement('div');
84+
}
4185

42-
// register our widget to Merkur
43-
createPreactWidget(widgetProperties);
86+
const container = document.createElement('div');
87+
const ViewComponent = args.viewComponent === 'headline' ? HeadlineSlot : View;
88+
render(h(ViewComponent, widget), container);
4489

45-
// defined our custom widget loader
46-
export const loaders = [
47-
createWidgetLoader({
48-
render: () => {
49-
addons.getChannel().emit(FORCE_RE_RENDER); // widget must be able to update the storybook playground
90+
return container;
5091
},
51-
widgetProperties, // created widget properties
52-
})
53-
];
54-
55-
// if you need Context in React or Preact widget you must define Context Provider.
56-
export const decorators = [
57-
(Story, { loaded: { widget }}) => {
58-
return (
59-
<WidgetContext.Provider value={widget}>
60-
<Story />
61-
</WidgetContext.Provider>
62-
);
63-
},
64-
];
92+
};
93+
94+
export default preview;
6595
```
6696

67-
> Note: If you use any pre-processors (webpack loaders) for building CSS styles you should also define then in `webpackFinal` function by extending given `config` object. More on that topic can be found in [official Storybook documentation](https://storybook.js.org/docs/react/configure/styling-and-css).
97+
### Vanilla JavaScript Widget
98+
99+
For vanilla JavaScript widgets that render HTML strings, configure Storybook to use the HTML framework.
68100

69-
That's all. Now we can write our stories.
101+
#### `.storybook/main.mjs`
102+
103+
```javascript
104+
const config = {
105+
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
106+
framework: {
107+
name: '@storybook/html-vite',
108+
},
109+
};
70110

71-
## How to write stories
111+
export default config;
112+
```
72113

73-
You can use every [format](https://storybook.js.org/docs/react/writing-stories/introduction) which Storybook offers. For example we pick up `named exports` format and our counter component from demo page for `Preact` preview.
114+
#### `.storybook/preview.mjs`
74115

75116
```javascript
76-
// /src/component/Counter.jsx
117+
import { createPreviewConfig } from '@merkur/tool-storybook';
118+
import widgetProperties from '../src/widget.js';
119+
import View from '../src/views/View.js';
120+
import HeadlineSlot from '../src/slots/HeadlineSlot.js';
121+
122+
// Create preview configuration with Merkur widget support
123+
const preview = {
124+
...createPreviewConfig({
125+
widgetProperties,
126+
}),
127+
// Vanilla JS render function
128+
render: (args, { loaded: { widget } }) => {
129+
if (!widget) {
130+
return document.createElement('div');
131+
}
132+
133+
const container = document.createElement('div');
134+
const viewFunction = args.viewComponent === 'headline' ? HeadlineSlot : View;
135+
container.innerHTML = viewFunction(widget);
136+
137+
return container;
138+
},
139+
};
140+
141+
export default preview;
142+
```
143+
144+
## Writing Stories
145+
146+
You can use any [story format](https://storybook.js.org/docs/react/writing-stories/introduction) that Storybook supports. Below are examples for both Preact and vanilla widgets.
147+
148+
### Preact Widget Stories
149+
150+
For Preact components that use context, wrap them in the appropriate provider:
151+
152+
```jsx
153+
// /src/components/Counter.jsx
77154
import { useContext } from 'preact/hooks';
78155
import WidgetContext from './WidgetContext';
79156

@@ -89,22 +166,29 @@ export default function Counter({ counter }) {
89166
</div>
90167
);
91168
}
169+
```
92170

93-
// /src/component/Counter.stories.jsx
94-
import Counter from './Counter';
171+
```jsx
172+
// /src/components/__tests__/Counter.stories.jsx
173+
import Counter from '../Counter';
174+
import WidgetContext from '../WidgetContext';
95175

96176
export default {
97177
title: 'Counter',
98178
args: {
99179
// Every Merkur story must have defined props property
100180
widget: {
101181
props: {},
102-
}
182+
},
103183
},
104184
};
105185

106186
const Template = (args, { loaded: { widget } }) => {
107-
return <Counter counter={widget.state.counter} />
187+
return (
188+
<WidgetContext.Provider value={widget}>
189+
<Counter counter={widget.state.counter} />
190+
</WidgetContext.Provider>
191+
);
108192
};
109193

110194
export const DefaultCounter = Template.bind({});
@@ -116,9 +200,79 @@ TenCounter.args = {
116200
// change default widget state from 0 to 10
117201
state: {
118202
counter: 10,
119-
}
120-
}
203+
},
204+
},
121205
};
122206
```
123207

124-
Now run command `npm run storybook` and you will see our Counter component with two settings.
208+
### Vanilla JavaScript Widget Stories
209+
210+
For vanilla widgets that return HTML strings:
211+
212+
```javascript
213+
// /src/components/Counter.js
214+
export default function Counter(widget) {
215+
return `
216+
<div>
217+
<h3>Counter widget:</h3>
218+
<p>Count: <span data-merkur="counter">${widget.state.counter}</span></p>
219+
<button data-merkur="on-increase">
220+
increase counter
221+
</button>
222+
<button data-merkur="on-reset">
223+
reset counter
224+
</button>
225+
</div>
226+
`;
227+
}
228+
```
229+
230+
```javascript
231+
// /src/components/__tests__/Counter.stories.js
232+
import Counter from '../Counter';
233+
234+
export default {
235+
title: 'Counter',
236+
args: {
237+
// Every Merkur story must have defined props property
238+
widget: {
239+
props: {},
240+
},
241+
},
242+
};
243+
244+
const Template = (args, { loaded: { widget } }) => {
245+
const container = document.createElement('div');
246+
container.innerHTML = Counter(widget);
247+
return container;
248+
};
249+
250+
export const DefaultCounter = Template.bind({});
251+
252+
export const TenCounter = Template.bind({});
253+
TenCounter.args = {
254+
widget: {
255+
props: {},
256+
// change default widget state from 0 to 10
257+
state: {
258+
counter: 10,
259+
},
260+
},
261+
};
262+
```
263+
264+
## Running Storybook
265+
266+
Start Storybook with:
267+
268+
```bash
269+
npm run storybook
270+
```
271+
272+
You will see your Counter component with different configurations in the Storybook UI.
273+
274+
## Key Differences
275+
276+
- **Preact widgets**: Use `.jsx` extension, JSX syntax, and component-based rendering with `render(h(Component, props), container)`
277+
- **Vanilla widgets**: Use `.js` extension, return HTML strings, and render with `container.innerHTML = componentFunction(widget)`
278+
- **Framework config**: Preact uses `@storybook/preact-vite`, vanilla uses `@storybook/html-vite`

0 commit comments

Comments
 (0)