Storybook
https://storybook.js.org/ is an incredibly useful tool. It allows you to visually document your components. For each component you "tell a story" of what that component looks like in different states. It can even http://blog.caplin.com/2017/10/04/dev-week-storybook-and-jest/.
Integrating it into a ReasonML project is relatively straightforward. Given that bsb
already compiles our ReasonML files into JavaScript, it is imply a case of tweaking storybook's configuration.
NOTE: this assumes your project was created using bsb -init <your project> -theme react
. If not, and you used create-react-app
then the directories/generated JS might be in different places.
Installing Storybook
Install Storybook by following https://storybook.js.org/basics/quick-start-guide. This will automatically do the following:
- add a new
stories
directory containing an exampleindex.stories.js
- create a
.storybook
directory containing itsconfig.js
and a Storybook specificwebpack.config.js
- add a "storybook" script into
package.json
for running a development Storybook server - add a "build-storybook" script into
package.json
. I will leave it as an exercise for the reader to find out what that does ;-).
Next we add in the plumbing so we can write stories in ReasonML.
Storybook, meet ReasonML
To teach ReasonML and Storybook about each other we need to do the following:
- Create a directory where we will store our stories (or accept the default
stories
directory) - Provide bindings for the Storybook library
- Teach
bsb
to build the stories in that directory - Teach Storybook to consume the stories in that directory
- Write some stories!
1. Create the directory
As convention, I prefer to have a dedicated stories
directory as a sibling of src
, which is also the default setup for Storybook so this should already exist.
2. Provide bindings
As with most libraries we need to provide bindings to bsb
so it can understand how to interact with the third party library. There is prior art here at https://github.com/splish-me/bs-storybook, but unfortunately they use the older format style and isn't published anywhere.
For now, I suggest creating a vendor-src
directory and putting library bindings under that. Specifically, vendor-src/storybook
:
- Create
vendor-src/storybook
- Add
vendor-src/storybook
to thesources
key inbsconfig.json
Now, create the actual bindings in vendor-src/storybook/storybook.rei
:
type section;
type story = unit => ReasonReact.reactElement;
[@bs.val] [@bs.module "@storybook/react"]
external createSection : (string, 'a) => section = "storiesOf";
[@bs.send]
external addDecorator : (section, story => ReasonReact.reactElement) => unit =
"";
[@bs.send] external addStory : (section, string, story) => unit = "add";
Your project should now understand how to interpret stories written in ReasonML.
3. Teach `bsb` to build the stories
In step 1. we created (or used the existing) stories
directory. To each bsb
about that simply add the stories
to the sources
key in bsconfig.json
.
4. Teach storybook to consume the stories
We should now have stories correctly translated from ReasonML to JS. If you aren't already running yarn start
(or running bsb
some other way then please do so now).
Storybook's config (in .storybook/config.js
is already configured to look in the stories
directory, but it is restricted to only files that match/.story.js$/
which won't match our generated JS, so change it to /.js$/
.
If you have chosen a different directory for your stories you will need to change the "../stories"
to "../whatever-you-called-your-directory"
.
At this point .storybook/config.js
should look something like:
import { configure } from "@storybook/react";
// automatically import all files ending in *.stories.js
const req = require.context("../stories", true, /.js$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);
5. Write some stories!
To create a story we first need a component. I am assuming you have a src/page.re
:
/* This is the basic component. */
let component = ReasonReact.statelessComponent("Page");
/* Your familiar handleClick from ReactJS. This mandatorily takes the payload,
then the `self` record, which contains state (none here), `handle`, `reduce`
and other utilities */
let handleClick = (_event, _self) => Js.log("clicked!");
/* `make` is the function that mandatorily takes `children` (if you want to use
`JSX). `message` is a named argument, which simulates ReactJS props. Usage:
`<Page message="hello" />`
Which desugars to
`ReasonReact.element(Page.make(~message="hello", [||]))` */
let make = (~message, _children) => {
...component,
render: (self) =>
<div onClick=(self.handle(handleClick))> (ReasonReact.stringToElement(message)) </div>
};
In stories/PageStories.re
enter the following:
let pageSection = Storybook.createSection("Page", [%bs.raw "module"]);
Storybook.addStory(pageSection, "Test", () => <Page message="Hello!" />);
Assuming bsb
is running (if not then run it) you should see a PageStories.bs.js
. If you run yarn storybook
then your browser should magically open at http://localhost:6006 and look something like:
Complete source code
https://github.com/yatesco/reason-storybook-example contains a project configured using the following steps.