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

Vue3: Fix CSF2 support with decorators #20995

Merged
merged 18 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions code/lib/store/template/stories/rendering.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { global as globalThis } from '@storybook/global';
import type { PlayFunctionContext } from '@storybook/types';
import { within, waitFor } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { FORCE_REMOUNT, RESET_STORY_ARGS, UPDATE_STORY_ARGS } from '@storybook/core-events';
import {
FORCE_REMOUNT,
RESET_STORY_ARGS,
STORY_ARGS_UPDATED,
UPDATE_STORY_ARGS,
} from '@storybook/core-events';

export default {
component: globalThis.Components.Button,
Expand Down Expand Up @@ -40,21 +45,25 @@ export const ForceRemount = {
export const ChangeArgs = {
play: async ({ canvasElement, id }: PlayFunctionContext<any>) => {
const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__;

await channel.emit(RESET_STORY_ARGS, { storyId: id });
await new Promise((resolve) => {
chakAs3 marked this conversation as resolved.
Show resolved Hide resolved
channel.once(STORY_ARGS_UPDATED, resolve);
});

const button = await within(canvasElement).findByRole('button');
await button.focus();
await expect(button).toHaveFocus();

// Web-components: https://github.com/storybookjs/storybook/issues/19415
// Preact: https://github.com/storybookjs/storybook/issues/19504

if (['web-components', 'html', 'preact'].includes(globalThis.storybookRenderer)) return;

// When we change the args to the button, it should not remount
await channel.emit(UPDATE_STORY_ARGS, { storyId: id, updatedArgs: { label: 'New Text' } });
await within(canvasElement).findByText(/New Text/);
await expect(button).toHaveFocus();

await channel.emit(RESET_STORY_ARGS, { storyId: id });
chakAs3 marked this conversation as resolved.
Show resolved Hide resolved
await within(canvasElement).findByText(/Click me/);
await within(canvasElement).findByText(/New Text/);
await expect(button).toHaveFocus();
},
};
14 changes: 5 additions & 9 deletions code/renderers/vue3/src/decorateStory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ConcreteComponent, Component, ComponentOptions } from 'vue';
import { h, isReactive, reactive } from 'vue';
import { h } from 'vue';
import type { DecoratorFunction, StoryContext, LegacyStoryFn } from '@storybook/types';
import { sanitizeStoryContextUpdate } from '@storybook/preview-api';

Expand Down Expand Up @@ -47,16 +47,11 @@ export function decorateStory(
return decorators.reduce(
(decorated: LegacyStoryFn<VueRenderer>, decorator) => (context: StoryContext<VueRenderer>) => {
let story: VueRenderer['storyResult'] | undefined;
if (!isReactive(context.args)) context.args = reactive(context.args);
const decoratedStory: VueRenderer['storyResult'] = decorator((update) => {
const updatedArgs =
update?.args && !isReactive(update.args)
? { ...update, args: reactive(update.args) }
: update;

const decoratedStory: VueRenderer['storyResult'] = decorator((update) => {
story = decorated({
...context,
...sanitizeStoryContextUpdate(updatedArgs),
...sanitizeStoryContextUpdate(update),
});
return story;
}, context);
Expand All @@ -68,7 +63,8 @@ export function decorateStory(
if (decoratedStory === story) {
return story;
}
return prepare(decoratedStory, story) as VueRenderer['storyResult'];

return prepare(decoratedStory, h(story, context.args)) as VueRenderer['storyResult'];
},
(context) => prepare(storyFn(context)) as LegacyStoryFn<VueRenderer>
);
Expand Down
58 changes: 15 additions & 43 deletions code/renderers/vue3/src/render.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/* eslint-disable no-param-reassign */
import { dedent } from 'ts-dedent';
import { createApp, h, reactive, toRefs } from 'vue';
import { createApp, h, reactive } from 'vue';
import type { RenderContext, ArgsStoryFn } from '@storybook/types';

import type { Args, StoryContext } from '@storybook/csf';
import type { StoryFnVueReturnType, VueRenderer } from './types';

Expand All @@ -27,50 +25,36 @@ const map = new Map<
{ vueApp: ReturnType<typeof createApp>; reactiveArgs: any }
>();

const elementMap = new Map<VueRenderer['canvasElement'], StoryFnVueReturnType>();

export function renderToCanvas(
{
storyFn,
forceRemount,
showMain,
showError,
showException,
name,
title,
storyContext,
}: RenderContext<VueRenderer>,
{ storyFn, forceRemount, showMain, showException, storyContext }: RenderContext<VueRenderer>,
canvasElement: VueRenderer['canvasElement']
) {
let { reactiveArgs } = useReactive(storyContext);
// fetch the story with the updated context (with reactive args)
const element: StoryFnVueReturnType = storyFn(storyContext);

if (!element) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing error handling here?

Copy link
Contributor Author

@chakAs3 chakAs3 Feb 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, i removed it because it will never happen as we don't need to define the template any more, especially in CSF3, but I don't know if CSF2 requires this... Please guide me on this, anyway will put it back for now

Copy link
Contributor Author

@chakAs3 chakAs3 Feb 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

so if can see this will just work even if the story template has no component

export const WithRenderTemplate: Story = {
  args: {
    label: 'Button',
    size: 'small',
  },
  render(args: any){
    return ({
    components: { Button },
    setup() {
      return { args };
    },
    template: '<b> No Button Component  <pre>{{ args }}</pre></b>' ,
    })
  },
};

Copy link
Contributor Author

@chakAs3 chakAs3 Feb 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shilman I have done more tests and even with CSF2 no error rises, However, Vue handles the case when there is no template defined,it shows a Warning message on the console.
let me know if still want me to keep it.

showError({
title: `Expecting a Vue component from the story: "${name}" of "${title}".`,
description: dedent`
Did you forget to return the Vue component from the story?
Use "() => ({ template: '<my-comp></my-comp>' })" or "() => ({ components: MyComp, template: '<my-comp></my-comp>' })" when defining the story.
`,
});
return () => {};
}
// getting the props from the render function
storyContext.args = reactive(storyContext.args);
const element: StoryFnVueReturnType = storyFn();
elementMap.set(canvasElement, element);

const props = (element as any).render?.().props;
if (props) reactiveArgs = reactive(props);
const reactiveArgs = props ? reactive(props) : storyContext.args;

const existingApp = map.get(canvasElement);

if (existingApp && !forceRemount) {
updateArgs(existingApp.reactiveArgs, reactiveArgs);
updateArgs(existingApp.reactiveArgs, storyContext.args);
return () => {
teardown(existingApp.vueApp, canvasElement);
};
}

if (existingApp && forceRemount) teardown(existingApp.vueApp, canvasElement);

const storybookApp = createApp({
render() {
const renderedElement: any = elementMap.get(canvasElement);
const current = renderedElement && renderedElement.template ? renderedElement : element;
map.set(canvasElement, { vueApp: storybookApp, reactiveArgs });
return h(element, reactiveArgs);
return h(current, reactiveArgs);
},
});

Expand Down Expand Up @@ -120,15 +104,3 @@ function teardown(
storybookApp?.unmount();
if (map.has(canvasElement)) map.delete(canvasElement);
}

/**
* create a reactive args and return it and the refs to avoid losing reactivity when passing it to the story
* @param storyContext
* @returns
*/

function useReactive(storyContext: StoryContext<VueRenderer, Args>) {
const reactiveArgs = reactive(storyContext.args || {});
storyContext.args = toRefs(reactiveArgs);
return { reactiveArgs, refsArgs: storyContext.args };
}