storybookjs / storybook

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

Storybook controls dont react to v-model changes #14259

Open blowsie opened 3 years ago

blowsie commented 3 years ago

Describe the bug

To Reproduce Create a component that uses v-model and have storybook provide the value

10019 also shows this

Expected behavior Controls to react to the input

Screenshots If applicable, add screenshots to help explain your problem. image

Code snippets If applicable, add code samples to help explain your problem.

System

λ npx sb@next info

Environment Info:

  System:
    OS: Windows 10 10.0.18363
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Binaries:
    Node: 10.16.3 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.4 - C:\Program Files\nodejs\yarn.CMD
    npm: 6.9.0 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 89.0.4389.90
    Edge: Spartan (44.18362.449.0)
  npmPackages:
    @storybook/addon-essentials: ^6.1.14 => 6.1.14
    @storybook/vue: ^6.1.14 => 6.1.14

Additional context Add any other context about the problem here.

blowsie commented 3 years ago

Looking at the source code, perhaps the control could be updated if the input events were passed to the knob as onChange events https://github.com/storybookjs/storybook/blob/master/addons/knobs/src/components/types/Text.tsx

ghengeveld commented 3 years ago

Essentially, you're asking for the Storybook arg to be automatically updated to reflect any mutation on the value (by the component). Most args are primitive values, so these can't actually be mutated, only replaced. Controls was never designed to be bi-directional or "reactive". If we go down that road, we'd have to implement it to work with any framework, not just Vue. Introducing a wrapper object (a "ref" in React nomenclature) to enable mutability on arg values could be a solution. Alternatively we could also pass a "setter" for each arg, so that one can explicitly hook that up to their component (in the story template).

saiichihashimoto commented 3 years ago

I would love the "setter" solution, which would be a lot more generic than using something like a ref (or whatever fits with each framework) and, if desired, there could be wrappers that work with each framework.

dudedigital commented 3 years ago

I feel this feature is essential, and should be added to the newer Controls.

TrayHard commented 3 years ago

Really hope this will be added

jshimkoski commented 2 years ago

A setter would be rad. Any idea if this is in the works?

shilman commented 2 years ago

No it's not. If somebody wants to create an addon to do this, it might be pretty easy to do and I'd be happy to guide. We probably won't build this into Storybook by default, but if somebody makes an addon and it's simple enough, I'd consider adding it as part of @storybook/vue

Juice10 commented 2 years ago

This blogpost solved the issue for me: https://craigbaldwin.com/blog/updating-args-storybook-vue/

dondevi commented 1 year ago

This blogpost solved the issue for me: https://craigbaldwin.com/blog/updating-args-storybook-vue/

It work for me. .storybook/preview.js:

import type { Preview } from '@storybook/vue3';
import { useArgs } from '@storybook/preview-api';

const preview: Preview = {
  decorators: [
    /**
     * Support `v-model` for vue
     * @see {@link https://craigbaldwin.com/blog/updating-args-storybook-vue/}
     */
    (story, context) => {
      const [args, updateArgs] = useArgs();
      if ('modelValue' in args) {
        const update = args['onUpdate:model-value'] || args['onUpdate:modelValue'];
        args['onUpdate:model-value'] = undefined;
        args['onUpdate:modelValue'] = (...vals) => {
          update?.(...vals);
          /**
           * Arg with `undefined` will be deleted by `deleteUndefined()`, then loss of reactive
           * @see {@link https://github.com/storybookjs/storybook/blob/next/code/lib/preview-api/src/modules/store/ArgsStore.ts#L63}
           */
          const modelValue = vals[0] === undefined ? null : vals[0];
          updateArgs({ modelValue });
        };
      }
      return story({ ...context, updateArgs });
    }
  ],
};

export default preview;
dondevi commented 1 year ago

😃 A better solution .storybook/preview.ts:

import { SetupContext, watch } from 'vue';
import { Preview } from '@storybook/vue3';
import { useArgs } from '@storybook/preview-api';

type Decorator = Required<Preview>['decorators'][number];

/**
 * Insensibly realize two-way binding of vue3's `v-model` and storybook7's `args`
 */
const patchModel: Decorator = (story, context) => {
  const [args, updateArgs] = useArgs();
  const { component } = context as any;

  if (component.setup.__id === window.location.href) {
    return story(context);
  }

  const proxy = (props: Record<string, any>, ctx: SetupContext) => {
    Object.keys(props).forEach((key: string) => {
      const [event, model] = key.split(':');
      const updateModel = props[key];
      if (event !== 'onUpdate' || typeof updateModel !== 'function') {
        return;
      }
      /**
       * @fix Arg with `undefined` will be deleted by `deleteUndefined()`, then loss of reactive
       * @see {@link https://github.com/storybookjs/storybook/blob/next/code/lib/preview-api/src/modules/store/ArgsStore.ts#L63}
       */
      watch(() => props[model], (val = null) => {
        updateArgs({ [model]: val });
      });
      watch(() => args[model], val => {
        updateModel(val);
      });
    });
    return proxy.__origin(props, ctx);
  };

  if (!proxy.__origin) {
    proxy.__origin = component.setup;
  }
  proxy.__id = window.location.href;
  component.setup = proxy;

  return story(context);
};

export const decorators: Preview['decorators'] = [
  patchModel,
];
Xenossolitarius commented 10 months ago

Or this naive solution

import { Preview } from '@storybook/vue3'
import { useArgs } from '@storybook/preview-api'

type Decorator = Required<Preview>['decorators'][number]

/**
 * Insensibly realize two-way binding of vue3's `v-model` and storybook7's `args`
 * Not interested in maintaining this, when it stops working we can just delete it
 */
export const patchVModel: Decorator = (story, context) => {
  const [args, updateArgs] = useArgs()
  const { component } = context as any

  component?.emits?.forEach((emit: string) => {
    const [, model] = emit.split(':')
    if(!model) return
    const update = args[emit]
    args[emit.replace('update', 'onUpdate')] = (...vals) => {
      update?.(...vals)
      /**
       * Arg with `undefined` will be deleted by `deleteUndefined()`, then loss of reactive
       * @see {@link https://github.com/storybookjs/storybook/blob/next/code/lib/preview-api/src/modules/store/ArgsStore.ts#L63}
       */
      const modelValue = vals[0] === undefined ? null : vals[0]
      updateArgs({ [model]: modelValue })
    }
  })

  return story({ ...context, updateArgs })
}
TNGD-YQ commented 9 months ago

Or this naive solution

import { Preview } from '@storybook/vue3'
import { useArgs } from '@storybook/preview-api'

type Decorator = Required<Preview>['decorators'][number]

/**
 * Insensibly realize two-way binding of vue3's `v-model` and storybook7's `args`
 * Not interested in maintaining this, when it stops working we can just delete it
 */
export const patchVModel: Decorator = (story, context) => {
  const [args, updateArgs] = useArgs()
  const { component } = context as any

  component?.emits?.forEach((emit: string) => {
    const [, model] = emit.split(':')
    if(!model) return
    const update = args[emit]
    args[emit.replace('update', 'onUpdate')] = (...vals) => {
      update?.(...vals)
      /**
       * Arg with `undefined` will be deleted by `deleteUndefined()`, then loss of reactive
       * @see {@link https://github.com/storybookjs/storybook/blob/next/code/lib/preview-api/src/modules/store/ArgsStore.ts#L63}
       */
      const modelValue = vals[0] === undefined ? null : vals[0]
      updateArgs({ [model]: modelValue })
    }
  })

  return story({ ...context, updateArgs })
}

please be aware this solution will break the source code generation

Nathan7139 commented 4 months ago

Is there a solution for Storybook 8? When attempting to use useArgs() within a Decorator, an error is thrown: "Invalid hook call. Hooks can only be called inside the body of a function component." This issue may arise due to the usage of useArgs() imported from @storybook/manager-api.

It also works with Storybook 8. However, it's advised not to use useArgs from @storybook/manager-api. Instead, you should use useArgs from @storybook/preview-api.