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!

Walnussbaer commented 6 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

Same problem here. Our stories use a simliar format to be able to use the Auto Events Addon for Storybook (https://storybook.js.org/addons/storybook-auto-events)

Our story looks like this:

const storyMeta = {
  component: MyComponent,
  title: "common/comment/components/MyComponent",
  parameters: {
    docs: {
      description: {
        component: "Foo"
      },
    },
  },
  render: (args, { events }) => ({
    // events --> https://storybook.js.org/addons/storybook-auto-events
    components: { MyComponent},
    setup() {
      return { args, events };
    },
    template: `
      <my-component v-bind="args" v-on="events"></my-component>
    `,
  }),
} satisfies Meta<typeof MyCComponent>;

When trying to display the source code, we get only the args definition and not the HTML code.

grafik

Using Storybook 7.6.17. I hope an upgrade to Storybook 8 helps us.

itsJohnnyGrid commented 6 months ago

Here is a working example for storybook 8

(we use this set for our UI library, and all works pretty well).

preview.js

import { vue3SourceDecorator } from "./decorators/vue3SourceDecorator";

export default {
  decorators: [vue3SourceDecorator],
  //... (other config)
};

vue3SourceDecorator.js

import { addons, makeDecorator } from "@storybook/preview-api";
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", {
                id: context.id,
                args: context.args,
                source: formattedCode,
              });
            };

            emitFormattedTemplate();
          } 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;
    }
  }

  const slotTemplateCode =
    // eslint-disable-next-line vue/max-len
    '<template v-for="(slot, index) of slots" :key="index" v-slot:[slot]><template v-if="args[slot]">{{ args[slot] }}</template></template>';
  const templateDefaultRegEx = /<template #default>([\s\S]*?)<\/template>/g;

  return templateSource
    .replace(/>[\s]+</g, "><")
    .trim()
    .replace(slotTemplateCode, "")
    .replace(templateDefaultRegEx, "$1")
    .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("");
}

Example of the story: button.stories.js

import { getArgTypes, getSlotNames } from "vueless/service.storybook";

import UButton from "vueless/ui.button";
import UIcon from "vueless/ui.image-icon";
import URow from "vueless/ui.container-row";
import UGroup from "vueless/ui.container-group";

export default {
  id: "1010",
  title: "Buttons & Links / Button",
  component: UButton,
  args: {
    text: "Button",
  },
  argTypes: {
    ...getArgTypes(UButton.name),
  },
};

const DefaultTemplate = (args) => ({
  components: { UButton },
  setup() {
    const slots = getSlotNames(UButton.name);

    return { args, slots };
  },
  template: `
    <UButton v-bind="args">
      <template v-for="(slot, index) of slots" :key="index" v-slot:[slot]>
        <template v-if="args[slot]">{{ args[slot] }}</template>
      </template>
    </UButton>
  `,
});

const SlotTemplate = (args) => ({
  components: { UButton, UIcon },
  setup() {
    return { args };
  },
  template: `
    <UButton v-bind="args">
      ${args.slotTemplate}
    </UButton>
  `,
});

const VariantsTemplate = (args, { argTypes } = {}) => ({
  components: { UButton, URow },
  setup() {
    return {
      args,
      variants: argTypes.variant.options,
    };
  },
  template: `
    <URow>
      <UButton
        v-for="(variant, index) in variants"
        v-bind="args"
        :variant="variant"
        :text="variant"
        :key="index"
      />
    </URow>
  `,
});

export const Default = DefaultTemplate.bind({});
Default.args = {};

export const variants = VariantsTemplate.bind({});
variants.args = {};

export const slotDefault = SlotTemplate.bind({});
slotDefault.args = {
  slotTemplate: `
    <template #default>
      🤘🤘🤘
    </template>
  `,
};