Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Vue SFC syntax to stories #9768

Open
visualjerk opened this issue Feb 5, 2020 · 20 comments
Open

Add support for Vue SFC syntax to stories #9768

visualjerk opened this issue Feb 5, 2020 · 20 comments

Comments

@visualjerk
Copy link

Is your feature request related to a problem? Please describe.
When writing a story for Vue components inside mdx files, it differs from the way one would write the code inside a .vue file.

Also when reading the story source inside storybook, the code differs from the code one would normally put inside a .vue file.

So there is this mental switch developers have to do, when using storybook as their component documentation. This is especially troublesome for new developers, which in many teams are the main target group of the documentation.

Describe the solution you'd like
In addition to writing a Vue story like this:

<Story name='basic' height='400px'>{{
  components: { InfoButton },
  template: '<info-button label="I\'m a button!"/>',
}}</Story>

It would be nice to add support for stories in this format:

<Story name='basic' height='400px'>
<template>
  <info-button label="I\'m a button!"/>
</template>
<script>
  export default {
    components: { InfoButton },
  }
</script>
</Story>

Are you able to assist bring the feature to reality?
yes

@shilman
Copy link
Member

shilman commented Feb 6, 2020

@visualjerk this looks awesome!

Related: #7729

@Aaron-Pool has thought about this problem quite a bit and might be able to give pointers.

@Aaron-Pool
Copy link
Contributor

Yeah, so there's two major parts of trying to get this feature implemented

  1. making it work
  2. making the developer experience not feel horrible

As for 1, I think we could use Babel macros to run just the contents of a story block (with a comment to trigger the macro, or something similar) through the Vue sfc compiler and Vue Babel preset.

2 is much trickier. And would be editor dependent. But for vscode, at least, I'm pretty confident it would be possible to write an extension that could detect .vue as an "embedded language" within story blocks marked with a Vue indicator. I dunno how well code complete and intellisense would work, but the syntax highlighting would probably be fine, at least.

@visualjerk
Copy link
Author

Thanks for the insights @Aaron-Pool

Regarding 1: Do you already have plans to work on that? If not, I would be glad to help implementing it.

Regarding 2: At least for syntax highlighting in VS Code the silvenon.mdx extension already does a pretty good job with stories written in this syntax.

@Aaron-Pool
Copy link
Contributor

Aaron-Pool commented Feb 7, 2020

@visualjerk I've had "plans" to work on it for a while now, but my day job has been eating up most of coding creative energy as of late. I'd be glad to offer guidance and help with any technical hurdles if you started a PR though 👌

And glad to know the current MDX plugin does a decent job already, I though I remembered it not knowing how to handle a non-root script tag, but I'm glad to be wrong 🤙

@rdhainaut
Copy link

rdhainaut commented Feb 16, 2020

It sounds a great idea.

@visualjerk
But if you want a awesome developer experience maybe the best is to use SFC (single file component) and the CSF (component story format).

// ButtonNormal.story.vue
<template>
    <Button>Normal Button</Button>
</template>

<script>
import Button from '../src/components/Button.vue';

export default {
    name: 'ButtonNormal',
    components: { Button },
};
</script>
// Button.stories.ts
import ButtonNormalStory from './ButtonNormal.story.vue';

export default {
    title: 'Button',
};

export const normalButton = () => ButtonNormalStory;

See this article for more details
https://dev.to/josephuspaye/using-storybook-with-vue-single-file-components-2od

Note: The cons is the story source is not pertinent (displayed via story-source / docs addon). The article explains how to load the SFC vue story but i have tried without success.

@shilman
Copy link
Member

shilman commented Feb 17, 2020

@rdhainaut will try to get an addon-docs Source fix for this scenario in 6.0 .. 🤞

@visualjerk
Copy link
Author

visualjerk commented Mar 2, 2020

@rdhainaut thanks for pointing out that solution. Maybe we will give it a try and see how it works with multiple stories in one docs file.

@Aaron-Pool After working on the feature request for a while I can give a quick update:

1. Implementation

The current implementation uses babel parser, which unfortunately does not support custom plugins: https://babeljs.io/docs/en/babel-parser#will-the-babel-parser-support-a-plugin-system

So it looks like we need a solution that handles SFC stories before babel parser does.

A possible approach would be to detect SFC stories and transform them to JSX stories. Afterwards babel can handle them just fine.

Additionally we would have to keep the original SFC story code and put it into the mdxSource later on, so docs can show it in the code preview.

2. Developer experience

I was wrong 😄 While the mdx extension mentioned above works fine for simple code snippets, it breaks with things like :rounded="true inside the template tag.

However we could provide a different syntax for SFC stories that works like a charm:

<Story name="to storybook" height="300px">
  ```vue
  <template>
    <button>click me {{ name }}</button>
  </template>
  <script>
    export default {
      data() {
        return {
          name: 'foo'
        }
      }
    }
  </script>
  ```
</Story>

@shilman
Copy link
Member

shilman commented Mar 24, 2020

If anybody here hasn't seen it, I recently made source snippet customizable, which could be used to improve the Source block for these SFC stories (which are awesome!!!). Here's the PR: #10089

@shilman shilman added this to the 6.0.0 milestone Mar 24, 2020
@storybookjs storybookjs deleted a comment from stale bot Mar 29, 2020
@graup
Copy link
Contributor

graup commented Mar 29, 2020

@visualjerk Is there any standard/precedent for this ```vue ... string? It looks a bit odd to me. It might make syntax highlighting difficult? I wonder if we should coordinate with @mdx-js/vue-loader to add this feature there first.

@shilman
Copy link
Member

shilman commented Mar 29, 2020

@graup thanks for the reference. I think https://github.com/mdx-js/mdx/tree/master/packages/vue-loader is the converse of what we want. It takes MDX and loads it into a Vue component. We want to load MDX into a React component since Storybook Docs is implemented in React. I'm also not sure whether @visualjerk 's approach is the best one, but it seems very reasonable at first glance. Definitely open for discussion.

@graup
Copy link
Contributor

graup commented Mar 29, 2020

@shilman you're right, I confused the purpose of @mdx-js/vue-loader.

Here's another idea instead of <Story> ```vue: how about something like <Story lang="vue">? That would feel pretty familiar to vue developers and be just as easily regex-able.

@Aaron-Pool
Copy link
Contributor

@graup I think the reason he went with the notated template string was because this gives us a lot of the DX provided by the markdown syntax highlighter for free (in most code editors, at least) 🤷‍♂️

@Aaron-Pool
Copy link
Contributor

If we want a different syntax, and a decent developer experience, we're going to need to work the MDX syntax highlighting extension and add some additional functionality to detect story block lang attribute.

@visualjerk
Copy link
Author

@graup Initially I was aiming for a solution similar to the one you proposed. But as @Aaron-Pool pointed out the syntax highlighting with this approach was not great. Having the fenced code block with vue around it lets markdown syntax highlighter recognize it as Vue SFC code.

@shilman shilman modified the milestones: 6.0, 6.0 docs May 8, 2020
@shilman shilman modified the milestones: 6.0 docs, 6.1 docs Jun 24, 2020
@shilman shilman modified the milestones: 6.1 docs, 6.2 docs Oct 13, 2020
@sethidden
Copy link

sethidden commented Apr 20, 2021

Any news on this?

The current approach of wirting the template in inline string template: "<div></div>" has some flaws:

  • eslint-plugin-vue rules are not checked in inline strings
  • no syntax highlighting (unless you use the vue` tagged template)
  • no language features (LSP)
template: vue`<my-component :title="123"/>` (this highlights for me in Vetur)

Right now as shown above, it's possible to export a .vue file in .stories.js like

import MyVueStoryForButtonWithOverflowingContent from '@/button/stories/OverflowingContent.vue`
export const ButtonWithOverflowingContent = () => MyVueStoryForButtonWithOverflowingContent;

Assuming that right now you use the inline template string approach, you probably have one template, but then you reuse it eg. 5 times to show different scenarios eg.

const Template = (args, {argTypes}) => {
   props: Object.keys(argTypes),
   template: `<my-button v-bind="$props">{{text}}</my-button>
}
export const Basic = Template.bind({}).args = {text: 'Hello', disabled: false}
export const Disabled = Template.bind({}).args = {text: 'Hello', disabled: true}
//add some 5 more cases here

with the way current .story.vue components work, you can't really reuse the template from above (say you've created an SFC that just contains <my-button v-bind="$props"> in the same way. You need to create a new story.vue component for each case

Sure you can do {components: MyStoryComponent, template: "<my-story-component/>"} but that's also just adding another layer on top

It'd be perfect if it was possible to write export default (args, {argTypes}) => {props: {prop1: String}, computed() {}} in SFC <script> and still have the text from attach, but that probably requires a custom loader (right now vue-docgen fails to parse)

@tobiasdiez
Copy link
Contributor

I was also getting a bit frustrated by the missing support for native vue stories and thus wrote a simple storybook addon that enables idiomatic vue stories. The package is still in an early stage, but please feel free to play around with it: https://github.com/tobiasdiez/storybook-vue-addon

<script setup lang="ts">
import Button from './Button.vue'
</script>
<template>
  <Stories
    title="Stories in Vue format 😍"
    :component="Button"
  >
    <Story title="Example">
      <Button
        background="#ff0"
        label="😄👍😍💯"
      />
    </Story>
  </Stories>
</template>

@matthew-dean
Copy link

@tobiasdiez Omg, that looks so good. That (or something like it) should become the official way to do Storybook stories in Vue. The .vue format is much more versatile and a better DX.

@andreasvirkus
Copy link

@tobiasdiez honestly massive props for this addon! ❤️ both the TS & MDX format felt so clunky and boilerplate'y and not at all Vuey... Oh, but this * chef's kiss *

@floroz
Copy link

floroz commented Jul 13, 2023

I was also getting a bit frustrated by the missing support for native vue stories and thus wrote a simple storybook addon that enables idiomatic vue stories. The package is still in an early stage, but please feel free to play around with it: https://github.com/tobiasdiez/storybook-vue-addon

<script setup lang="ts">
import Button from './Button.vue'
</script>
<template>
  <Stories
    title="Stories in Vue format 😍"
    :component="Button"
  >
    <Story title="Example">
      <Button
        background="#ff0"
        label="😄👍😍💯"
      />
    </Story>
  </Stories>
</template>

This should be the default way to write Vue stories in Storybook!

@JoaoHamerski
Copy link

JoaoHamerski commented Mar 19, 2024

If anyone is looking for an alternative to string template, you can use JSX in a separated file and just import your stories in the main story file.

Use vitejs/plugin-vue-jsx to support .jsx files.

Here's an example:

import { defineComponent } from "vue";
import AppButton from "@/components/button/AppButton.vue";

const ButtonDefault = defineComponent({
    render ({ $attrs }) {
        // Here we can use JSX as we want, way better DX than string template
        return <AppButton {...args} />
    }
})

// OR 

const ButtonDefault = defineComponent({
    setup (_, { attrs }) {
        return () => <AppButton {...attrs} />
    }
})

// Here you'll use the default story structure
export default {
    render: (args) => ({
        components: {
            ButtonDefault
        },
        setup () {
            return { args }
        },
       // Then just use the .jsx component here.
        template: '<ButtonDefault v-bind="args" />'
    })
}
// Now import the file 
import ButtonDefault from './ButtonDefault.tsx'

// ...your meta story here...

export const Default = ButtonDefault

Since @floroz lib looks deprecated. There's a fork of his lib also, but in the current date, it seems to be in early stages: https://storybook.js.org/addons/storybook-vue-csf-addon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests