storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.55k stars 9.29k forks source link

Add support for Vue SFC syntax to stories #9768

Open visualjerk opened 4 years ago

visualjerk commented 4 years ago

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 commented 4 years ago

@visualjerk this looks awesome!

Related: https://github.com/storybookjs/storybook/issues/7729

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

Aaron-Pool commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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

visualjerk commented 4 years ago

@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>

shilman commented 4 years ago

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: https://github.com/storybookjs/storybook/pull/10089

graup commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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.

sethidden commented 3 years ago

Any news on this?

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

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