storybookjs / storybook

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

Vue 3: Dynamic snippet rendering #13917

Closed ivanRemotely closed 1 year ago

ivanRemotely commented 3 years ago

Describe the bug When clicking the "View Source" option in the docs tab, the presented code is the template for the story as opposed to the generated Vue code. (See screenshot below)

To Reproduce Steps to reproduce the behavior:

The issue is present in the vue3 cli example, so you can:

  1. Clone, install and run https://github.com/storybookjs/storybook/tree/next/examples/vue-3-cli
  2. Open any component
  3. Go to the Docs tab
  4. Click "Show Source"

Expected behavior The presented panel should show usable Vue code.

Screenshots

Screen Shot 2021-02-15 at 3 09 29 PM

System

Environment Info:

System: OS: macOS 10.15.6 CPU: (8) x64 Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz Binaries: Node: 12.13.0 - ~/.nvm/versions/node/v12.13.0/bin/node npm: 6.12.0 - ~/.nvm/versions/node/v12.13.0/bin/npm Browsers: Chrome: 88.0.4324.150 Firefox: 85.0.2 Safari: 13.1.2 npmPackages: @storybook/addon-actions: 6.2.0-alpha.28 => 6.2.0-alpha.28 @storybook/addon-essentials: 6.2.0-alpha.28 => 6.2.0-alpha.28 @storybook/addon-links: 6.2.0-alpha.28 => 6.2.0-alpha.28 @storybook/addon-storyshots: 6.2.0-alpha.28 => 6.2.0-alpha.28 @storybook/vue3: 6.2.0-alpha.28 => 6.2.0-alpha.28 npmGlobalPackages: @storybook/cli: 6.2.0-alpha.24

Additional context I understand Vue 3 support is still a WIP. I'd be more than happy to help fix the issue if you can point me in the general direction of the problem. Thank you for you time and help!

shilman commented 3 years ago

This is available for Vue2 and is implemented here: https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts

Unfortunately the VNodes are not exposed in the same way in Vue3, so we need to figure out a different approach. @phated can elaborate on this, or perhaps this is enough to get you going @ivanRemotely ?

phated commented 3 years ago

That's correct, vnode's no longer contain most (any?) of the information that the sourceDecorator uses. From my deep-dive, it seems that this feature would need to be implemented with a "custom renderer", akin to Vue 3's SSR support. This would be done with https://v3.vuejs.org/api/global-api.html#createrenderer but it looks really complex (and I couldn't figure out how to do it).

ivanRemotely commented 3 years ago

Thanks for the info, I'll dig into it. I'm very new at Storybook so I can't guarantee results but I'll give it a spin nonetheless and see what sticks. I was able to at least get the template source by grabbing context().render().type.template, looking into renderers now.

My main question is: What I don't fully understand is what is the scope of this work? Would grabbing the name of the component and the used props be enough? I guess if the component has slots and nested components it would make the code incomplete...

phated commented 3 years ago

@ivanRemotely Trying to use template won't really work because many Vue 3 components don't have a template. A vnode can be made up of a template, a render function returning something constructed from h() or a setup function that returns a render function. The only way to have true coverage over any type of component is to implement a "string renderer" using createRenderer API.

As for the scope, it seems very large to me, as you have to implement everything a vnode can contain. The best "simplified" example of a renderer I could find is https://github.com/vuejs/vue-next/tree/master/packages/runtime-test (which is still quite complex).

lee-chase commented 3 years ago

Ooh was about to raise the same issue.

Workaround 1 roll it yourself.

const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { MyButton },
  template: '<my-button @click="onClick" v-bind="$props" />',
});
export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};
Primary.parameters = {
  docs: { source: { code: '<my-button @click="onClick" v-bind="$props" />' } },
};

Workaround 2 roll it yourself a bit more thoroughly. Much of which could be extracted to a utility function

const templateSourceCode = (templateSource, args, replacing = 'v-bind="$props"') => {
  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(args)
      .map((key) => propToSource(key, args[key]))
      .join(' ')
  );
};

const template = '<my-button @click="onClick" v-bind="$props" />';
const Template = (args, { argTypes }) => ({
  props: Object.keys(argTypes),
  components: { MyButton },
  template,
});

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

Primary.parameters = {
  docs: { source: { code: templateSourceCode(template, Primary.args) } },
};
RobineSavert commented 3 years ago

Thanks for the answers but workaround 2 breaks the Canvas tab.. it's also not Vue 3 which uses setup to bind the args/props

seanwuapps commented 3 years ago

Thanks for the answers but workaround 2 breaks the Canvas tab.. it's also not Vue 3 which uses setup to bind the args/props

Workaround 2 worked for me, make sure your template uses v-bind="$props" as that's what the function is looking for. or you can add the third argument when you call the templateSourceCode function to replace a different string in your template.

I do look forward to seeing this fixed officially, sorry I don't have enough knowledge in Storybook to create a PR

JamesCommits commented 3 years ago

Workaround 2 does indeed work, but unfortunately the template output is static and not dynamic to the controls and their current state in storybook.

mtorromeo commented 3 years ago

This is my workaround based on the code from @lee-chase.

For simplicity, I made it work by only looking for templates that are surrounded by backticks. The code goes in .storybook/preview.js and works globally without any modification required to the stories (easier to remove when this gets eventually fixed).

import dedent from "ts-dedent";
import { paramCase } from "param-case";

const 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(paramCase(key), args[key]))
      .join(""),
  );
};

export const parameters = {
  docs: {
    transformSource(src, ctx) {
      const match = /\b("')?template\1:\s*`([^`]+)`/.exec(src);
      if (match) {
        return templateSourceCode(dedent(match[2]), ctx.args, ctx.argTypes);
      }
      return src;
    },
  }
};

For my use-case I decided to add a filter to show only the props, but it can be easily removed.

lauthieb commented 2 years ago

Hi! We are very interested at Decathlon (https://github.com/decathlon/vitamin-web) in the resolution of this issue in Storybook but unfortunately, I don't know Storybook stack to contribute. Feel free if you have questions, would love to help you from consumer side. Thanks for the great job you do.

shilman commented 2 years ago

Hey @lauthieb!! Big fan of your work! 👋 Last time I checked, it was a technical blocker on the vue3 side of things -- unlike vue2, the output of Storybook's render function is not straightforward to turn into a string. @pocka is this still a blocker?

lauthieb commented 2 years ago

Hi @shilman, thanks for your quick answer! For the moment I've implemented the workaround from @mtorromeo based on @lee-chase solution, it works well but I'm ready to migrate to built-in support in the next versions of @storybook/vue3 for sure! https://github.com/Decathlon/vitamin-web/commit/a47e90d601e6858f24660e28e33aa6b46c662589

productdevbook commented 2 years ago
CleanShot 2021-11-09 at 11 13 34@2x

some problem

Radouch commented 2 years ago

Workaround by @mtorromeo worked great for me until update to Storybook 6.4.5.

After update docs are not rendered at all and in the console it claims that args is undefined at const val = args[k];

It seems that instead of args property there is initialArgs in the ctx object. So the last block is now:

export const parameters = {
  docs: {
    transformSource(src, ctx) {
      const match = /\b("')?template\1:\s*`([^`]+)`/.exec(src);
      if (match) {
        return templateSourceCode(dedent(match[2]), ctx.initialArgs, ctx.argTypes); // HERE is the change
      }
      return src;
    },
  }
};
mtorromeo commented 2 years ago

@Radouch replacing args with initialArgs is better than nothing, but it doesn't reflect changes to the props made at runtime.

Sadly, vue support in storybook is merely an afterthought and has too many quirks IMO.

tiagoskaneta commented 2 years ago

I'm using the following decorator (globally registered) to workaround this issue:

import { addons, makeDecorator } from '@storybook/addons'
import { h, onMounted } from 'vue'

// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`

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 from the depths of storybook
            const src = context.originalStoryFn.parameters.storySource.source
            // this extracts the template from the story source
            const match = /\b(["']?)template\1:\s*(["'`])([^\1]+)\2/.exec(src)
            if (match) {
              // generate the source code based on the current args
              const code = templateSourceCode(match[3], 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'
                  })
                )
              }

              emitFormattedTemplate()
            }
          } catch (e) {
            console.warn('Failed to render code', e)
          }
        })

        return () => h(story)
      }
    }
  }
})

The templateSourceCode is the same function as already mentioned above. Ugly but it works for now. Notice that this replaces the transformSource approach mentioned above.

I tried creating a custom renderer for vue as @phated mentioned above to try to include the necessary information but ended up having to copy a lot of code from @vue/runtime-dom - as it doesn't export the necessary methods - so I dropped that for now. May try again at some point.

pocka commented 2 years ago

I'm working on the Dynamic snippet rendering feature for Vue 3 in #17295. As I've never used Vue 3 for my projects, I want to hear opinions from Vue 3 users. I'll close the PR if the implementation does not cover common use cases or the PR does not get enough attention (again, I've never used Vue 3, what is "common" is unknown to me).

You can try the feature by editing examples/vue-3-cli/src/stories/addons/docs/DynamicSnippet.stories.js at pocka/feature/vue3-dynamic-code-rendering-experiments branch.

shilman commented 2 years ago

Thanks @pocka !!! I'll try to kick the tires this weekend

fenilli commented 2 years ago

I think the docs from https://storybook.js.org/docs/vue/writing-docs/doc-blocks should be updated with an warning that the vue3 example does not work as intended, with an explanation on how to disable the 'show/hide code'.

For now what i'm doing to make use of source in a way that makes sense is disabling source for all Canvas and implementing a static Source.

<Canvas withSource='none'>
  <Story 
    name="link button"
    args={{
      variant: 'link',
      default: 'link'
    }}
  >
    {Template.bind({})}
  </Story>
</Canvas>

<Source
  language="jsx"
  code={'<Button variant="link">link</Button>'}
/>
juzser commented 2 years ago

+1 this feature. Vue 3 is vue default version now, so it's really helpful if you can bring this feature to SB. Thank you.

kasperskov909 commented 2 years ago

+1 I was working on a workaround where I would create and mount a new vue instance in memory in the transformSource function so that I could extract source code and the generated outer HTML - only to realize that only the initial arguments are available as pointed out by @Radouch, meaning the end result would be static and not influenced by user input. Why was args removed from the storyContext argument? Or is it simply a limitation inherited from Vue3?

This feature is vital for the usage of Vue3 in SB. Manually writing docs/source code defeats the whole purpose IMO.

@pocka can you give us an update on your PR? Is it alive and if so, what's the plan?

tiagoskaneta commented 2 years ago

@kasperskov909 the "solution" I found for that is to make it a decorator instead of using the transformSource function. So the args are available and updated accordingly.

kasperskov909 commented 2 years ago

@tiagoskaneta alright sounds good. How do I implement your solution though?

tiagoskaneta commented 2 years ago

@kasperskov909 see https://github.com/storybookjs/storybook/issues/13917#issuecomment-1013019386 It's a bit of hit or miss sometimes so a more permanent approach is definitely required to really use SB with vue3.

kasperskov909 commented 2 years ago

@tiagoskaneta I mean how do you implement it globally on all stories? I've wrapped stories in markup with decorators before but I can't figure out how to use yours?

pocka commented 2 years ago

@kasperskov909

@pocka can you give us an update on your PR? Is it alive and if so, what's the plan?

The PR is inactive. As I stated above, the implementation needs feedback from Vue3 users so that we get an idea of whether it matches users' expectations. Also, we need to find a better way to lookup a story component (https://github.com/storybookjs/storybook/pull/17295#issuecomment-1024363417).

tiagoskaneta commented 2 years ago

@kasperskov909. I removed the transformSource from the docs parameters in the main.js. In the preview.js I have

import { withSource } from './withSource'

export const decorators = [
  withSource
]

So this will apply the decorator globally. I am seeing an issue with my solution though, where the transformed code is not being applied when changing pages, only when refreshing the page. So your millage may vary.

kasperskov909 commented 2 years ago

@tiagoskaneta right, thanks. My issue was with the SNIPPET_RENDERED. Works now.

Btw I can't recognize context.originalStoryFn.parameters.storySource.source. Are we not looking for context.originalStoryFn().template? That way the regex is redundant.

tiagoskaneta commented 2 years ago

@kasperskov909 https://github.com/tiagoskaneta/vue3-sb-code if you still need some reference.

And yes, looks like context.originalStoryFn().template does work. so better then the regex :)

For the record, this is the full decorator I'm using

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);
      },
    };
  },
});
kasperskov909 commented 2 years ago

@tiagoskaneta cheers, much appreciated. I'll work on a solution for the page reload/route change problem and post here. After that I will look to the PR for a permanent solution.

tiagoskaneta commented 2 years ago

@kasperskov909 using setTimeout(emitFormattedTemplate, 0); did work for me to some extend.

kasperskov909 commented 2 years ago

For anyone reading this. Note that my suggetion for @tiagoskaneta solution with using the originalStoryFn() doesn't work with default slots from props if you define your template in your story something like this:

const Template = (args) => ({
    components: { XButton, XIcon },
    setup() {
        return { args };
    },
    template: `  
    <x-button v-bind="args">
        <template v-slot:icon-left><x-icon :name="args['icon-left']" />
        </template>
        ${args.default}
    </x-button>
    `
});

${args} will be empty. You'll need to send the args from the context as parameters to the originalStoryFn():

const src = context.originalStoryFn(context.args).template;

args['icon-left'] still won't be parsed. I'm working on a solution. Should be manageable in templateSourceCode().

danspratling commented 2 years ago

Hey all, I know this is being worked on, but I just wanted to note that the exact same issue is also present in the MDX versions of storybook when working with vue 3

Screenshot 2022-05-26 at 19 07 42
mokone91 commented 2 years ago

Hi! I have use snippet from @tiagoskaneta it works!

But i faced with issue that code snipet not highlited properly

image

is there any idea how its can be fixed?

btw, i have added extra type switch entry to parse object values

 const propToSource = (key, val) => {
    const type = typeof val;
    switch (type) {
      case 'boolean':
        return val ? key : '';
      case 'string':
        return `${key}="${val}"`;
      case 'object':
        return `${key}="${JSON.stringify(val).replace(/"(\w+)"\s*:/g, '$1:').replaceAll('"', '\'')}"`; // here
      default:
        return `:${key}="${val}"`;
    }
  };
Radouch commented 1 year ago

Does somebody know if the problem with source code of Vue 3 stories is planned to be solved in Storybook 7?

Unfortunatelly, Storybook 7.0.0-beta.8 seems not to show Vue 3 source code correctly...

vhoyer commented 1 year ago

I may not be completely in the loop here, but, as far as I know, there is planning for porting Vue source generation but only after the the major is released, because for the major 7 release there has been a major refactor of various parts of the code, that being the case as far as I understand, they've paused all progress on other features outside of major 7. That being said I think there's a high chance of this feature becoming available in the near future after the major 7 release

shilman commented 1 year ago

Jiminy cricket!! I just released https://github.com/storybookjs/storybook/releases/tag/v7.0.0-beta.29 containing PR #20498 that references this issue. Upgrade today to the @next NPM tag to try it out!

npx sb@next upgrade --prerelease

Closing this issue. Please re-open if you think there's still more to do.

kasperskov909 commented 1 year ago

Update SB version 7: Beware that the channel.emit() "hack" does not work in SB version 7. Not really a problem as Vue snippet rendering has been fixed. If you need to render other forms of snippets like HTML and Web Components you need to update your channel.emit call params to: channel.emit(SNIPPET_RENDERED, { id, args, source }); Notice they have been wrapped in an object.

sttrbl commented 1 year ago

@kasperskov909

Maybe you know how to get story template initial value now (in SB 7)? originalStoryFn(context.args, context).template doesn't work now

kasperskov909 commented 1 year ago

@sttrbl origianlStoryFn is now an extension method on the context. Use context.originalStoryFn(context.args).template where context is an argument from the wrapper arrow function.

Spirit04eK commented 1 year ago

@shilman Sorry, I don't understand how PR #20498 fix this issue. I have storybook 7.4.0. How can I enable pretty source code for Vue 3 component?
Now, I always see image

I try solution by @tiagoskaneta. It works, but template not re-render after apply/change props.

itsJohnnyGrid commented 9 months ago

Hi, for those who struggling with source code rerender absence on args change, here is the solution:

PS: prettier2 is prettier@2 installed as alias (because prettier@3 doesn't sync). PS: PS: I also renamed decorator into vue3SourceDecorator.

import { addons, makeDecorator } from "@storybook/addons";
import { h, onMounted, watch } from "vue";

export const vue3SourceDecorator = makeDecorator({
  name: "vue3SourceDecorator",
  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
    // watch args and re-emit on change
    return {
      components: {
        story,
      },
      setup() {
        onMounted(() => {
          setSourceCode();
        });

        watch(context.args, () => {
          setSourceCode();
        });

        function setSourceCode() {
          try {
            const src = context.originalStoryFn(context.args).template;
            const code = templateSourceCode(src, context.args, context.argTypes);
            const channel = addons.getChannel();

            const emitFormattedTemplate = async () => {
              const prettier = await import("prettier2");
              const prettierHtml = await import("prettier2/parser-html");

              const formattedCode = prettier.format(code, {
                parser: "html",
                plugins: [prettierHtml],
                htmlWhitespaceSensitivity: "ignore",
              });

              // emits an event when the transformation is completed
              channel.emit("storybook/docs/snippet-rendered", (context || {}).id, formattedCode);
            };

            setTimeout(emitFormattedTemplate, 0);
          } catch (e) {
            // eslint-disable-next-line no-console
            console.warn("Failed to render code", e);
          }
        }

        return () =>
          h("div", { style: `padding: 2rem 1.5rem 3rem 1.5rem;` }, [h(story)]);
      },
    };
  },
});

function templateSourceCode(templateSource, args, argTypes) {
  const componentArgs = {};

  for (const [key, val] of Object.entries(argTypes)) {
    const value = args[key];

    if (
      typeof val !== "undefined" &&
      val.table &&
      val.table.category === "props" &&
      value !== val.defaultValue
    ) {
      componentArgs[key] = val;
    }
  }

  return templateSource
    .replace(
      'v-bind="args"',
      Object.keys(componentArgs)
        .map((key) => " " + propToSource(kebabCase(key), args[key]))
        .join(""),
    );
}

function propToSource(key, val) {
  const type = typeof val;

  switch (type) {
    case "boolean":
      return val ? key : "";
    case "string":
      return `${key}="${val}"`;
    default:
      return `:${key}="${val}"`;
  }
}

function kebabCase(str) {
  return str
    .split("")
    .map((letter, idx) => {
      return letter.toUpperCase() === letter
        ? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}`
        : letter;
    })
    .join("");
}
isorna commented 9 months ago

Hi, for those who struggling with source code rerender absence on args change, here is the solution:

PS: prettier2 is prettier@2 installed as alias (because prettier@3 doesn't sync). PS: PS: I also renamed decorator into vue3SourceDecorator.

import { addons, makeDecorator } from "@storybook/addons";
import { h, onMounted, watch } from "vue";

export const vue3SourceDecorator = makeDecorator({
  name: "vue3SourceDecorator",
  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
    // watch args and re-emit on change
    return {
      components: {
        story,
      },
      setup() {
        onMounted(() => {
          setSourceCode();
        });

        watch(context.args, () => {
          setSourceCode();
        });

        function setSourceCode() {
          try {
            const src = context.originalStoryFn(context.args).template;
            const code = templateSourceCode(src, context.args, context.argTypes);
            const channel = addons.getChannel();

            const emitFormattedTemplate = async () => {
              const prettier = await import("prettier2");
              const prettierHtml = await import("prettier2/parser-html");

              const formattedCode = prettier.format(code, {
                parser: "html",
                plugins: [prettierHtml],
                htmlWhitespaceSensitivity: "ignore",
              });

              // emits an event when the transformation is completed
              channel.emit("storybook/docs/snippet-rendered", (context || {}).id, formattedCode);
            };

            setTimeout(emitFormattedTemplate, 0);
          } catch (e) {
            // eslint-disable-next-line no-console
            console.warn("Failed to render code", e);
          }
        }

        return () =>
          h("div", { style: `padding: 2rem 1.5rem 3rem 1.5rem;` }, [h(story)]);
      },
    };
  },
});

function templateSourceCode(templateSource, args, argTypes) {
  const componentArgs = {};

  for (const [key, val] of Object.entries(argTypes)) {
    const value = args[key];

    if (
      typeof val !== "undefined" &&
      val.table &&
      val.table.category === "props" &&
      value !== val.defaultValue
    ) {
      componentArgs[key] = val;
    }
  }

  return templateSource
    .replace(
      'v-bind="args"',
      Object.keys(componentArgs)
        .map((key) => " " + propToSource(kebabCase(key), args[key]))
        .join(""),
    );
}

function propToSource(key, val) {
  const type = typeof val;

  switch (type) {
    case "boolean":
      return val ? key : "";
    case "string":
      return `${key}="${val}"`;
    default:
      return `:${key}="${val}"`;
  }
}

function kebabCase(str) {
  return str
    .split("")
    .map((letter, idx) => {
      return letter.toUpperCase() === letter
        ? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}`
        : letter;
    })
    .join("");
}

Any idea on how to implement this decorator? currently using Storybook 7.6.8 with vue3-vite and it's only showing story's configuration code:

{
  name: 'Default',
  args: {
    ...defaultArgs
  }
}
szymonsterczewski commented 7 months ago

@isorna did you find a way how to use it? Facing the same issue.

isorna commented 7 months ago

I had to modify it from the story's render:

StoryName = {
  args: ...
  render: (args) => {
    StoryName.parameters.docs.source.code = getCodeTemplate(args);
    components: { MyComponent },
    setup() {
      return { args };
    },
    template: getStoryTemplate(args),
  }
}
mikemonteith commented 7 months ago

Am I missing something here?

Using a story definition of:

export const Primary = {
  args: {
    primary: true,
    label: 'Button',
  },
  render: (args) => {
    return {
      components: { MyButton },
      setup() {
        return { args };
      },
      template: '<my-button v-bind="args" />',
    }
  },
};

results in:

image

I expected to see some nicely rendered vue3 code like this:

image
shilman commented 7 months ago

@mikemonteith your example is working as expected for me. do you have a reproduction you can share?

mikemonteith commented 7 months ago

Thanks @shilman . I have reproduced it here: https://github.com/mikemonteith/storybook-dynamic-render-testcase

shilman commented 7 months ago

Hi @mikemonteith ! I upgraded your project to Storybook 8 using npx storybook@next upgrade and it's working fine there. SB8 is scheduled for full release next week and contains TONS more quality of life improvements for Vue users (and everybody!) so my recommendation would be to just upgrade. (It should work in SB7 too, but 🤷 !!!)

Astarosa commented 7 months ago

I have the exact same issue as @mikemonteith using storybook 7.6.17.

The problem is that we need this specific syntax when dealing with slots. If you have to render some text as slot value for example, because you don't want to use a props label (for many reasons), you have no choice to use a template. In React no problem children is a props so you can acces it with the short args object syntax.

I would not be surprised that the same problem occurs with Svelte as well.

I will wait for the v8 release and let you know if the problem is solved on my side.