How to dynamically mutate "args" in Storybook v6 from the component's action?
Asked Answered
P

3

32

Let's see we have the simple component ToggleButton:

const ButtonComponent = Vue.component('ButtonComponent', {
  props: {
    value: Boolean
  },

  methods: {
    handleClick() {
      this.$emit('toggle');
    }
  },

  template: `
    <button 
      :class="value ? 'on' : 'off'"
      @click="handleClick"
    >
      Toggle
    </button>`
});

And the story for that component:

import ToggleButton from './ToggleButton.vue';

export default {
  title: 'ToggleButton',
  component: ToggleButton,

  argTypes: {
    onToggle: {
      action: 'toggle' // <-- instead of logging "toggle" I'd like to mutate `args.value` here
    }
  }
};

export const Default = (_args, { argTypes }) => ({
  components: { ToggleButton },
  props: Object.keys(argTypes),

  template: `
    <ToggleButton
      :value="value"
      :toggle="onToggle"
    />
  `
});

Default.args = {
  value: false
}

What I want to achieve is to handle toggle action inside the story and change value that I've used in Default.args object to change the button style by changing the class name from .off to .on.

Papilla answered 2/9, 2020 at 14:57 Comment(4)
did you get this figured out by any chance?Stulin
@Stulin unfortunately no. I have created a wrapper component just inside the story for a similar thing (control props inside the wrapper), but it's not exactly the same that I'm looking for.Papilla
@Papilla Could you share what that component wrapper ended up looking like? I am trying to figure this out myselfEndophyte
@MaximFedotov I just created a new Vue component inside ButtonComponent.stories.ts, let's say const ControlComponent = Vue.component('ControlComponent', { components: { ButtonComponent } }); with dynamic properties inside that's being mutated by ButtonComponent. And then have used it instead of ButtonComponent.Papilla
R
34

I had the same exact issue, and kept looking for days, till I stumbled upon this github post: https://github.com/storybookjs/storybook/issues/12006

Currently in my React (am sure vue approach will be similar), I do following:

import React from 'react';
import CheckboxGroupElement from '../CheckboxGroup';
import { STORYBOOK_CATEGORIES } from 'elements/storybook.categories';
import { useArgs } from '@storybook/preview-api';

export default {
  component: CheckboxGroupElement,
  title: 'Components/CheckboxGroup',
  argTypes: {
    onChange: {
      control: 'func',
      table: {
        category: STORYBOOK_CATEGORIES.EVENTS,
      },
    },
  },
  parameters: { actions: { argTypesRegex: '^on.*' } },
};

const Template = (args) => {
  const [_, updateArgs] = useArgs();

  const handle = (e, f) => {
// inside this function I am updating arguments, but you can call it anywhere according to your demand, the key solution here is using `useArgs()`
// As you see I am updating list of options with new state here
    console.log(e, f);
    updateArgs({ ...args, options: e });
  };
  return <CheckboxGroupElement {...args} onChange={handle} />;
};

export const CheckboxGroup = Template.bind({});
CheckboxGroup.storyName = 'CheckboxGroup';
CheckboxGroup.args = {
//Here you define default args for your story (initial ones)
  controller: { label: 'Group controller' },
  options: [
    { label: 'option 1', checked: true },
    { label: 'option 2', checked: false },
    { label: 'option 3', checked: false },
  ],
  mode: 'nested',
};
Raffish answered 6/5, 2021 at 19:32 Comment(6)
This is often the way it goes with Storybook, isn’t it? You’re looking for a feature that is never mentioned in the docs only to find it either deep in their source code or in a GitHub issue.Gemology
This works great, but the args state seems to be out of sync with controls state.Knopp
@T.Gaud what exactly your issue looks like?Raffish
@Raffish if I change the state of my component with useArgs when an onClick is fired, the controls addon (which is included in storybook-essentials) doesn't reflect the updated state.Knopp
@T.Gaud are you talking about the actions panel in canvas tab?Raffish
@Raffish well yeah, but the controls tab. e.g. storybook.js.org/addons/@storybook/addon-controlsKnopp
B
0

I ended up making my own wrapper to useArgs, useStorybookState, which allows easier creation of Dispatch<SetStateEvent<T>> type functions inside of render functions:

// .storybook/useStorybookState.tsx
import { useArgs } from "@storybook/preview-api";

export const useStorybookState = <T extends object>() => {
    const [currentArgs, updateArgs] = useArgs<T>();

    const setArg = <K extends keyof T>(key: K, value: T[K] | ((prev: T[K]) => T[K])) => {
        const newArgs = (prevArgs: T) => {
            const updatedValue = typeof value === "function" ? (value as (prev: T[K]) => T[K])(prevArgs[key]) : value;
            return { ...prevArgs, [key]: updatedValue };
        };
        updateArgs(newArgs(currentArgs) as Partial<T>);
    };

    return [currentArgs, setArg] as const;
};

This can then be used in a render function as so:

render: function Render(args) {
    const [{ myVal, otherVal }, setArg] = useStorybookState<{
        myVal: string,
        otherVal: number
    }>({
        myVal: "This is my starting value"
    });
    const setMyVal = (value: SetStateAction<string>) => setArg("myVal", value);
    const setOtherVal = (value: SetStateAction<number>) =>  setArg("otherVal", value);
    ...
}
Blistery answered 23/5, 2024 at 16:41 Comment(0)
U
-1

Cannot find module client-api?

If you're like me you have tried the suggestion in ProllyGeek answer's you probably got the following error:

Cannot find module '@storybook/client-api' or its corresponding type declarations.

In Storybook v7, useArgs should now be imported from '@storybook/preview-api'

Example using react, but it's pretty much the same in vue

import { Decorator, Meta, StoryObj } from '@storybook/react'
import { useArgs } from '@storybook/preview-api'

import {
  Component,
  ComponentProps,
} from './Component'

const OnChangeSyncArgs: Decorator<ComponentProps> = (Story, context) => {
  const [, setArgs] = useArgs()

  return Story({
    ...context,
    args: {
      ...context.allArgs,
      onChange: (value) => setArgs({...context.args, value}),
    },
  })
}

export default {
  component: Component,
  decorators: [OnChangeSyncArgs],
} satisfies Meta<ComponentProps>


export const Overview: StoryObj<ComponentProps> = {
  //
}

My original answer stated that useArgs should be imported using '@storybook/addons' but cjl750 made me aware of my mistake. Thanks!

Unroof answered 10/8, 2023 at 13:36 Comment(1)
I ended up importing from @storybook/preview-api for use in a decorator with v7. Import from @storybook/addons is what you'd need if authoring your own add-on.Pagan

© 2022 - 2025 — McMap. All rights reserved.