The storybook_rails
gem provides a Ruby DSL for writing Storybook stories, allowing you to develop, preview, and test standard Rails view partials in Storybook.
This gem is a fork of ViewComponent::Storybook and has been adapted to work with standard Rails view partials.
- A Ruby DSL for writing Stories describing standard Rails view templates/partials
- A Rails controller backend for Storybook Server compatible with Storybook Controls Addon parameters
- More to come...
- Add the
storybook_rails
gem, to your Gemfile:gem 'storybook_rails'
- Run
bundle install
. - Add
require "action_view/storybook/engine"
toconfig/application.rb
- Add
**/*.stories.json
to.gitignore
If your views depend on Javascript, CSS or other assets served by the Rails application you will need to configure asset_hosts
appropriately for your various environments. For local development, do this by adding the following to config/development.rb
:
Rails.application.configure do
...
config.action_controller.asset_host = 'http://localhost:3000'
...
end
Equivalent configuration will be necessary in config/production.rb
or application.rb
based you your deployment environments.
- Add Storybook server as a dev dependedncy. The Storybook Controls addon isn't needed but is strongly recommended
yarn add @storybook/server @storybook/addon-controls --dev
- Add an NPM script to your package.json in order to start the storybook later in this guide
{ "scripts": { "storybook": "start-storybook" } }
- Create the .storybook/main.js file to configure Storybook to find the json stories the gem creates. Also configure the Controls addon:
module.exports = { stories: ['../test/components/**/*.stories.json'], addons: [ '@storybook/addon-controls', ], };
- Create the .storybook/preview.js file to configure Storybook with the Rails application url to call for the html content of the stories
export const parameters = { server: { url: `http://localhost:3000/storybook`, }, };
If your application uses Webpacker to compile your JavaScript and/or CSS, you will need to modify the default Storybook webpack configuration. Please see the Storybook Webpack config for more information on how to do that.
Here's an example of what that might look like:
// .storybook/main.js
const path = require('path');
// Import the Webpacker webpack environment
const environment = require('../config/webpack/environment');
const { merge } = require('webpack-merge');
module.exports = {
webpackFinal: async (config, { configType }) => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// You can change the configuration based on that.
// 'PRODUCTION' is used when building the static version of storybook.
let envConfig = environment.toWebpackConfig();
// Tell Webpack to compile an additional entry point, pointing to your Webpacker pack file(s)
let entries = {
main: config.entry,
application: path.resolve(__dirname, '../app/javascript/packs/application.js')
}
config.entry = entries
// Storybook doesn't support .scss out of the box, and we like scss, so...
// Add a rule to tell Webpack to process .scss files using these loaders
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: path.resolve(__dirname, '../'),
});
// merge Webpacker's config with Storybook's Webpack config
let merged = merge(config, {module: envConfig.module}, {plugins: envConfig.plugins}, {devtool: 'cheap-module-source-map'})
// Return the altered config
return config;
},
};
For a better developer experience, install your favorite file watching utility, such as chokidar and add a couple scripts to enable automatic regeneration of *.stories.json
files when you update *_stories.rb
files:
yarn add -D chokidar-cli
In package.json:
{
...
"scripts": {
"storybook:clean": "find ./test/components/stories -name '*.stories.json' -delete && rm -rf node_modules/.cache/storybook",
"storybook:start": "yarn storybook:clean && yarn storybook:write-json && start-storybook -p 8081",
"storybook:write-json": "bundle exec rake storybook_rails:write_stories_json",
"storybook:watch": "chokidar '**/*_stories.rb' -c 'yarn storybook:write-json'"
},
...
}
Suppose our app has a shared app/views/shared/_button.html.erb
partial:
<% variant_class_map = {
primary: "button",
secondary: "button-secondary",
outline: "button-outline",
} %>
<button class="<%= variant_class_map[variant.to_sym] %>">
<%= button_text %>
</button>
We can write a stories describing the _button.html.erb
partial:
# test/components/stories/buttons/button_stories.rb
class Buttons::ButtonStories < ActionView::Storybook::Stories
self.title = "Buttons"
story(:primary) do
controls do
text(:button_text, "Primary")
end
end
story(:secondary) do
controls do
text(:button_text, "Secondary")
end
end
story(:outline) do
controls do
text(:button_text, "Outline")
end
end
end
And a story template to render individual stories:
# test/components/stories/buttons/button_stories.html.erb
<% story_name_class_map = {
primary: "button",
secondary: "button-secondary",
outline: "button-outline"
} %>
<%= render partial: 'shared/button',
locals: { variant: story_params[:story_name], button_text: story_params[:button_text] } %>
It's up to you how handle rendering your partials in Storybook, but storybook_rails
will look for a view template that matches the story name (buttons/button_stories.html.erb
in the example above. In addition, storybook_rails
provides a story_params
helper which provides quick access to the params and args specified in the story config. You can use these parameters in your view template to render each story dynamically. Or not. It's up to you.
storybook_rails
includes a Rails generator to make it easy to generate the files outlined in the section above.
To generate the files above, we could have done this: bin/rails generate storybook_rails:stories Button primary secondary outline
.
For more detail, bin/rails storybook_rails:stories --help
.
Generate the Storybook JSON stories by running the rake task:
rake storybook_rails:write_stories_json
In separate shells start the Rails app and Storybook
rails s
yarn start-storybook
Alternatively you can use tools like Foreman to start both Rails and Storybook with one command.
By Default storybook_rails
expects to find stories in the folder test/components/stories
. This can be configured but setting config.storybook_rails.stories_path
in config/applicaion.rb
. For example, if you're using RSpec you might set the following configuration:
config.storybook_rails.stories_path = Rails.root.join("spec/components/stories")
If Storybook fails to load with a cannot get /
error, it could be related to this issue. As a workaround, you can update the yarn storybook
script to remove the node_modules/.cache/storybook
files before Storybook starts:
{
"scripts": {
"storybook": "rm -rf node_modules/.cache/storybook && start-storybook"
}
}
If the Storybook UI is rendering, but not fetching stories from your Rails server and you see an error in the browser console like this:
Access to fetch at 'http://localhost:3000/storybook/button/primary' from origin 'http://localhost:8081' has been blocked by
CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs,
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Then you probably need to setup CORS. Please see rack-cors for examples.
🚧 Work in progress... 🚧
From the Storybook CSF docs:
Parameters are a set of static, named metadata about a story, typically used to control the behavior of Storybook features and addons.
Using the example parameters configuration for the Backgrounds addon, this is how you could accomplish the same parameters using the parameters
method:
class ButtonStories < ActionView::Storybook::Stories
background_data = {
default: 'twitter',
values: [
{ name: 'twitter', value: '#00aced' },
{ name: 'facebook', value: '#3b5998' }
]
}
parameters( { backgrounds: background_data })
story(:primary) do
parameters({ backgrounds: { default: 'facebook'} })
controls do
text(:button_text, "Primary")
end
end
end
By default, your stories will render without a layout. This can be customized just like you would normally in a Rails controller, by calling layout "application"
, at either the class level, the story level, or both.
class Buttons::ButtonStories < ActionView::Storybook::Stories
self.title = "Buttons"
layout "application"
story(:primary) do
controls do
text(:button_text, "Primary")
end
end
story(:secondary) do
controls do
text(:button_text, "Secondary")
end
end
story(:custom) do
controls do
text(:button_text, "Custom")
end
layout "custom"
end
end
Coming soon... in the meantime, see https://github.com/danieldpence/storybook_rails/tree/main/lib/action_view/storybook/controls.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/danieldpence/storybook_rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the storybook_rails
project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.