Open blowsie opened 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
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).
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.
I feel this feature is essential, and should be added to the newer Controls.
Really hope this will be added
A setter would be rad. Any idea if this is in the works?
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
This blogpost solved the issue for me: https://craigbaldwin.com/blog/updating-args-storybook-vue/
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;
😃 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,
];
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 })
}
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
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
.
Describe the bug
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;
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.
Code snippets If applicable, add code samples to help explain your problem.
System
Additional context Add any other context about the problem here.