I am using Vue 3 + Storybook. Everything is working fine, except when I click "Show Code", it just shows everything instead of just the template.. what am I doing wrong?
Here is my story:
import Button from './Button.vue';
export default {
title: 'Components/Button',
component: Button
};
const Template = (args) => ({
// Components used in your story `template` are defined in the `components` object
components: { Button },
// The story's `args` need to be mapped into the template through the `setup()` method
setup() {
return { args };
},
// And then the `args` are bound to your component with `v-bind="args"`
template: '<my-button v-bind="args" />',
});
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button',
};
export const Large = Template.bind({});
Large.args = {
size: 'large',
label: 'Button',
};
export const Small = Template.bind({});
Small.args = {
size: 'small',
label: 'Button',
};
As outlined in the screenshot below, it does work, as you would expect in Vue 2.
However, I'm getting the same results as you with Vue 3.
It's not implemented for Vue 3 yet.
As you can see in the source code of the docs add-on for Storybook, there is a separate implementation for the Vue 3 framework. However, the Vue 3 implementation lacks the source decorator, which generates a rendered version of the source code.
If you don't want to wait until the Storybook team has released an update, you can use the following code to generate your own docs, based on your arguments. Keep in mind that this does not cover all use cases.
const stringifyArguments = (key, value) => {
switch (typeof value) {
case 'string':
return `${key}="${value}"`;
case 'boolean':
return value ? key : '';
default:
return `:${key}="${value}"`;
}
};
const generateSource = (templateSource, args) => {
const stringifiedArguments = Object.keys(args)
.map((key) => stringifyArguments(key, args[key]))
.join(' ');
return templateSource.replace('v-bind="args"', stringifiedArguments);
};
const template = '<my-button v-bind="args" />';
const Template = (args) => ({
components: { MyButton },
setup() {
return { args };
},
template,
});
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
Primary.parameters = {
docs: {
source: { code: generateSource(template, Primary.args) },
},
};
Another temporary solution is to manually write the source code, instead of having it automatically generated.
Primary.parameters = {
docs: {
source: { code: '<my-button primary label="Button" />' },
},
};
This is a known issue
One of possible options is to use current workaround that I found in the GH issue by the link above.
Create file withSource.js
in the .storybook
folder with following content:
import { addons, makeDecorator } from "@storybook/addons";
import kebabCase from "lodash.kebabcase"
import { h, onMounted } from "vue";
// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;
function templateSourceCode (
templateSource,
args,
argTypes,
replacing = 'v-bind="args"',
) {
const componentArgs = {}
for (const [k, t] of Object.entries(argTypes)) {
const val = args[k]
if (typeof val !== 'undefined' && t.table && t.table.category === 'props' && val !== t.defaultValue) {
componentArgs[k] = val
}
}
const propToSource = (key, val) => {
const type = typeof val
switch (type) {
case "boolean":
return val ? key : ""
case "string":
return `${key}="${val}"`
default:
return `:${key}="${val}"`
}
}
return templateSource.replace(
replacing,
Object.keys(componentArgs)
.map((key) => " " + propToSource(kebabCase(key), args[key]))
.join(""),
)
}
export const withSource = makeDecorator({
name: "withSource",
wrapper: (storyFn, context) => {
const story = storyFn(context);
// this returns a new component that computes the source code when mounted
// and emits an events that is handled by addons-docs
// this approach is based on the vue (2) implementation
// see https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts
return {
components: {
Story: story,
},
setup() {
onMounted(() => {
try {
// get the story source
const src = context.originalStoryFn().template;
// generate the source code based on the current args
const code = templateSourceCode(
src,
context.args,
context.argTypes
);
const channel = addons.getChannel();
const emitFormattedTemplate = async () => {
const prettier = await import("prettier/standalone");
const prettierHtml = await import("prettier/parser-html");
// emits an event when the transformation is completed
channel.emit(
SNIPPET_RENDERED,
(context || {}).id,
prettier.format(`<template>${code}</template>`, {
parser: "vue",
plugins: [prettierHtml],
htmlWhitespaceSensitivity: "ignore",
})
);
};
setTimeout(emitFormattedTemplate, 0);
} catch (e) {
console.warn("Failed to render code", e);
}
});
return () => h(story);
},
};
},
});
And then add this decorator to preview.js
:
import { withSource } from './withSource'
...
export const decorators = [
withSource
]
Author of the solution
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With