stencil
stencil copied to clipboard
Integrate with Storybook
Stencil version:
All
I'm submitting a:
[ ] bug report [x] feature request [ ] support request
Current behavior:
Works great, but isn't explicitly supported on open-source Storybook.
Expected behavior:
Have an implementation supported explicitly by Storybook and listed in their "Guides".
Steps to reproduce: n/a (visit the website)
Related code: n/a
Other information: n/a
My setup: https://gist.github.com/jpzwarte/0be6a491a3762f2ee2e784ab29669a2c
I still have to figure out how to get <Props>
to work; something to do with the readme stencil outputs during the build.
@jpzwarte you have to generate a custom-elements.json file by adding config in your stencil.config.ts
import { Config } from '@stencil/core';
export const config: Config = {
namespace: 'roadtrip',
outputTargets: [
{
type: 'docs-vscode',
file: 'custom-elements.json'
}
]
};
import it in preview.js
import customElements from '../custom-elements.json';
setCustomElements(customElements);
then in a component story add the component:
parameter in the default export with the name of the component.
export default {
title: 'Forms|Input',
component: 'my-input',
};
https://github.com/storybookjs/storybook/tree/next/addons/docs/web-components#custom-elementsjson
@dmartinjs thanks, that helps. But your solution does not allow for JSX in an MDX story, right?
@jpzwarte I didn't try in a MDX story.
@dmartinjs there must be some part still missing. I generate the custom-elements.json
and import that, but don't i need to import some generated JS from dist/
as well? Currently, none of my web-components are working.
@jpzwarte you don't have to import something else, Storybook use the data generated in the custom-elements.json to display props.
Is the solution of @dmartinjs up to date? The link is not working now
@wjureczka a folder has been removed or moved in the storybbok repository.
You can still find the link here: https://github.com/storybookjs/storybook/tree/v6.1.21/addons/docs/web-components
but the solution isn't up-to-date anymore, the docs-vscode
output isn't working now,
for generating the custom-elements.json
you'll need to add a docs-custom output as follow:
stencil.config.ts
import { promises as fs } from 'fs';
import { Config } from '@stencil/core';
import { JsonDocs } from '@stencil/core/internal';
async function generateCustomElementsJson(docsData: JsonDocs) {
const jsonData = {
version: 1.2,
tags: docsData.components.map((component) => ({
name: component.tag,
path: component.filePath,
description: component.docs,
attributes: component.props
.filter((prop) => prop.attr)
.map((prop) => ({
name: prop.attr,
type: prop.type,
description: prop.docs,
defaultValue: prop.default,
required: prop.required,
})),
events: component.events.map((event) => ({
name: event.event,
type: event.detail,
description: event.docs,
})),
methods: component.methods.map((method) => ({
name: method.name,
description: method.docs,
signature: method.signature,
})),
slots: component.slots.map((slot) => ({
name: slot.name,
description: slot.docs,
})),
cssProperties: component.styles
.filter((style) => style.annotation === 'prop')
.map((style) => ({
name: style.name,
description: style.docs,
})),
cssParts: component.parts.map((part) => ({
name: part.name,
description: part.docs,
})),
})),
};
await fs.writeFile(
'./custom-elements.json',
JSON.stringify(jsonData, null, 2),
);
}
export const config: Config = {
namespace: 'yourNamespace',
outputTargets: [
{
type: 'docs-custom',
generator: generateCustomElementsJson
},
],
};
import it in preview.js
import customElements from '../custom-elements.json';
setCustomElements(customElements);
then in a component story add the component:
parameter in the default export with the name of the component.
export default {
title: 'Forms|Input',
component: 'my-input',
};
I will try it, thanks for your time! <3
Hi together.
I've been working with stencil for a long time and I'm thrilled. In doing so, I also docked the storybook. But please don't include that in stencil. Because then there will be some collisions. The package Json is also mega large and confusing (> 50 deps). There are also file extension collisions, with tsx. I use tsx for my React stories. mdx is also possible, but linting is not optimal there.
How can I run the components in IE? So how do I use the ES5 modules from a React app? I think the documentation still has a gap. It doesn't work for me and I don't see why.
So far, I would really like to have more contact. Good JOB!
@deleonio I think there is a misunderstanding, the point here isn't to integrate Storybook in Stencil codebase, but make the integration of Stencil components and documentation easier in Storybook.
But I think this issue can be closed since the Storybook team is investigating to build official support for Stencil.
A first milestone would be to run stories writing jsx so to render them in stencil. A complete solution would include compiling stencil components through storybook.
I have a PR open for the first one. The only drawbacks are:
- you need to run npm run build:watch and npm run storybook separately.
- while editing stories leverage stencil HMR, editing components for the moment forces full refresh.
Here's my PR: https://github.com/storybookjs/storybook/pull/15479/files
In the mean time you can hack around and configure it standalone using @storybook/html.
In preview.js
define the following decorator:
import { renderVdom, registerHost, getHostRef, h } from '@stencil/core/internal/client';
import { defineCustomElements } from '../dist/esm/loader';
defineCustomElements();
const rootElement = document.getElementById('root');
const storyRoot = document.createElement('div');
rootElement.parentElement.appendChild(storyRoot);
registerHost(storyRoot, { $flags$: 0, $tagName$: 'story-root' })
const hostRef = getHostRef(storyRoot);
export const decorators = [
(Story) => {
renderVdom(hostRef, Story());
return '<div />';
}
];
In main.js
define babel plugin:
babelDefault: (config) => {
return {
...config,
plugins: [
...config.plugins,
[require.resolve('@babel/plugin-transform-react-jsx'), { pragma: 'h' }, 'preset'],
],
};
}
Write your story like:
import { h } from '@stencil/core';
export default {
title: 'Welcome',
};
export const Default = () => {
return (
<container-component>
<div>Header</div>
<div><data-component richData={{ foo: 'bar' }}></data-component></div>
<div>Footer</div>
</container-component>
)
};
My 5 cents: Storybook
is a nightmare of dependencies, it does a lot, but bring also a lot of issues during the development. Integrating Stencil
with Storybook
is like integrating it with some UI/UX Angular-like framework (if you know what I mean haha).
That said, I'd love to see Stencil
integrated with an "underdog" UI/UX framework, something like:
- https://github.com/glorious-codes/glorious-pitsby
- https://utopia.app/
@zimaah the cons of using an underdog is that they have almost no integrations with other tools and a smaller community to help you when you're stuck...
In the other side, Storybook is listed in all design system surveys, have a lot of integrations and can be extensible with plugins.
Thanks @marcolanaro for workaround But I am facing below issue while following this process, Any Idea..?

@apurvaojas I'm experiencing a similar error while trying the decorator pattern from this blog post, which, similar to the above process utilizes renderVDom()
:

This is in stroybook 6.5. In earlier versions of storybook we were able to get stories work with stencil components by having the stories return a string, i.e.:
export const Default = args => `<my-component some-attr="${args.someAttr}" />`
Someone else on our team set up our storybook, but I assume that the above relied on stroybook's web component addon (as well as calling defineCusomElements()
in preview.js. We'd also have to use custom story decorators to set object/array props after the story rendered. All in all kind of clunky, but it worked.... until it didn't. At some point the strings returned by the stories started being rendered in the dom as strings

instead of elements

So we're now in a spot where some patch update in some dependency of some storybook addon made all our stories useless (just rendering the element tag as a string), so we're trying to change our stories not to return strings and use what appears to be a more robust approach to integrating stencil into storybook (i.e. using renderVDom()
, but we're getting a similar error to @apurvaojas. We've invested in dozens of stories, and now none of them work.
I'd have hoped that the stencil team would have a better, less hacky, more first class story for storybook integration given the intent of Stencil is to author UI libraries.
For anyone else who ends up here, it seems like @marcolanaro has a more recent solution here: https://github.com/storybookjs/storybook/issues/4600#issuecomment-899055226
And it looks like the storybook and stencil teams were interested in using that as the basis for an official integration, but it's not clear to me where that effort ended up.
I feel like having official support for Storybook would bring more people over to Stencil. Many design systems use Storybook and having that logo anywhere on the Stencil page would seal the deal for many looking towards componentization. What initially attracted me to Stencil was that much of the tooling was already done for me (scss, framework bindings, etc.).
Storybook 7 was just released today. How awesome it would be to be able to strap on the Storybook jetpack to jump between development and design space. I'm hoping there's someone well versed enough in both tools to make this happen. I would donate to that end.
I'm working with Storybook 7 and I just added the below but it does not seem to work. I don't get any errors but I still have to add my arg types manually.
/* preview.js */
import { setCustomElementsManifest } from '@storybook/web-components';
import customElements from '../vscode-data.json';
setCustomElementsManifest(customElements);
inspired by this post, we managed to get it working in by loading stencil's compiler within storybook.
/* main.js */
module.exports = {
...,
webpack: config => {
return {
...config,
module: {
...config.module,
rules: [
...config.module.rules,
{
test: /\.(tsx)$/,
loader: path.resolve('./.storybook/loader.js'),
},
],
},
};
},
...
};
/* loader.js */
const stencil = require('@stencil/core/compiler');
module.exports = function (source) {
const callback = this.async();
patchedSource = patchSourceWithFragment(source);
const compiled = stencil.transpileSync(patchedSource, { sourceMap: false });
callback(null, compiled.code);
};
function patchSourceWithFragment(source) {
const regex = /import\s*\{([\w\s,]+)\}\s*from\s*'@stencil\/core';/;
const match = source.match(regex);
const fragment = 'Fragment';
if (match) {
const existingImports = match[1].split(',').map(item => item.trim());
if (!existingImports.includes(fragment)) {
const newImports = [...existingImports, fragment].join(', ');
return source.replace(regex, `import { ${newImports} } from '@stencil/core';`);
}
console.error('Import already exists.');
return source;
}
console.error('No matching import found.');
return source;
}
with this setup, stories can be written as per the storybook docs if your props are nice and simple.
To use a callback (e.g. searching/filtering in a table of data), and allow the parent to govern the state, we can use renderVdom
and some jsx like below. this is analagous to using React's useState.
/* myComponent.stories.tsx */
import { getHostRef, registerHost, renderVdom } from '@stencil/core/internal/client';
export const WithRenderVdom: Story = {
render: () => {
const rootRef = document.getElementById('storybook-root').appendChild(document.createElement('div'));
registerHost(rootRef, { $flags$: 0, $tagName$: 'host' });
const hostRef = getHostRef(rootRef);
const render = () => {
renderVdom(
hostRef,
<div style={{ margin: '50px' }}>
<my-component data={data} searchHandler={searchHandler}></my-component>
</div>,
);
};
// An example arg which we want to update via a callback
let data = getTableData();
const searchHandler = searchValue => {
const d = getTableData();
const filteredData = {
columns: d.columns,
rows: d.rows.filter(r => {
return String(r.data.Name).toLowerCase().includes(String(searchValue.value).toLowerCase());
}),
};
data = filteredData;
render();
};
render();
return rootRef;
},
};
*/
using storybook 7.0.7. and these framework builders:
"@storybook/web-components": "7.3.2",
"@storybook/web-components-webpack5": "7.3.2",
doing it this way, we've found 3 annoying things so far:
- you can't kill the
npm run storybook
process in the terminal using ctrl-C. the terminal session has to be killed entirely. - due to using the stencil compiler, the component is rendered twice in storybook. so there's a short "flicker" as it reloads twice.
- interaction tests (storybook Plays) aren't seamless. we are finding we have to add arbitrary waits, due to waiting for the stencil compiler to finish and then attach the elements
I would really like feedback from the ionic/stencil team please if this is an appropriate use of getHostRef
, registerHost
, renderVdom
.
and, if any expert storybook users can suggest improvements (such as moving our render to somewhere with global effect ) it would be most appreciated!