Storybook: Change knobs via actions [idea]

Created on 9 Jul 2018  ·  55Comments  ·  Source: storybookjs/storybook

I've just started using storybook and so far I love it. One thing I've struggled with is how to deal with pure "stateless" components which require state to be contained in a parent. For example, I have a checkbox which takes a checked prop. Clicking the checkbox does not toggle its state, but rather fires an onChange, and waits to get an updated checked prop back. There doesn't seem to be any documentation on best practices to handle these kinds of components, and the suggestions in issues such as https://github.com/storybooks/storybook/issues/197 have been to create a wrapper component or to add another add-on. I would rather not make a component wrapper if I can help it, because I'd like to keep my stories as simple as possible.

One idea I had to handle this is to wire up the actions add-on with knobs, allowing knobs to be toggled programmatically via actions. As I said I'm brand-new at storybook, so I have no idea whether this is even feasible, but I wanted to at least raise the suggestion.

This is a stumbling point for me in implementing stories, and I imagine it might be for others as well. If creating a wrapper component really is the best thing to do, perhaps some documentation could be added to clarify this, and how to accomplish it?

knobs feature request todo

Most helpful comment

We're almost done with 5.3 (early Jan release) and looking into a knobs rewrite for 6.0 (late Mar release) that will solve a bunch of long-standing knobs issues. I'll see if we can work this in! Thanks for your patience!

All 55 comments

Some kind of similar idea was introduced in this #3701 PR, that was controversial (and was not merged).

We can open a discussion about it again, and listen to the API suggestions =).

Ah thank you, I hadn't seen that PR. Thanks @aherriot for putting that together, it seems we have the same thoughts.

Before diving into an API, it seems like the fundamental concept needs to be discussed and agreed on. One of the comments in the PR was this from @Hypnosphi:

I don't like the fact that it introduces multiple sources of truth (component callbacks and UI knobs)

It seems to me that the idea is not to introduce different sources of truth, but rather to allow maintaining a _single_ source of truth for component state—the knobs. All of the other approaches I've seen suggested (wrapper components, state add-ons, recompose) would in fact introduce another source of truth. In my checkbox example, I would not be able to have a knob for checked, and also allow a wrapper component to provide the checked prop. I see the knobs control panel as the component's parent, except that currently it cannot get any callbacks from the component, which is kind of one-sided and not how react apps are normally built.

By allowing knobs to be controlled programmatically, the story can be isolated to just the component being demonstrated, leaving the actual state management mechanism opaque, as it should be for a simple presentational component like a checkbox. The checkbox itself doesn't care how it gets its props, it might get connected to redux, its parent might use setState, maybe it uses withState from recompose, or maybe the props are controlled by storybook knobs.

Anyways like I said I'm very new to this, so I'm only sharing my intuitive thoughts. If I'm off-base and there is a generally-accepted "best practice" for dealing with these kinds of pure stateless components, maybe someone can point me to a good example that I can follow?

Hi :)

I just want to add I stumbled upon that as my components are receiving if they should display their mobile layouts (or not) from props, and my goal was to tie the viewport change to my knob, and it would have been really nice ;)

Just wanted to add my use case here, and as @IanVS even a callback would have been nice (so if I toggle my isMobile props, I could trigger a viewport change )

I have heard several people express interest in a method to update knob state. If we can agree on a good architecture, I think this will be of value to many people. Especially since this is extra functionality that people can choose to use or not and because it doesn't negatively affect those who don't want to use it.

@Hypnosphi, you had objections about the previous implementation, WDYT?

Commenting here to keep this issue open. I am still curious what @Hypnosphi has to say about @igor-dv previously proposed here.

It seems to me that the idea is not to introduce different sources of truth, but rather to allow maintaining a single source of truth for component state—the knobs. All of the other approaches I've seen suggested (wrapper components, state add-ons, recompose) would in fact introduce another source of truth. In my checkbox example, I would not be able to have a knob for checked, and also allow a wrapper component to provide the checked prop. I see the knobs control panel as the component's parent, except that currently it cannot get any callbacks from the component, which is kind of one-sided and not how react apps are normally built.

Sounds reasonable to me. Let's discuss the API

This will be a great feature. I just faced this same need here with a very basic controlled component and had faced it before in other components also. Thanks for looking at it.

To keep up with current syntax seems a little challenge. Maybe you could use something like:

const {value: name, change: setName} = text('Name', 'Kent');

@brunoreis, I would worry that changing the return signature of the knobs would be a breaking change. My off-the-cuff suggestion would be to do something like:

import {boolean, changeBoolean} from '@storybook/addon-knobs/react';

stories.add('custom checkbox', () => (
    <MyCheckbox
        checked={boolean('checked', false)}
        onChange={(isChecked) => changeBoolean('checked', isChecked)} />
));

Where the onChange callback might even allow currying, so that this would also work:

onChange={(isChecked) => changeBoolean('checked')(isChecked)}

// which of course simplifies down to
onChange={changeBoolean('checked')}

The important part is that the first argument must be the same as the label of the knob which should be changed. I believe this would allow users to opt-in to this behavior without changing anything about the way knobs are currently used. (Unless maybe labels are not currently required to be unique? I assume they are…)

@IanVS, that's nice. I agree with you about not changing the way things work. I could not find a way to do so because I did not think about using the label as a key. That might work. Let's see what @Hypnosphi has in mind.

changing the return signature of the knobs would be a breaking change

Technically, that's not a problem right now. We have a major release upcoming. But it indeed would be better to allow some backwards compatibility.

I like the idea of currying support.

Unless maybe labels are not currently required to be unique?

Yes they are, and actually they are unique across different types. So there should be no need in separate change<Type> exports, just change should be enough. That's basically what was made in https://github.com/storybooks/storybook/pull/3701
I think I'll just reopen that PR to let @aherriot finish it with some help from your side

We can have this:

const {value: name, change: setName} = text('Name', 'Kent');

without having to change the return type of text.

Javascript functions are objects and thus can have properties.
Object can be destructured.

screen shot 2018-09-19 at 00 08 35

@ndelangen the text function is indeed an object, but its return value isn't. This won't work in your example:

const { foo, bar } = x() // note the parens

We could have something like this though (the name is disputable):

const {value: name, change: setName} = text.mutable('Name', 'Kent');

why won't this work?

const { foo, bar } = x() // note the parens

The return of x is a function, which can have properties as well.

screen shot 2018-09-23 at 14 12 28

I was talking about x = () => {} from your original example.

If we make text return a function, users will need to change their code:

// Before
<Foo bar={text('Bar', '')}>

// After
<Foo bar={text('Bar', '')()}>
                         ^^ this

I see

What about @IanVS suggestion to this?

would be cool to have this feature.

what about "destruct", like with "react hooks" ?:

const [foo, setFoo] = useString('foo', 'default');

Came to say the same as @DimaRGB
Would be able to preserve existing syntax and add hook-like calls, for example:

.add('example', () => {
  const [selected, setSelected] = useBool(false);

  return <SomeComponent selected={selected} onSelected={setSelected} />
})

The occasional need being that react components aren't supposed to update their own props, but instead tell their parent that they need changed. Sometimes a component contains some element that is supposed to toggle it's props (I ran into this because sometimes a nav bar menu I have needs to open itself if a child element is clicked).

@IanVS I have the exact same situation where I have a toggle component that is only updated via parent props and once the user of a storybook story toggles it, the UI state is inconsistent with what's shown in the knobs panel. I did hack around it by using a private method from @storybook/addons-knobs - a simpler suggestion would be to provide an official API to do something like:

import { manager } from '@storybook/addons-knob/dist/registerKnobs.js'

const { knobStore } = manager
// The name given to your knob - i.e:  `select("Checked", options, defaultValue)`
knobStore.store['Checked'].value = newValue
// Danger zone! _mayCallChannel() triggers a re-render of the _whole_ knobs form.
manager._mayCallChannel()

@erickwilder I tried your approach but that never seemed to update the knob form (even when it did update the props provided to the component).

EDIT:

scratch that; I just didn't see that updates because I was using options() with checkboxes which are operated in an 'uncontrolled' manner apparently. By switching to multi-select and using this method in a callback I did get the updated values into the knob form:

// Ditch when https://github.com/storybooks/storybook/issues/3855 gets resolved properly.
function FixMeKnobUpdate(name, value) {
    addons.getChannel().emit(CHANGE, { name, value });
}

I might have omitted some details of my setup:

  • We're using Vue.js with Storybook
  • The code mutating the value and triggering the re-render was done inside a method if the story wrapping component.

I don't know if such approach would work for everyone.

I completely agree with @IanVS, I love storybook but I really miss the ability to update knobs via actions. In my case, I have two individual components A and B but in one of my stories I want to show a composition using both of them, so that whenever I change A I emit an action that would modify B and viceversa.

I tried @erickwilder's hack with my setup (Angular 7), but whenever I try to update the knob store I recieve the following error:

Error: Expected to not be in Angular Zone, but it is!

I've tried to somehow run this outside Angular but I couldn't manage to do it... Worst case scenario, I will create a third component C which will be a wrapper of A and B.

@davidolivefarga temporary solution from @erickwilder should work for any framework as it is not related to the framework/library (react, angular, vue, anything else) but to the rerendering of the Knobs addon.

Worked for us using React the same way as mentioned above

+1 to add a publicly visible method to trigger, containing knob name as a string to update and a new value, for example:

import { manager } from "@storybook/addon-knobs"

manager.updateKnob(
  propName, // knobs property, example from above "Checked"
  newValue, // new value to set programmatically, ex. true
)

I would really like for this to be officially supported.

In the meantime, with Storybook v5 I had to go with this terribly hacky solution:

window.__STORYBOOK_ADDONS.channel.emit('storybookjs/knobs/change', {
  name: 'The name of my knob',
  value: 'the_new_value'
})

🙈

🙈

Related: #6916

This would be cool... Especially because modals.

I think this would be useful.

Example:

storiesOf('input', module)
  .addDecorator(withKnobs)
  .add('default', () => <input type={text('type', 'text')} value={text('value', '')} disabled={boolean('disabled', false)} placeholder={text('placeholder', '')} onChange={action('onChange')} />)

This currently works, but seems natural that we want to wire the onChange event target value to the value set in props of the tested element. This would better demonstrate the application of the element.

This would be useful

+1 to @mathieuk/@raspo for pointing towards a solution here.

We can potentially get access to this in another way, too. Creating a few generic methods in a helpers module?

import addons from "@storybook/addons";

export const emitter = (type, options) => addons.getChannel().emit(type, options);

export const updateKnob = (name, value) => (
  emitter("storybookjs/knobs/change", { name, value })
);

And calling as needed in stories...

import { text } from "@storybook/addon-knobs";
import { updateKnob } from 'helpers';

// ...
const value = text("value", "Initial value");
<select
  value={value}
  onChange={({ target }) => updateKnob("value", target.value)}
>

...but still feels like some funky two-way binding workaround to something that could be built into the knobs API

Any update on the above? Would be a very useful feature 👍

This feature being implemented would let us do some really powerful cross-dependent knobs quite easily. Imagine making a pagination component story if you could have one knob dynamically updating depending on another, with something like this in your story:

const resultsCount = number(
    'Results count',
    currentPage, {
        max: 100,
        min: 0,
        range: true
    }
);
const resultsArray: React.ReactNode[] = new Array(resultsCount)
    .fill(true)
    .map((_, idx) => <div key={idx + 1}>Test Pagination Result #{idx + 1}</div>);
const childrenPerPage = 10;
const currentPage = number(
    'Current page index',
    0, {
        max: Math.ceil(resultsCount / childrenPerPage) - 1,
        min: 0,
        range: true
    }
);

I've tried doing essentially this, hoping that the max range on my currentPage knob would dynamically update when increasing the resultsCount knob at an increment of 10, however it seems the initial value of each knob gets cached at creation time and does not get used to override internal state values in subsequent renders. With the above code when I increase resultsCount by 10+ I would expect the max of currentPage to also increase by 1, however its value stays the same despite the underlying values being used to calculate it having changed (logging Math.ceil(resultsCount / childrenPerPage) - 1 shows the expected new value).

We're almost done with 5.3 (early Jan release) and looking into a knobs rewrite for 6.0 (late Mar release) that will solve a bunch of long-standing knobs issues. I'll see if we can work this in! Thanks for your patience!

@shilman I'm very excited about this feature 😄

Me, @atanasster & @PlayMa256 have been working on the foundation for this for a while. It will take a few more iterations to get right, but I feel very confident we can get is to 100% in 6.0.0 and really revolutionise data & reactivity for storybook.

Revolutionise? Hot damn diggity. Stop teasing me, my body is ready.

+1 for modals :)

Can't believe that it's impossible from the beginning. Waiting for updates ...

Hi folks!
As a temporary solution I've used in my app next code:

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

Still waiting this feature will be implemented natively.

I solved that problem using observable. I'm using storybook with angular

`
.add('updating chart data', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

@norbert-doofus thanks your example helped me 👍

Hi gang, We’ve just released addon-controls in 6.0-beta!

Controls are portable, auto-generated knobs that are intended to replace addon-knobs long term.

Please upgrade and try them out today. Thanks for your help and support getting this stable for release!

Thats great! I can't quite tell from reading the README (I might have missed it), can these new controls be changed programmatically, which was the request being made in this issue?

They can be, although I'm not sure that API is officially supported yet (tho we are doing exactly that for the controls in addon-docs). I'll work with @tmeasday to figure out the best way to get that in after the first round of Controls bugs stabilize.

  • [ ] add updateArgs to the story context?
  • [ ] make the story context available to a callback that's called from the story? (this.context?.updateArgs(....))

For anybody who is interested in Controls but don't know where to start, I've created a quick & dirty step-by-step walkthrough to go from a fresh CRA project to a working demo. Check it out:

=> Storybook Controls w/ CRA & TypeScript

There are also some "knobs to controls" migration docs in the Controls README:

=> How do I migrate from addon-knobs?

Hi folks!
As a temporary solution I've used in my app next code:

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

Still waiting this feature will be implemented natively.

Keep in mind that when using groupId you will need to add the group id to the name as follows:

 const show = boolean('Show Something', true, 'Group')

channel.emit(CHANGE, {
  name: 'Show Something_Group',
  value: prop_value,
});

Also, channel is an EventEmitter, so you can addListener to it to check what are the parameters being received.

channel.addListener(CHANGE, console.log)

Here's a code snippet for anybody who's interested in how to do this using addon-controls in v6.

import { useArgs } from '@storybook/client-api';

// Inside a story
export const Basic = ({ label, counter }) => {
    const [args, updateArgs] = useArgs();
    return <Button onClick={() => updateArgs({ counter: counter+1 })>{label}: {counter}<Button>;
}

I don't know if this is the best API for this purpose but it should work. Example in the monorepo:

https://github.com/storybookjs/storybook/blob/next/examples/official-storybook/stories/core/args.stories.js#L34-L43

Thanks, @shilman! That did the trick.

For folks interested, we happen to have the exact Checkbox story checked prop that started this whole thread. Here's our newly wired up story using Storybook 6.0.0-rc.9:

export const checkbox = (args) => {
  const [{ checked }, updateArgs] = useArgs();
  const toggleChecked = () => updateArgs({ checked: !checked });
  return <Checkbox {...args} onChange={toggleChecked} />;
};
checkbox.args = {
  checked: false,
  label: 'hello checkbox!',
};
checkbox.argTypes = {
  checked: { control: 'boolean' },
};

cb-arg

@shilman I attempted to utilize useArgs in a story for a controlled text input (a case where we might normally use the useState hook to update the value prop of the component via its onChange event). However, I encountered an issue where every time the user types a character, the component loses focus. Is this perhaps caused by it refreshing/re-rendering the story every time we update the args?

Is there a different recommended method for utilizing args/controls for a component with controlled text input?

This was with 6.0.0-rc.13

@jcq Can you create a new issue with a repro? This was not the primary use case for useArgs, but certainly one that we'd like to support, so we'd be happy to dig into it.

@shilman No prob — here is the new issue:
https://github.com/storybookjs/storybook/issues/11657

I also should have clarified that the errant behavior shows in Docs, while it works correctly in the normal Canvas mode.

I solved that problem using observable. I'm using storybook with angular

`
.add('updating chart data', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

This worked for me using Angular but changing to myData.value on line 5

I haven't tried this out yet (stuck on an older version of storybook for now) but it seems like this issue can be closed now. Thanks for the great work on args / controls!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dnlsandiego picture dnlsandiego  ·  3Comments

xogeny picture xogeny  ·  3Comments

levithomason picture levithomason  ·  3Comments

arunoda picture arunoda  ·  3Comments

purplecones picture purplecones  ·  3Comments