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

Addon-Controls: Next-generation knobs #10834

Merged
merged 19 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions addons/controls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<center>
<img src="./docs/addon-controls-hero.gif" width="100%" />

shilman marked this conversation as resolved.
Show resolved Hide resolved
</center>

<h1>Storybook Addon Controls</h1>

Storybook Controls provides UI to interactive to edit the properties of your components and stories.
shilman marked this conversation as resolved.
Show resolved Hide resolved

Controls replaces [Storybook Knobs](https://github.com/storybookjs/storybook/tree/master/addons/knobs) and offers the following improvements:

- **Convenience.** Auto-generated based on your components (even for non-React frameworks)
shilman marked this conversation as resolved.
Show resolved Hide resolved
- **Compatibility.** Controls is based on [Storybook Args](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs), so stories written for Controls will be portable:
- **Documentation.** Controls 100% compatible with [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/addons/docs).
- **Testing.** Re-use stories directly inside testing tools like [Jest](https://jestjs.io/).
- **And more.** Re-use stories in design and development tools as Args is adopted.

Most importantly, Controls incorporates lessons from years of supporting Knobs on tens of thousands of projects and dozens of different frameworks. We couldn't incrementally fix knobs, so we built a better version.

- [Get started](#get-started)
- [Installation](#installation)
- [Writing stories](#writing-stories)
- [Auto-generated args](#auto-generated-args)
- [Manually-configured args](#manually-configured-args)
- [Migrating from knobs](#migrating-from-knobs)
- [Knobs to auto-generated args](#knobs-to-auto-generated-args)
- [Knobs to manually-configured args](#knobs-to-manually-configured-args)
- [Available controls](#available-controls)
- [Parameters](#parameters)
- [expanded](#expanded)
- [FAQs](#faqs)
- [How will this replace addon-knobs?](#how-will-this-replace-addon-knobs)

## Get started

To get started with `addon-controls`:

1. [install the addon](#installation)
2. [write an args story](#writing-stories)
3. [configure the addon (optional)](#configuration)

If you're installing it into an existing project that uses `addon-knobs`, you might also [migrate your existing stories](#migrating-from-knobs).

## Installation

In the first version, Controls requires that you're [Storybook Docs](https://github.com/storybookjs/storybook/tree/master/addons/docs). If you're not using it already, please install that first.
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

Next, install the package:

```sh
npm install @storybook/addon-controls -D # or yarn
```

And add it to your `.storybook/main.js` config:

```js
module.exports = {
addons: ['@storybook/addon-controls'],
};
```

Once the addon is installed you should see a `Controls` tab in the addons panel:
shilman marked this conversation as resolved.
Show resolved Hide resolved

## Writing stories

Controls are only available for stories that make use of [Storybook Args](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs), so if your story functions don't accept an `Args` object as the first argument, you'll need to upgrade your stories.

Here's a `react` story that doesn't use args:

```js
export const Basic = () => <Button label="hello" />;
```

And here's one that does:

```js
export const Basic = (args = { label: 'hello' }) => <Button {...args} />;
shilman marked this conversation as resolved.
Show resolved Hide resolved
```

There are a few different ways to get from here to there:

- The quickest way to do this is to use [auto-generated args](#auto-generated-args).
- If you're implementing custom story logic based on control value, you should use [manually-configured args](#manually-configured-args)
- And if you're adding this to an existing project that's already using knobs, you should [migrate it](#migrating-from-knobs).

### Auto-generated args

In the example above, [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/addons/docs) automatically annotates the `Button` with `ArgTypes` metadata that allows `Controls` to automatically create rows for that component.
shilman marked this conversation as resolved.
Show resolved Hide resolved

So if the full story was:

```js
shilman marked this conversation as resolved.
Show resolved Hide resolved
import { Button } from './Button';
export default { title: 'Button', component: Button };

export const Basic = () => <Button label="hello" />;
```

You could simply rewrite it as:
shilman marked this conversation as resolved.
Show resolved Hide resolved

```js
export const Basic = (args = { label: 'hello' }) => <Button {...args} />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to add import { Button } from './Button'; to this example too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just makes the page much longer without providing informational value. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, the docs talk about automatic stuff and you're showing scenario A to B, where in scenario A you have an import and in scenario B you don't.. so it might be confusing for people without context. I'd say either:
1 - Remove the import from both, people can infer that the Button is coming from somewhere
2 - Make the text more specific like: Rewrite the Basic story as:

```

### Manually-configured args

Auto-generated args make sense when the controls correspond exactly to component. But there are cases where you want controls to control something about the story instead of (or in addition to) the component properties.

Consider the following example that shows reflow behavior of an array of `Button` instances inside a `ButtonGroup` container:

```js
import range from 'lodash/range';
import { Button, ButtonGroup } from '.';

export const Reflow = ({ count }) => (
<ButtonGroup>
{range(count).map((i) => (
<Button label={`button ${i}`} />
))}
</ButtonGroup>
);
```

In this case, we need to specify `ArgTypes` metadata about the story to render properly:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the (guidelines)[https://github.com/storybookjs/storybook/tree/next/docs#guidelines-for-writing-good-documentation] of Storybook, avoid the use of pronouns

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with that guideline

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I guess.. let's discuss about them and make them in a way that matches the preferred style so that anyone following them will be consistent with the rest of the documentation?


```js
Reflow.story = {
argTypes: {
count: { control: { type: 'range', min: 0, max: 100 } },
},
};
```

This is the same kind of data that gets filled in automatically in the [auto-generated args case](#auto-generated-args).
shilman marked this conversation as resolved.
Show resolved Hide resolved

## Migrating from knobs
shilman marked this conversation as resolved.
Show resolved Hide resolved

If you're already using [Storybook Knobs](https://github.com/storybookjs/storybook/tree/master/addons/knobs) you should consider migrating to Controls.

're probably using it for something that can be satisfied by one of the two previous cases, or [manually-configured args](#manually-configured-args).
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

Let's consider a few different knobs-based stories.

### Knobs to auto-generated args

First, let's consider a knobs version of a basic story that fills in the props for a component:

```js
import { text } from '@storybook/addon-knobs';
import { Button } from './Button';

export const Basic = () => <Button label={text('Label', 'hello')} />;
```

This fills in the Button's label based on a knob, which is exactly the [auto-generated](#auto-generated-args) use case above. So we can rewrite it using auto-generated args:

```js
export const Basic = (args = { label: 'hello' }) => <Button {...args} />;
```

### Knobs to manually-configured args

Similarly, we can also consider a story that uses knob inputs to change its behavior:

```js
import range from 'lodash/range';
import { number } from '@storybook/addon-knobs';

export const Reflow = () => {
const count = number('Count', 10, { min: 0, max: 100, range: true });
return (
<ButtonGroup>
{range(count).map((i) => (
<Button label={`button ${i}`} />
))}
</ButtonGroup>
);
};
```

And again, as above, this can be rewritten using [manually-configured args](#manually-configured-args):

```js
export const Reflow = ({ count }) => (
<ButtonGroup>
{range(count).map((i) => (
<Button label={`button ${i}`} />
))}
</ButtonGroup>
);
Reflow.story = {
argTypes: {
count: { control: { type: 'range', min: 0, max: 100 } },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a mixed message in this example, because not only you're adding the argTypes, but also there seems to be a new mechanic within the control API that translates perfectly the use case of lodash/range. Users who don't fit in this use case might either be confused about this change or get it and wonder what other types are available

},
};
```

## Available controls

FIXME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops 👽


## Parameters

Controls supports the following configuration parameters, either [globally or on a per-story basis](https://storybook.js.org/docs/basics/writing-stories/#parameters):
shilman marked this conversation as resolved.
Show resolved Hide resolved

#### expanded
shilman marked this conversation as resolved.
Show resolved Hide resolved

Since Controls is built on the same engine as Storybook Docs, it can also show property documentation alongside your controls using the `expanded` parameter (defaults to `false`).

To enable expanded mode globally, add the following to `.storybook/preview.js`:

```js
export const parameters = {
controls: { expanded: true },
};
```

And here's what the resulting UI looks like:

> FIXME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops 👽


## FAQs

### How will this replace addon-knobs?

Addon-knobs is one of Storybook's most popular addons with over 1M weekly downloads, so we know lots of users will be affected by this change. Knobs is also a mature addon, with various options that are not available in addon-controls.

Therefore, rather than deprecating addon-knobs immediately, we will continue to release knobs with the Storybook core distribution until 7.0. This will give us time to improve Controls based on user feedback, and also give knobs users ample time to migrate.

If you are somehow tied to knobs or prefer the knobs interface, we are happy to take on maintainers for the knobs project. If this interests you, hop on our [Discord](https://discord.gg/UUt2PJb).
Binary file added addons/controls/docs/addon-controls-hero.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions addons/controls/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@storybook/addon-controls",
"version": "6.0.0-beta.8",
"description": "Controls for component properties",
"keywords": [
"addon",
"storybook",
"knobs",
"controls",
"properties"
],
"homepage": "https://github.com/storybookjs/storybook/tree/next/addons/controls",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "addons/controls"
},
"license": "MIT",
"main": "dist/register.js",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addon-docs": "6.0.0-beta.8",
"@storybook/addons": "6.0.0-beta.8",
"@storybook/api": "6.0.0-beta.8",
"@storybook/client-api": "6.0.0-beta.8",
"@storybook/components": "6.0.0-beta.8",
"core-js": "^3.0.1"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions addons/controls/preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/preset');
1 change: 1 addition & 0 deletions addons/controls/register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/register';
14 changes: 14 additions & 0 deletions addons/controls/src/components/ControlsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { FC } from 'react';
shilman marked this conversation as resolved.
Show resolved Hide resolved
import { ArgsTable } from '@storybook/components';
import { useArgs, useArgTypes, useParameter } from '@storybook/api';

interface ControlsParameters {
expanded?: boolean;
}

export const ControlsPanel: FC = () => {
const [args, updateArgs] = useArgs();
const rows = useArgTypes();
const { expanded } = useParameter<ControlsParameters>('controls', {});
shilman marked this conversation as resolved.
Show resolved Hide resolved
return <ArgsTable {...{ compact: !expanded, rows, args, updateArgs }} />;
shilman marked this conversation as resolved.
Show resolved Hide resolved
};
2 changes: 2 additions & 0 deletions addons/controls/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ID = 'addon-controls' as const;
shilman marked this conversation as resolved.
Show resolved Hide resolved
export const PARAM = 'controls' as const;
3 changes: 3 additions & 0 deletions addons/controls/src/preset/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function managerEntries(entry: any[] = []) {
return [...entry, require.resolve('../register')];
}
23 changes: 23 additions & 0 deletions addons/controls/src/register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import addons, { types } from '@storybook/addons';
import { AddonPanel } from '@storybook/components';
import { API } from '@storybook/api';
import { ControlsPanel } from './components/ControlsPanel';
import { ID } from './constants';

addons.register(ID, (api: API) => {
addons.addPanel(ID, {
title: 'Controls',
type: types.PANEL,
render: ({ active }) => {
if (!active || !api.getCurrentStoryData()) {
return null;
}
return (
<AddonPanel active={active}>
<ControlsPanel />
</AddonPanel>
);
},
});
});
9 changes: 9 additions & 0 deletions addons/controls/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest"]
},
"include": ["src/**/*"],
"exclude": ["src/**.test.ts"]
}
2 changes: 1 addition & 1 deletion examples/official-storybook/components/ButtonGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';

/** ButtonGroup component description from docgen */
export const ButtonGroup = ({ background, children }) => (
<div style={{ background }}>{children}</div>
<div style={{ padding: 10, background }}>{children}</div>
);

ButtonGroup.defaultProps = {
Expand Down
1 change: 1 addition & 0 deletions examples/official-storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
'@storybook/addon-links',
'@storybook/addon-events',
'@storybook/addon-knobs',
'@storybook/addon-controls',
'@storybook/addon-cssresources',
'@storybook/addon-backgrounds',
'@storybook/addon-a11y',
Expand Down
1 change: 1 addition & 0 deletions examples/official-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@storybook/addon-a11y": "6.0.0-beta.8",
"@storybook/addon-actions": "6.0.0-beta.8",
"@storybook/addon-backgrounds": "6.0.0-beta.8",
"@storybook/addon-controls": "6.0.0-beta.8",
"@storybook/addon-cssresources": "6.0.0-beta.8",
"@storybook/addon-design-assets": "6.0.0-beta.8",
"@storybook/addon-docs": "6.0.0-beta.8",
Expand Down
1 change: 1 addition & 0 deletions examples/official-storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ addParameters({
restoreScroll: true,
},
},
controls: { expanded: true },
options: {
storySort: (a, b) =>
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DocgenButton } from '../../components/DocgenButton';
export default {
title: 'Addons/Docs/ArgsStory',
component: DocgenButton,
parameters: { chromatic: { disable: true } },
parameters: { controls: { expanded: false }, chromatic: { disable: true } },
};

export const One = argsStory({ label: 'One' });
Expand Down
Loading