storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.73k stars 9.33k forks source link

Is it possible to dynamically generate stories in CSF #9828

Closed michael-ecb closed 9 months ago

michael-ecb commented 4 years ago

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 commented 4 years ago

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

michael-ecb commented 4 years ago

ok thanks!

michael-ecb commented 4 years ago

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

shilman commented 4 years ago
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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

Same use case as @matyasf here

shilman commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@shilman I'm already using MDX, I explained it in another issue, let's continue there: https://github.com/storybookjs/storybook/issues/7671#issuecomment-797064663

wtakayama-chwy commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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.

kaviththiranga commented 2 years ago

We are also in need of this feature. We have a set of json objects, each having the args for the component being rendered in the story. These json objects are automatically generated by a different tool by analyzing a git hub repo. We want to start storybook with stories for each of them and go through manually and write automated tests to verify that component rendered fine for each of these cases.

Irrelon commented 2 years ago

We have a similar use case. We have a bunch of themes that are passed to a white-labelled app and our storybook needs to be able to show a story for each component named for the theme so a user can see how a component looks with each theme. The themes get added to regularly. We want to be able to maintain a single themes.ts file and export each theme, then import * as themes from "themes.ts", after which, being able to dynamically generate a story per-theme, something like:

const stories = storiesOf('ComponentName', module);

Object.entries(themes).forEach(([themeName, themeData]) => {
    stories.add(themeName, (args) => {<ComponentName {...args}>{children}</ComponentName>}, {
      chakra: {
        theme: themeData
      }
    });
});

The issue here is that we lose all the nice auto-generated actions, and the args are not handled like the new version of storybook (I only just worked out how to do the above so maybe I can still have auto-generated actions and I'm just missing something obvious?).

I can of course export each themed version manually using the CSF way, and that works perfectly with actions included etc, however then we don't have the ability to see a new theme - we'd have to maintain each story file and add an import for each theme every time we add a new one... that will get very time consuming!

shilman commented 2 years ago

@Irrelon Thanks for sharing that example. It's a great use case and, unlike some of we could probably support it in a way that's statically analyzable (details TBD)

stabback commented 2 years ago

To add another example - we are also using a loop to create stories, which unfortunately conflicts with the goal of making stories statically analyzable. I've been thinking of ways to migrate to CSF and can't think of any way to make the switch without writing a lot of repetitive code.

Our application is effectively a stateless widget that takes a complex state object as a prop. We have hundreds of fixtures with more added weekly to demonstrate how the application works with various versions of this object. We use Chromatic to render a thousand stories, which is great for visual regression testing and feature documentation.

It's very nice to be able to manage these fixtures as standard TS and JSON, as anyone (not just people familiar with StoryBook) can add data quickly.

It's also nice to be able to create all of our stories in one place with the following pseudocode:

import App from './App'
import fixtures from './fixtures'

const stories = storiesOf(`Application`, module);
Object.entries(fixtures).forEach(([name, fixture]) => stories.add(name, () => <App state={fixture} />))

We actually have a loop around this block, so we're calling storiesOf dynamically as well to add some additional structure to our StoryBook.

shilman commented 2 years ago

@stabback that's another one that we can probably statically analyze, depending on what kind of restrictions we put on the data structure, e.g. "must be an object, where keys are stories and values are the props passed into your component". Needs more thought when we actually go about tackling this.

For discussion's sake, would CSF3 solve your problem?

import App from './App'
export default { title: 'Application', component: App }

export const Key1 = { args: { state: Val1 } }
export const Key2 = { args: { state: Val2 } }
// ...etc.

Where KeyX/ValX are the existing keys in your JSON file. The key difference is that the fixture file is JS/TS, not JSON, but in terms of human readabilty/code, I'd think this would be on par with your existing approach. What do you think?

jsomsanith-tlnd commented 2 years ago

Hello everyone, and thanks a lot to the SB team for their hard work. Just want to share what we did to solve this issue with current possible storybook formats.

Our context

In order too avoid having tons of configuration in every repository, we are converging with 1 common configuration for all of our repositories, with some allowed customisation.

I have this issue too, I need to create stories dynamically without storiesOf.
In some repos, we need the buildStoriesJson option. It doesn't seem to be compatible with storiesOf. And in other repos, we have cases described in json files, and each one contains a json schema passed as props for a distinct story. We want it to stay dynamic, as when we add a json, it will add automatically a new story.

Our solution

What I ended up doing is writing a non-ES module to achieve it:

const { getAllStories } = require('./json');

module.exports = {
    // NOTICE the require.context to get all the json files in the folder, each json will represent a story
    ...getAllStories('concepts', require.context(`./json/concepts`, true, /\.json$/)),
    default: {
        title: 'JSON Schema/Core concepts',
    },
};

The getAllStories() function will create an object

{
    [StoryNameInCamelCase]: () => <StoryComponent/>
}

This object is then exported via module.exports.

This is equivalent to the ES format but with dynamic exported content and works perfectly.

export const StoryNameInCamelCase = () => <StoryComponent />;
shilman commented 2 years ago

@jsomsanith-tlnd unfortunately your solution won't work with buildStoriesJson or storyStoreV7 (the on-demand story store that will become the default in 7.0). We'll still support legacy mode until we can resolve this issue to our satisfaction (hopefully by 8.0?), but i would't count on this working in the future. i'm hopeful we can come up with a workable alternative to support your "json file" use case.

can you please share your getAllStories function? the link doesn't work 🙏

jsomsanith-tlnd commented 2 years ago

Ok, good to know. Yeah I hope we can find a solution for that. A working link : getAllStories()

stabback commented 2 years ago

@shilman - Sorry, I missed your response to my comment on February 10th.

While we definitely could just change all of our source files from JSON and JS to CSF, we'd like to avoid that. We also use these files for other testing purposes outside of Storybook. While we could also update our test files to understand how to work with these files in a new format, that is a relationship we'd prefer not to maintain and does seem a bit backward.

Storybook is certainly a first-class citizen in our toolchain and our development cycle, so we definitely could tailor our other tools around its requirements, but it'd be super cool not to have to do that and let data just be data.

shilman commented 2 years ago

@stabback totally understood. i think we'll be able to support the "JSON file" use case before we get rid of storiesOf.

sir-captainmorgan21 commented 2 years ago

@shilman am I right that there is no working solution to dynamically render stories based some object from JSON if we are opting in to storyStoreV7?

We have json files that are in CSS in JS format. We are hoping to loop over the classes and render thier usage dynamically. IE Typography classes. We can for dynamically build one big template and stuff it into one story, butt hat is not ideal

shilman commented 2 years ago

Correct @sir-captainmorgan21. "butt hat" indeed 😂

The tentative plan is:

oliverlloyd commented 2 years ago

Hi @shilman , thanks for this extra info!

  • potentially provide some statically-analyzable solutions for two use cases in 7.x

Can I ask, what might these solutions look like? Specifically, would 'combinatorial testing' allow us to loop an object adding stories based on its properties?

We recently added the ability to dynamically add stories by looping an enum using the storiesOf api and we're keen to keep this ability in future versions

sir-captainmorgan21 commented 2 years ago

@shilman haha!

Thanks for that update!

shilman commented 2 years ago

@oliverlloyd here's an example of how we might provide combinatorial testing in CSF: https://www.notion.so/chromatic-ui/Storybook-Combos-a5abecd87e9c4e0b86277244af093aea -- not the final spec, just an idea for now

fsjsd commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@shilman - is there a way to create nested titles for this example? https://github.com/storybookjs/storybook/issues/9828#issuecomment-585480129

thibaudcolas commented 1 year ago

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:

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 commented 1 year ago

@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.

nstuyvesant commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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: https://github.com/storybookjs/storybook/discussions/23177. Feedback is welcome on the RFC discussion or in DM on Discord if you prefer chatting about it http://discord.gg/storybook

valentinpalkovic commented 9 months ago

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.