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

Storybook controls dont react to v-model changes #14259

Open
blowsie opened this issue Mar 17, 2021 · 13 comments
Open

Storybook controls dont react to v-model changes #14259

blowsie opened this issue Mar 17, 2021 · 13 comments

Comments

@blowsie
Copy link

blowsie commented Mar 17, 2021

Describe the bug

  • Changing state in the controls updates the component as expected
  • Changing state in the component does not update the control state as expected

In vue you should not mutate props, and they propose you should implent a handler like so for v-model;

  setup(props, { emit }) {
   const model = computed({
     get() {
       return props.value
     },
     set(value) {
       emit('input', value)
     },
   })
   return {
     model,
   }
 },

I've done this but storybook doesn't react to the input event

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
Copy link
Author

blowsie commented Mar 17, 2021

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
Copy link
Member

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
Copy link
Contributor

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
Copy link

dudedigital commented Mar 31, 2021

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

@TrayHard
Copy link

Really hope this will be added

@jshimkoski
Copy link

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

@shilman
Copy link
Member

shilman commented Oct 28, 2021

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
Copy link

Juice10 commented May 23, 2022

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

@dondevi
Copy link

dondevi commented Apr 27, 2023

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
Copy link

dondevi commented Jun 21, 2023

😃 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
Copy link

Xenossolitarius commented Nov 13, 2023

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
Copy link

TNGD-YQ commented Dec 2, 2023

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
Copy link

Nathan7139 commented Apr 26, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests