storybook
storybook copied to clipboard
Button control type
With the Knobs addon I could include a button and associated handler... is this possible with the Controls addon?
import { button } from '@storybook/addon-knobs';
const label = 'Do Something';
const handler = () => doSomething('foobar');
const groupId = 'GROUP-ID1';
button(label, handler, groupId);
Not currently possible, adding it as a feature request
Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!
I'll throw my two cents in that buttons are critical for us—we'll have to keep using knobs unless buttons are added to controls as well.
@lewispeel, @kendagriff and anyone else who has a need for this feature:
Can you describe the ways in which you intend to use this "button" control? How are you currently using Knobs? Can you give specific examples of real use cases? What problem does it solve for you?
Even though there's a PR open that adds a button control, we've decided to put that on hold and go back to the drawing board. Right now there's two use cases we've identified that would be addressed with this button control, but they're distinct enough to be separate features altogether, with their own purpose-built UI and API. Are there more use cases we haven't considered?
Read more here: https://github.com/storybookjs/storybook/pull/14330#issuecomment-857532439
In our case we're using a button to save the current state of the component to localStorage
.
But speaking generally, I think it's a straightforward need: imagine needing a button that increments a counter which is in turn used as an input to the component you're testing in Storybook. I can't think of any other reasonable control accept to kind of fake it with a number or range input.
@ghengeveld another use case from Discord #angular
channel
allExpand: button("Expand all", () => {
openState.item1 = true;
openState.item2 = true;
}),
allCollapse: button("Collapse all", () => {
openState.item1 = false;
openState.item2 = false;
}),
@kendagriff can you explain what you use that localStorage
data for afterwards? Does the component retrieve that data from localStorage? Storybook itself is moving away from localStorage, instead using the URL to track state (args and globals, specifically). Only Storybook UI state (panel size and such) is still in localStorage.
The localStorage
example is an "interaction" or "side-effect", technically unrelated to args (although it uses args as input, it doesn't directly affect any Storybook state).
The "counter" example that mutates args is a different use case, which we currently think of as "macros".
The "expand/collapse all" example is also a macro, because it similarly mutates args.
The tricky part is going to be creating a UI that makes sense for these various use cases. Does it really have to coexist with Controls, or can it be a separate addon (and thus, tab in the addons panel)?
@ghengeveld For me, I'd like a button control so I can call methods on my components. We would use it to simulate focus/unfocus states, show/hide animations or something else that can't be done by passing a value to a property. My components aren't built with React so not everything can be done with passing props.
I have similar usecase as @lewispeel - I have canvas application exposes API which need to be triggered somehow. Knobs does that nicely.
It could be argued that this situation is more complicated than what a story should be doing anyway, but the one knob I had trouble converting to controls was a knob that updates the route. I am considering trying an alternate implementation of Angular's router that doesn't actually update the page's location for stories, because if Storybook or an addon starts using the # part of the url I would have to find another solution anyway. For now though, I am using the following solution.
I can't decide if the route makes sense as an arg or should be handled by another addon. I think of the url as the component's input, but the component is observing the router instead of a component input.
With knobs, I had a few buttons that would set the route to different locations. The component wasn't re-rendering, so it would still trigger any router changing features, such as animations or data resolvers. I could see it being argued that those things only need to be documented, since they aren't necessarily features of the component itself, but I wanted a fully working router example that could be played with to see how the component reacts to different router events.
My solution was to use a select
control where each option is one of the routes that used be a button knob. The select
control has an onChange
callback, but it happens in the manager, so I send a message containing the selected option back to the preview with a channel. Using the onChange
and sending my own message avoids the storyArgsUpdated
event, but still lets me run code in the preview.
My story:
export const Example: Story = (args) => ({
moduleMetadata: {
declarations: [
StoryEmptyComponent
],
providers: [
StoryUsersDataService,
StoryUserIdToNameResolver
],
imports: [
BrowserAnimationsModule,
BrowserModule,
RouterModule.forRoot([
{
path: 'users',
component: StoryEmptyComponent,
data: {
breadcrumb: 'Users'
},
children: [
{
path: ':userId',
component: StoryEmptyComponent,
data: { },
resolve: {
breadcrumb: StoryUserIdToNameResolver
}
}
]
}
], { useHash: true }),
// NOTE: This could probably be replaced with Storybook's new `setup` feature.
StoryInitialRouteModule.forRoot('/users/123')
]
},
props: args,
template: `
<seam-breadcrumbs></seam-breadcrumbs>
<router-outlet></router-outlet>
`
})
Example.argTypes = {
routes: routesArgType([
'/users',
'/users/123',
'/users/987',
'/users/999'
])
}
The function building the argType to replace my routing button knobs:
import { ArgType } from '@storybook/addons'
declare const __STORYBOOK_ADDONS: any
function goToHashUrl(url: string): void { location.hash = `#${url}` }
// I could directly set the iframe, but I didn't want to assume that iframe will always be there and let me change the location.
// function goToHashUrl(url: string): void { (document.querySelector('#storybook-preview-iframe') as any).contentWindow.location.hash = `#${url}` }
__STORYBOOK_ADDONS.getChannel().on('custom/go-to-hash', (data: { hash: string }) => {
goToHashUrl(data.hash)
})
export function routesArgType(routes: string[]): ArgType {
return {
options: routes,
control: {
type: 'select',
// Runs in the 'manager', so I am emitting to a channel in the 'preview'.
onChange: (url: string) => { __STORYBOOK_ADDONS.getChannel().emit('custom/go-to-hash', { hash: url }) }
}
}
}
In my case I have some components that expose methods with useImperativeHandle hook, like "animateIn()" and "animateOut()". Button controls would be very handy to test these methods.
Another use case from discussion https://github.com/storybookjs/storybook/discussions/15779
I'm using button knobs to push items to an array that is used as component input:
export const defaultElements: string[] = ["test1", "test2", "test3"];
const addElementAction = () => {
defaultElements.push("test");
};
export const Primary = () => ({
component: ListComponent,
props: {
elements: defaultElements, // elements is an input to ListComponent
addElement: button('Add Element', addElementAction),
},
});
There doesn't seem to be a matching replacement for this using addon-controls
I also have the same use case as @dpeger - I have some components that take an array of data as a prop, and need simple controls to add data, remove data, or reset the array. Without the ability to control the mock data like this, I would be unable to use Storybook as my dev environment for these components. This was easy to build with Knobs, but appears impossible with Controls.
Likewise as with others above. For me the problem boils down to this:
When building components that have rich animations, the component's interface is not just defined by props - instead it's defined by props + some methods, where the methods trigger animations.
For example, imagine a Torch
component, that you want to flash for a split second.
Yes you could pass in a prop isFlashing
that you quickly set from false to true to false but that's not only clunky it's the wrong abstraction really.
If the component has a method, e.g. torch.flash()
this is done very easily.
Given that React functional components have no methods they've provided useImperativeHandle
https://reactjs.org/docs/hooks-reference.html#useimperativehandle as an equivalent, but certainly with WebComponents, React class components, and other libraries compatible with Storybook you can call methods on components.
So back to controls... how would these component methods be callable using controls? (e.g. a button that calls torch.flash()
)
My 2 cents. We use buttons in Knobs for a wide array of things. From triggering animations, clearing localstorage and resetting components, to pulling data from different environments. Not sure how to go about doing this with Controls...
This is essentially why we need button controls. Not everyone is on modern frameworks. https://dev.to/joelstransky/render-callbacks-in-storybook-5c0c
I did it like this:
postinstall.js:
const path = require('path');
const fs = require('fs');
addCustomControls();
function addCustomControls() {
const file = path.resolve('node_modules', '@storybook', 'components', 'dist', 'esm', 'index-681e4b07.js');
replaceInFile(
file,
'var Control=Controls[control.type]||NoControl',
'var Control=(Object.assign({}, Controls, (window["STORYBOOK_CUSTOM_CONTROLS"] || {})))[control.type]||NoControl',
);
}
function replaceInFile(file, search, value) {
let content = fs.readFileSync(file, 'utf-8');
content = content.replace(search, value);
fs.writeFileSync(file, content, 'utf-8');
}
manager.js:
const React = require('react');
const { Form } = require('@storybook/components');
window["STORYBOOK_CUSTOM_CONTROLS"] = {
'button': function ({ argType, value, onChange }) {
return React.createElement(Form.Button, {
onClick: function(){
value = typeof value === 'number' ? value : 0;
onChange(value + 1);
},
}, argType.name);
},
}
in stories:
export default {
// ...
argTypes: {
Prev: {
name: 'Prev Slide',
control: 'button',
}
}
// ...
}
export const Slider = ({ Prev }) => {
// ...
const initPrev = useRef(Prev);
useEffect(() => {
if (initPrev.current !== Prev) {
model.PrevSlide();
}
}, [Prev]);
// ...
}
Our use-case: a bunch of our components have internal state and will react on external events. A button is the most suitable of the bunch to make this work.
Vovencia exposes a cool little thing though. Why not expose the Controls
dictionary and allow users to add or overwrite control types? Then I think anyone could create another addon to allow for button types from within the controls window. This might also improve controls that accept objects, so we can build a tiny UI to set relevant properties which non-technical users can use.