);
```
This is how you can select the above story:
@@ -107,7 +94,7 @@ This is how you can select the above story:
```js
addonAPI.register('my-organisation/my-addon', storybookAPI => {
storybookAPI.selectStory('heading', 'with text');
-})
+});
```
### storybookAPI.selectInCurrentKind()
@@ -161,7 +148,7 @@ This method allows you to get application url state with some changed params. Fo
addonAPI.register('my-organisation/my-addon', storybookAPI => {
const href = storybookAPI.getUrlState({
selectedKind: 'kind',
- selectedStory: 'story'
+ selectedStory: 'story',
}).url;
});
```
@@ -175,3 +162,33 @@ addonAPI.register('my-organisation/my-addon', storybookAPI => {
storybookAPI.onStory((kind, story) => console.log(kind, story));
});
```
+
+## `makeDecorator` API
+
+The `makeDecorator` API can be used to create decorators in the style of the official addons easily. Use it like so:
+
+```js
+import { makeDecorator } from '@storybook/addons';
+
+export makeDecorator({
+ name: 'withSomething',
+ parameterName: 'something',
+ wrapper: (storyFn, context, { parameters }) => {
+ // Do something with `parameters`, which are set via { something: ... }
+
+ // Note you may alter the story output if you like, although generally that's
+ // not advised
+ return storyFn(context);
+ }
+})
+```
+
+The options to `makeDecorator` are:
+
+- `name`: The name of the export (e.g. `withNotes`)
+- `parameterName`: The name of the parameter your addon uses. This should be unique.
+- `skipIfNoParametersOrOptions`: Don't run your decorator if the user hasn't set options (via `.addDecorator(withFoo(options)))`) or parameters (`.add('story', () => , { foo: 'param' })`, or `.addParameters({foo: 'param'})`).
+- `allowDeprecatedUsage`: support the deprecated "wrapper" usage (`.add('story', () => withFoo(options)(() => ))`).
+- `wrapper`: your decorator function. Takes the `storyFn`, `context`, and both the `options` and `parameters` (as defined in `skipIfNoParametersOrOptions` above).
+
+Note if the parameters to a story include `{ foo: {disable: true } }` (where `foo` is the `parameterName` of your addon), your decorator will note be called.
diff --git a/docs/src/pages/addons/writing-addons/index.md b/docs/src/pages/addons/writing-addons/index.md
index 781a34ab0baf..de6d06808d98 100644
--- a/docs/src/pages/addons/writing-addons/index.md
+++ b/docs/src/pages/addons/writing-addons/index.md
@@ -21,11 +21,11 @@ As shown in the above image, there's a communication channel that the Manager Ap
With an addon, you can add more functionality to Storybook. Here are a few things you could do:
-- Add a panel to Storybook (like Action Logger).
-- Interact with the story and the panel.
-- Set and get URL query params.
-- Select a story.
-- Register keyboard shortcuts (coming soon).
+- Add a panel to Storybook (like Action Logger).
+- Interact with the story and the panel.
+- Set and get URL query params.
+- Select a story.
+- Register keyboard shortcuts (coming soon).
With this, you can write some pretty cool addons. Look at our [Addon gallery](/addons/addon-gallery) to have a look at some sample addons.
@@ -43,21 +43,26 @@ We write a story for our addon like this:
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
-import { WithNotes } from '../notes-addon';
+import withNotes from '../notes-addon';
import Button from './Button';
storiesOf('Button', module)
- .add('with text', () => (
-
-
-
- ))
- .add('with some emoji', () => (
-
-
-
- ));
+ .addDecorator(withNotes)
+ .add('with text', () => , {
+ notes: 'This is a very simple Button and you can click on it.',
+ })
+ .add(
+ 'with some emoji',
+ () => (
+
+ ),
+ { notes: 'Here we use some emoji as the Button text. Doesn't it look nice?' }
+ );
```
Then it will appear in the Notes panel like this:
@@ -79,23 +84,29 @@ Now we need to create two files, `register.js` and `index.js,` inside a director
## The Addon
-Let's add the following content to the `index.js`. It will expose a class called `WithNotes`, which wraps our story.
+Let's add the following content to the `index.js`. It will expose a decorator called `withNotes` which we use the `.addDecorator()` API to decorate all our stories.
+
+The `@storybook/addons` package contains a `makeDecorator` function which we can easily use to create such a decorator:
```js
import React from 'react';
-import addons from '@storybook/addons';
-
-export class WithNotes extends React.Component {
- render() {
- const { children, notes } = this.props;
+import addons, { makeDecorator } from '@storybook/addons';
+
+export withNotes = makeDecorator({
+ name: 'withNotes',
+ parameterName: 'notes',
+ // This means don't run this decorator if the notes decorator is not set
+ skipIfNoParametersOrOptions: true,
+ wrapper: (getStory, context, {parameters}) => {
const channel = addons.getChannel();
- // send the notes to the channel.
- channel.emit('MYADDON/add_notes', notes);
- // return children elements.
- return children;
+ // Our simple API above simply sets the notes parameter to a string,
+ // which we send to the channel
+ channel.emit('MYADDON/add_notes', parameters);
+
+ return story(context);
}
-}
+})
```
In this case, our component can access something called the channel. It lets us communicate with the panel (where we display notes). It has a NodeJS [EventEmitter](https://nodejs.org/api/events.html) compatible API.
@@ -122,7 +133,7 @@ class Notes extends React.Component {
onAddNotes = text => {
this.setState({ text });
- }
+ };
componentDidMount() {
const { channel, api } = this.props;
@@ -138,11 +149,9 @@ class Notes extends React.Component {
render() {
const { text } = this.state;
const { active } = this.props;
- const textAfterFormatted = text? text.trim().replace(/\n/g, ' ') : "";
+ const textAfterFormatted = text ? text.trim().replace(/\n/g, ' ') : '';
- return active ?
- :
- null;
+ return active ? : null;
}
// This is some cleanup tasks when the Notes panel is unmounting.
@@ -158,15 +167,13 @@ class Notes extends React.Component {
}
// Register the addon with a unique name.
-addons.register('MYADDON', (api) => {
+addons.register('MYADDON', api => {
// Also need to set a unique name to the panel.
addons.addPanel('MYADDON/panel', {
title: 'Notes',
- render: ({ active }) => (
-
- ),
- })
-})
+ render: ({ active }) => ,
+ });
+});
```
It will register our addon and add a panel. In this case, the panel represents a React component called `Notes`. That component has access to the channel and storybook api.
@@ -199,21 +206,26 @@ That's it. Now you can create notes for any story as shown below:
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
-import { WithNotes } from '../notes-addon';
+import withNotes from '../notes-addon';
import Button from './Button';
storiesOf('Button', module)
- .add('with text', () => (
-
-
-
- ))
- .add('with some emojies', () => (
-
-
-
- ));
+ .addDecorator(withNotes)
+ .add('with text', () => , {
+ notes: 'This is a very simple Button and you can click on it.',
+ })
+ .add(
+ 'with some emoji',
+ () => (
+
+ ),
+ { notes: 'Here we use some emoji as the Button text. Doesn't it look nice?' }
+ );
```
## Styling your addon
diff --git a/lib/addons/src/make-decorator.js b/lib/addons/src/make-decorator.js
index dc2669b35610..e7df28f726f1 100644
--- a/lib/addons/src/make-decorator.js
+++ b/lib/addons/src/make-decorator.js
@@ -15,6 +15,7 @@ export const makeDecorator = ({
parameterName,
wrapper,
skipIfNoParametersOrOptions = false,
+ allowDeprecatedUsage = false,
}) => {
const decorator = options => (getStory, context) => {
const parameters = context.parameters && context.parameters[parameterName];
@@ -44,11 +45,17 @@ export const makeDecorator = ({
return decorator(...args)(...innerArgs);
}
- // Used to wrap a story directly .add('story', decorator(options)(() => ))
- // This is now deprecated:
- return deprecate(
- context => decorator(...args)(innerArgs[0], context),
- `Passing stories directly into ${name}() is deprecated, instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter`
+ if (allowDeprecatedUsage) {
+ // Used to wrap a story directly .add('story', decorator(options)(() => ))
+ // This is now deprecated:
+ return deprecate(
+ context => decorator(...args)(innerArgs[0], context),
+ `Passing stories directly into ${name}() is deprecated, instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter`
+ );
+ }
+
+ throw new Error(
+ `Passing stories directly into ${name}() is not allowed, instead use addDecorator(${name}) and pass options with the '${parameterName}' parameter`
);
};
};
diff --git a/lib/addons/src/make-decorator.test.js b/lib/addons/src/make-decorator.test.js
index e6e94ae7e742..9beb3a706511 100644
--- a/lib/addons/src/make-decorator.test.js
+++ b/lib/addons/src/make-decorator.test.js
@@ -103,10 +103,15 @@ describe('makeDecorator', () => {
expect(story).toHaveBeenCalled();
});
- it('passes options added at story time, but with a deprecation warning', () => {
+ it('passes options added at story time, but with a deprecation warning, if allowed', () => {
deprecatedFns = [];
const wrapper = jest.fn();
- const decorator = makeDecorator({ wrapper, name: 'test', parameterName: 'test' });
+ const decorator = makeDecorator({
+ wrapper,
+ name: 'test',
+ parameterName: 'test',
+ allowDeprecatedUsage: true,
+ });
const options = 'test-val';
const story = jest.fn();
const decoratedStory = decorator(options)(story);
@@ -121,4 +126,17 @@ describe('makeDecorator', () => {
});
expect(deprecatedFns[0].deprecatedFn).toHaveBeenCalled();
});
+
+ it('throws if options are added at storytime, if not allowed', () => {
+ const wrapper = jest.fn();
+ const decorator = makeDecorator({
+ wrapper,
+ name: 'test',
+ parameterName: 'test',
+ allowDeprecatedUsage: false,
+ });
+ const options = 'test-val';
+ const story = jest.fn();
+ expect(() => decorator(options)(story)).toThrow(/not allowed/);
+ });
});