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

Is it possible to dynamically generate stories in CSF #9828

Closed
michael-ecb opened this issue Feb 12, 2020 · 44 comments
Closed

Is it possible to dynamically generate stories in CSF #9828

michael-ecb opened this issue Feb 12, 2020 · 44 comments

Comments

@michael-ecb
Copy link

michael-ecb commented Feb 12, 2020

Hi,

I have a folder with many icon components and I need to load them dynamically into stories.
is it possible with CSF like it was possible before : in this previous issue

thanks

@shilman
Copy link
Member

shilman commented Feb 12, 2020

No, you need to use the API for dynamic stories AFAIK

@shilman shilman closed this as completed Feb 12, 2020
@michael-ecb
Copy link
Author

ok thanks!

@michael-ecb
Copy link
Author

@shilman perhaps you have an example to give us?
without it we can't upgrade to the latest version ;/

@shilman
Copy link
Member

shilman commented Feb 13, 2020

import { storiesOf } from '@storybook/react';
const cases = ['a', 'b', 'c'];
const stories = storiesOf('foo', module);
cases.forEach(label => stories.add(label, () => <>{label}</>));

https://storybook.js.org/docs/formats/storiesof-api/

@matyasf
Copy link

matyasf commented Feb 17, 2021

@shilman The storiesof API document that you linked states that it's a legacy API. What is the current way of adding multiple stories programatically? I could not find anything in the csf docs at https://storybook.js.org/docs/react/api/csf

@shilman
Copy link
Member

shilman commented Feb 18, 2021

@matyasf we don't have one. what's your use case?

It's very important for us that CSF be statically analyzable, so we're not planning on adding a dynamic story API. One common use case I've seen is combinatorial testing, and we might support that through some other mechanism, such as this proposal.

@matyasf
Copy link

matyasf commented Feb 18, 2021

We are making a UI library and generating component variations programatically. These are fed to Chromatic which compares them to the last result, this way we can see easily and precisely what visual changes a code change caused.

Heres out stories.js: https://github.com/instructure/instructure-ui/pull/399/files#diff-55e315ae08c20edbe6c750231bdfa74a92bdc0752777f9332db54675e17af835

and the variation generator: https://github.com/instructure/instructure-ui/tree/master/packages/ui-component-examples

@hypervillain
Copy link

Same use case as @matyasf here

@shilman
Copy link
Member

shilman commented Feb 18, 2021

Thanks @matyasf. I believe the combos proposal--possibly with some extra features--should satisfy your use case. We don't plan to formally deprecate storiesOf until we have a replacement that we feel confident about, so you shouldn't need to worry about the "legacy" thing at this time. We just want everybody who doesn't need this feature on CSF, because it will save a lot of pain later on.

@IanEdington
Copy link

IanEdington commented Mar 8, 2021

This might be another example of the benefit of an api.
We have a ~200 emails that we would like to storybook + chromatic with.
They all follow the same pattern so could easily be generated by finding all files that end in email.tsx and looping over them.

Any suggestions are welcome :D

@shilman
Copy link
Member

shilman commented Mar 8, 2021

@IanEdington Thanks for sharing that! The "CSF-compatible" way to do this would be to write a custom loader for those email files, which should be as simple as the for-loop you describe. We'll create a sample when we deprecate storiesOf to make it easy for people with this use case to transition off.

cc @jonniebigodes

@nhoizey
Copy link

nhoizey commented Mar 31, 2021

the combos proposal

@shilman I've read the doc about this proposal, is there an issue to follow implementation? (I couldn't find it)

I wonder if it could also be used for color palettes/shades, to present design tokens.

@shilman
Copy link
Member

shilman commented Mar 31, 2021

It's just a proposal, we haven't agreed on anything and haven't started implementing it. However, we are intent on deprecating storiesOf and will try to get a suitable alternative in place before we pull the trigger.

@nhoizey
Copy link

nhoizey commented Mar 31, 2021

@shilman ok, thanks. I've not used storiesOf yet, but I find ColorPalette/ColorItem, Typeset, etc. not enough for presenting design tokens (I get them in a JSON file generated with Style Dictionary), so I'm looking for alternatives.

@shilman
Copy link
Member

shilman commented Mar 31, 2021

@nhoizey look into MDX -- you can embed arbitrary react elements in there to do whatever you want

https://storybook.js.org/docs/react/writing-docs/mdx

@nhoizey
Copy link

nhoizey commented Mar 31, 2021

@shilman I'm already using MDX, I explained it in another issue, let's continue there: #7671 (comment)

@wtakayama-chwy
Copy link

I've come up with an idea that worked for me quite good when trying to "generate" stories dynamically for an icons folder.
Hope this workaround could help someone. It does not exactly generate stories, but creates your input selector dynamic, so everytime you add a new icon into your icons folder and export it on your index.js the storybook will be updated

The basic folder structure is as follow:
icons > ArrowRight.js

  1. Create a index.js file at your root folder (icons)
    1.1 In this file export all your icons as followed: export { default as ArrowRight} from './ArrowRight'
  2. On your Icons.stories.js
    2.1 Import all icons: import * as icons from './index'
    2.2 Create a story where it's possible to select your icons dynamically (need to install - withKnobs)
    e.g.
export const IconDefaultPicker = () => {
  const componentOptions = select('component', Object.keys(icons), 'Cat', 'main')
  const fontSizeOptions = select(
    'fontSize',
    ['default', 'inherit', 'large', 'medium', 'small'],
    'large',
    'main',
  )
  const htmlColorOptions = select(
    'htmlColor',
    ['inherit', 'primary', 'secondary', 'action', 'disabled', 'error'],
    'primary',
    'main',
  )

  return (
    <SvgIcon
      component={icons[componentOptions]}
      fontSize={fontSizeOptions}
      htmlColor={htmlColorOptions}
    />
  )
}

@msakrejda
Copy link
Contributor

Just wanted to note I have a use case similar to @IanEdington: we have some components that display some complex data, and we pass that to the component as a plain JS object. These are generated as JSON by a separate system, and we have some JSON "expected" test files for that system to ensure we don't regress anything. We would like to generate a separate story for each JSON file. I can do that with storiesOf now. I skimmed over the proposal linked above, and I think that would also work for our use case (mostly as a degenerate case of a lot of variations of one complex prop).

@eze-peralta
Copy link

eze-peralta commented Oct 9, 2021

Hello, I think the previous approach using knobs was better as it allowed to easily create dynamic stories.

For example with knobs it is easy to do something like

import React from 'react';
import {storiesOf} from '@storybook/react';
import {componentFactory} from "../factories/componentFactory";
import {text, object} from '@storybook/addon-knobs';

const testConfig = [
    {
        "type": "DefaultCreateButton",
        "storyName": "Contained",
        "props": {
            "resource": "users"
        },
        "config": {
            "label": "Test",
            "variant": "contained"
        }
    },
    {
        "type": "DefaultCreateButton",
        "storyName": "Text",
        "props": {
            "resource": "users"
        },
        "config": {
![Screen Shot 2021-10-09 at 5 02 46 PM](https://user-images.githubusercontent.com/25964088/136676849-6c733ca8-742d-4fb4-be66-739113a8ee47.png)
            "label": "Test",
            "variant": "text"
        }
    },
    {
        "type": "CreateResourceWithRelated",
        "storyName": "Primary",
        "props": {
            'resource': "carrier_bids",
            'resourceName': "Carrier Bids",
            'relatedResourceName': "Items",
            'relationshipField': "carrier_bid_items",
            'fields': [
                {
                    source: "id",
                    "label": "ID"
                }
            ],
            'record': {
                id: 1,
                owner_username: 'test_user',
                'carrier_bid_items': []
            },
        },
        "config": {
            'resource': 'carrier_bids',
            'fieldProps': [
                {
                    label: "Charge Type",
                    field: "charge_type",
                    defaultValue: "Line Haul",
                    size: "small",
                    variant: "filled"
                },
                {
                    label: "Amount",
                    field: "amount",
                    defaultValue: 0,
                    type: "number",
                    size: "small",
                    variant: "outlined"
                },
                {
                    label: "Currency",
                    field: "currency",
                    defaultValue: "USD",
                    size: "small",
                    variant: "outlined"
                }
            ]
        }
    }
]

for (let conf of testConfig) {
    const compType = conf.type
    const comp = componentFactory({'type': compType})

    storiesOf(compType, module)
        .add(conf.storyName, (args) => {
            let _args = {}
            let _config = {}

            const configGroupId = 'Config';
            const propsGroupId = 'Props';

            for (const [k, v] of Object.entries(conf.config)) {
                if (typeof v === 'object') {
                    _config[k] = (object(k, v, configGroupId))
                    continue
                }
                _config[k] = (text(k, v, configGroupId))
            }

            for (const [k, v] of Object.entries(conf.props)) {
                if (typeof v === 'object') {
                    _args[k] = (object(k, v, propsGroupId))
                    continue
                }
                _args[k] = (text(k, v, propsGroupId))
            }
            return comp(React, _config)(_args)
        })
}

where my components are all defined like this

import {CreateButton} from "react-admin"

const DefaultCreateButton = (React, buttonConfig) => {
    return (props) => {
        console.log(props, buttonConfig)
        const to_resource = props.resource
        return (
            <CreateButton style={{backgroundColor: props.backgroundColor}}
             basePath={to_resource}
              label={buttonConfig.label}
               variant={buttonConfig.variant}/>
        )
    }
}

export default DefaultCreateButton;

My components are factories, so we can define some initial config and then allow parent components to still pass props.

Is it possible to do this using the new Controls approach ???

It seems impossible due to inability to do dynamic export in js.

@shilman shilman reopened this Oct 25, 2021
@shilman shilman added this to the 7.0 milestone Oct 25, 2021
@fsjsd
Copy link

fsjsd commented May 3, 2022

Adding 2 cents to this. Implemented Storybook in 2 orgs where I've loaded test case data from Google Sheets in so colleagues can see how each row of data appears in a feature mounted in Storybook. Very useful solution for Product Managers to be able to browse their features in states they can maintain in Google Sheets, so for loop wrapping storiesOf has been a good solution.

More recently I've done something sort of along the same lines as what @shilman is proposing with Combos, building an intermediate wrapper around args that shunts the values dynamically into Storybook controls, but not ideal - Combos looks like a great solutions to storiesOf.

@oriooctopus
Copy link

I think the combos addon is a great idea. However, I don't think it will work well for our use case, which I'd like to share here.

TLDR: We need a way to show extra stories in Chromatic but not in regular storybook.

We have 3 different themes. On storybook, only 1 theme is shown at a time and users can toggle between the themes using the theme-switcher addon. On chromatic however, we show all 3 themes on the same page at the same time.
There exists an edge case where having all 3 iterations on one page at the same time will break things, so we need to render a separate story per iteration, 3 in total. We only want this to happen on chromatic, and on normal storybook we would just have the one story instead of 3.

The combos addon wouldn't work because it would put everything on the same story/page, which would break things. storiesOf won't work because we want to use the new v7 store.

@shilman
Copy link
Member

shilman commented Jun 15, 2022

@oriooctopus I'd propose the following workaround for your case:

https://www.notion.so/chromatic-ui/Story-tags-586c7d9ff24f4792bc42a7d840b1142b

For those special case stories that only show up in chromatic, you'd tag those stories with chromatic and configure them to not be visible in Storybook by default, but to be visible in Chromatic. The exact mechanism for this is still TBD.

What do you think?

@Tallyb
Copy link

Tallyb commented Jul 6, 2022

@shilman - is there a way to create nested titles for this example? #9828 (comment)

@thibaudcolas
Copy link

Is this the best issue to follow for updates on storiesOf-type use cases in v7? The v7.0 beta announcement mentions project view 7.0 Burndown, is there an issue on there that’s about dynamic stories generation?


A few words about my use case for this if it helps – roughly what’s described as "JSON fixture" in discussion #18480:

  • We’re starting to use Storybook with a simpler "pattern library" generator, where the fixtures are defined as YAML files.
  • Without Storybook, those fixtures would be discovered on the filesystem by the generator to produce its "patterns".
  • With Storybook, we instead use Webpack / Node APIs (require.context or just loads of imports) to load those fixtures and then use storiesOf to generate stories.

Here’s an example script if this helps.

I believe this specific use case might still be possible with a Babel transform or macro generating CSF, but we’ve moved away from Babel for everything else (using instead vanilla TypeScript or ESBuild or SWC). In an ideal world we’d even be able to generate stories at runtime in the browser – make an API request to our pattern library generator, and generate stories based on that.

@shilman
Copy link
Member

shilman commented Jan 3, 2023

@thibaudcolas we're not going to fix this for 7.0. the workaround is to use the following flag in .storybook/main.js:

module.exports = {
  features: {
    storyStoreV7: false
  }
}

This will use the old, unoptimized way of loading Storybook. So you'll miss out on a lot of the performance optimizations in V7, but you'll still be able to use the storiesOf API. We will address this properly in a 7.x release and plan to remove storiesOf support entirely in 8.0 once we have a solution that we consider acceptable for common use cases described in this issue.

kaga added a commit to philals/rtl-sbi-performance that referenced this issue Jan 16, 2023
Storybook use of CSF format it is optimized in v7.

storybookjs/storybook#9828

Since we are measuring performance here, I use mustache to generate the test case before running the test / storybook
@nstuyvesant
Copy link

For @carbon/charts (monorepo), there are five packages: Angular, Vanilla JavaScript, React, Svelte/Sveltekit, and Vue.js. For each environment, there are 179 stories created for the charts using storiesOf() and a big array. There's another 28 for diagrams (applicable for Angular and React) plus another 12 docs. Net/net - over 1000 stories.

Here's a link to the Vanilla JS storybook: https://charts.carbondesignsystem.com/?path=/story/docs--welcome. The Welcome page has the links to the others. This was all created in Storybook 5.x.

To move to CSF3, I'm guessing we would need to write our own storiesOf API that would output a file per story into a cache folder that would be viewed as a story source in our main.ts. The files would have to be gitignored and we'd need to clean them before each storybook build.

I'm open to any solution but the capability of building stories from data is very important especially for large publicly-facing monorepos where Storybook is the documentation.

@nstuyvesant
Copy link

nstuyvesant commented Mar 30, 2023

Another item I wanted to add here... we have a number of components that are too simplistic to show as separate stories (like lines, or lines with arrows). For those, we want to follow a bunch of them together in an MDX file for documentation purposes. If we had to turn them into stories, they would look like the Diagrams section here which provides very little value:
https://charts.carbondesignsystem.com/angular/?path=/story/diagrams-edges--color

Instead, we want something more like this as an MDX (this example is using storiesOf())...
https://charts.carbondesignsystem.com/angular/?path=/story/diagrams--start-here
but without navigation nodes on the left for each component. Essentially we want stories to appear in the MDX but not on their own.

@shilman
Copy link
Member

shilman commented Jun 21, 2023

OK, I finally have a proposal for deprecating StoriesOf and providing some rough paths forward for users. I've documented this in an RFC, which even includes a working prototype for people to play with: #23177. Feedback is welcome on the RFC discussion or in DM on Discord if you prefer chatting about it http://discord.gg/storybook

luontola added a commit to luontola/territory-bro that referenced this issue Aug 2, 2023
Why:
- Webpack + Babel is a pain to maintain and there are nowadays better
  build tools. Vite had good results in the State of JavaScript 2022.
    https://2022.stateofjs.com/en-US/libraries/build-tools/
- Frontend directories had to be relocated to accommodate Vite
  conventions. However, we changed the default root from "." to "web",
  to maintain the separation between frontend and backend code.
- For CSS modules to work in Vite, their file extension had to be
  changed to "".module.css".
- Mocha didn't have good Vite integration, so the test runner was
  changed to Vitest. This avoids the Babel dependency.
- Vite crashes with segmentation fault inside Debian, so the builder's
  Linux distro was changed to Alpine.
- Storybook had to be upgraded for Vite support. Storybook 7 has
  deprecated the storiesOf function, but we can't migrate away from it
  because there is not yet any other way to generate stories
  dynamically. See storybookjs/storybook#9828
@valentinpalkovic
Copy link
Contributor

valentinpalkovic commented Feb 19, 2024

Please use the documented experimental indexer API to generate stories dynamically. Closing this issue. Let us know if the indexer API isn't sufficient for your use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests