Skip to content
Automatically generate stories for all of your component variants
TypeScript JavaScript HTML CSS
Branch: master
Clone or download
Latest commit 233c86c Jan 9, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.vscode
dist
example
media Add cartesian components Jan 9, 2019
react add prop=names Jan 9, 2019
src add prop=names Jan 9, 2019
.gitignore Add cartesian components Jan 9, 2019
.npmignore
.travis.yml build: travis Sep 29, 2018
LICENSE.txt first commit Sep 28, 2018
README.md add prop=names Jan 9, 2019
package.json
rollup.config.js
tsconfig.json
tslint.json first commit Sep 28, 2018
yarn.lock Add cartesian components Jan 9, 2019

README.md

Storybook Cartesian

Automatically generate stories for all of your component variants.

See more about this example in examples/app.

Quick Start

Install:

$ yarn add --dev storybook-cartesian

To integrate in your own project, add to your stories:

import cartesian from 'storybook-cartesian'
cartesian(storiesOf('Button/Cartesian', module))
  .add(() => ({
        colors: [
            { bg: '#FF5630', fg: '#FFBDAD' }, 
            { bg: '#4C9AFF', fg: '#B3D4FF' }
        ],
        text: ['Click Me', '', '你好']
    }),
    props => <Button 
                style={{ 
                    padding: '1em 3em', 
                    border: 'none', 
                    backgroundColor: props.colors.bg, 
                    color: props.colors.fg 
                }}>
                {props.text}
            </Button>
  )

Basics

The general structure for cartesian is this:

cartesian(<stories>)
    .add(
        <seed function>,
        <component renderer>,
        { 
            renderTitle: <title renderer>,
            valid: <valid combination filter (optional)>,
            apply: <story apply function (optional)>
        }
    )

Which gets you this kind of story layout generated automatically (for now the last "All/all variants" is a story discussed in Advanced):

Your seed function is responsible to generate content in the form of:

// if this is a sample of your props:
const props = {
    one: "hello",
    two: "foobar"
    check: true
}

// then this is your seed function:
const seedfn = ()=>({
    one: ["hello", "another"],
    two: ["foobar"]
    check: [true, false]
})

If you want to have just a selection of props be cartesian you can use the special choice function:

import cartesian, { choice } from 'cartesian'

const seedfn = ()=>({
    one: "rabbit",
    two: "rabbit, rabbit",
    check: choice(true, false)
})

This will create a special data strucure which tells cartesian to create these combinations:

[{
    one: "rabbit",
    two: "rabbit, rabbit",
    check: true
},{
    one: "rabbit",
    two: "rabbit, rabbit",
    check: false
}]

Your titleRender function gets an instance of your props and returns a string:

const renderTitle = props => `${props.one} / ${props.check}`

Your storyRender function gets an instance of your props and returns a component:

const componentRender = props => <Button {...props} />

And to compose all of these with cartesian we can now do:

cartesian(storiesOf('Button/Cartesian'))
    .add(
        seedfn,
        componentRender,
        { renderTitle }
    )

Extras

Applying Stories with Premade Components

You can showcase all variants in two ways with one of the premade components:

  • Tiles - showcase variants as tiles which fill up the screen (many components on rows and columns).
  • Rows - same thing, just one component per row.

And you have a helper function applyWith(title, component) that takes a title and one of these components, and generates your stories apply function.

import { Tiles, applyWith }  from 'storybook-cartesian/react'

cartesian(storiesOf('Button/Cartesian/applyWith(Tiles)', module))
  .add(() => ({
      colors: [{ bg: '#FF5630', fg: '#FFBDAD' }, { bg: '#4C9AFF', fg: '#B3D4FF' }],
      text: ['Click Me', '', '你好']
    }),
    props => <Button style={{ padding: '1em 3em', border: 'none', backgroundColor: props.colors.bg, color: props.colors.fg }}>{props.text}</Button>,
    { 
      renderTitle: titles.renderPropNames(),
      apply: applyWith("everything!", Tiles)
    }
  )

And the result:

You can also use any of the individual components on their own:

import { Rows }  from 'storybook-cartesian/react'

cartesian(storiesOf('Button/Cartesian/Tiles', module))
  .add(() => ({
      colors: [{ bg: '#FF5630', fg: '#FFBDAD' }, { bg: '#4C9AFF', fg: '#B3D4FF' }],
      text: ['Click Me', '', '你好']
    }),
    props => <Button style={{ padding: '1em 3em', border: 'none', backgroundColor: props.colors.bg, color: props.colors.fg }}>{props.text}</Button>,
    { 
      renderTitle: titles.renderPropNames(),
      apply: (stories, candidates) => {
        stories.add('all variants', () => <Rows items={candidates}/>)
      }
    }
  )

Premade title renderers

You can pick a title renderer from a premade collection:

  • renderCheckSignIfExists - renders the prop name and a 'check' sign if it exists, 'x' if missing
  • renderPropNames - renders just the prop names given
  • renderProps - render a prop=value format

You can use one of these like so:

import cartesian, { titles } from 'cartesian'
cartesian(storiesOf('Button/Cartesian'))
    .add(
        seedfn,
        componentRender,
        { renderTitle: titles.renderCheckSignsIfExists() }
    )

Which produces the following title with props = { oneProp: null, twoProp: 2}:

x oneProp | ✓ twoProp

There are more renderers that you can explore (also - happy to get PRs with more!).

Beautiful names for variants

If you'd like prop values to have logical names, try renderWithLegend:

import cartesian, { renderWithLegend } from 'cartesian'

const complex = { foo:1, bar: 2 }
complex.toString = () => 'complex-1'

const titleWithLegend = renderWithLegend({
  '#FF5630': 'primary',
  '#FFBDAD': 'secondary',
  '#4C9AFF': 'primary-opt',
  '#B3D4FF': 'secondary-opt',
  'Click Me': 'english',
  [complex]: 'complex object',
  '': 'empty',
  '你好': 'chinese'
})

cartesian(storiesOf('Button/Cartesian (legend)', module))
  .add(() => ({
    colors: [{ bg: '#FF5630', fg: '#FFBDAD' }, { bg: '#4C9AFF', fg: '#B3D4FF' }],
    text: ['Click Me', '', '你好']
  }),
    props => <Button style={{ padding: '1em 3em', border: 'none', backgroundColor: props.colors.bg, color: props.colors.fg }}>{props.text}</Button>,
    {
        renderTitle: titleWithLegend(props => `"${props.text}" ${props.colors.bg + '/' + props.colors.fg}`),
    }
  )

renderWithLegend takes a legend dict, that maps actual prop values to the ones you give in the legend.

Then, it takes a normal renderTitle function that you supply, and it will make sure prop values will be legend values.

If you want just a top level legend translation (not going into all values in a data structure) use renderWithLegendFlat.

Validating Variants

Some times, not all prop combinations make sense. For example if you have an isLoading and a results props it doesn't make sense to have both true and results populated:

// doesn't make sense
<SearchResults isLoading={true} results={['hello', 'world']}>

For this, we have an valid function that we can add, and the following will filter out this invalid combination:

cartesian(storiesOf('Button/Cartesian'))
    .add(
        seedfn,
        componentRender,
        { 
            renderTitle,
            valid: props => !(props.isLoading && props.results)
        }
    )

Some other times you might want to customize how you add stories. For example, let's say you want just one story to contain all cartesian product items.

For this, we have another optional function:

const allVariantsInOne = (stories, variants)=>{
    const story = variants.map(c=>(
        <div>
            <div>{c.title}</div>
            <div>{c.story}</div>
        </div>))
    stories.add('all variants', ()=> story)
}

cartesian(storiesOf('Button/Cartesian'))
    .add(
        seedfn,
        componentRender,
        { apply: allVariantsInOne }
    )

Contributing

Fork, implement, add tests, pull request, get my everlasting thanks and a respectable place here :).

Thanks

To all Contributors - you make this happen, thanks!

Copyright

Copyright (c) 2018 Dotan Nahum @jondot. See LICENSE for further details.

You can’t perform that action at this time.