storybookjs / react-native

📓 Storybook for React Native!
https://storybook.js.org
MIT License
996 stars 141 forks source link

Automated "All stories" story #505

Open sregg opened 11 months ago

sregg commented 11 months ago

Is your feature request related to a problem? Please describe. When I have a lot of stories for a given component, it can be tedious to switch stories (i.e. side bar, click story, side bar, click story, etc...). I'd like to be able to see all stories in one screen. Especially for small components like buttons or list items.

Describe the solution you'd like It'd be great to have the ability to automatically add an "All" story where all stories are rendered after an other with their title.

Describe alternatives you've considered Infinite Red's Ignite had some cool components like UseCase for achieving this (see PR when they removed all that here) but the new CSF format doesn't support this (i.e. you can't easily render multiple components in one story).

Are you able to assist bring the feature to reality? Yes!

Additional context Example of Story in Ignite:

Something similar but automated would be awesome.

sregg commented 11 months ago

I just found this: https://storybook.js.org/addons/storybook-addon-variants It'd be great to support this on mobile or build something similar.

dannyhw commented 11 months ago

You can pretty easily make something like this happen with csf, excuse the formatting I'm just doing this from memory.

const meta = {
   title: "Button",
   component: Button,
}

export const Story1 = {
  args: { 
    text: "text1"
  }
}

export const Story2 = {
  args: { 
    text: "text2"
  }
}

export const AllStory = {
  render ()=>{
    return (<>
      <Button {...Story1.args}>
      <Button {...Story2.args}>
   </>)
 }
}
sregg commented 11 months ago

Nice! I didn't know about this render() function 🎉 Looks like we could automatize this though.

import React from 'react';

import { CookStats } from './CookStats';

import type { Meta, StoryObj } from '@storybook/react-native';
import type { CookStatsProps } from './types';
import { InlineStorySectionHeader } from '~/storybook/components/InlineStorySectionHeader';

const CookStatsMeta = {
  title: 'Components/CookStats',
  component: CookStats,
  args: {
    size: 'large',
    distanceFromCustomer: '3.8mi',
  },
  argTypes: {
    size: {
      type: { name: 'enum', value: ['small', 'large'] },
    },
  },
} as Meta<CookStatsProps>;

export default CookStatsMeta;

export const WithEverything: StoryObj<CookStatsProps> = {
  args: {
    orderCount: 1126,
    averageRating: 4.7,
    ratingCount: 304,
  },
};

export const NoOrderCount: StoryObj<CookStatsProps> = {
  args: {
    averageRating: 4.2,
    ratingCount: 304,
  },
};

export const NoRating: StoryObj<CookStatsProps> = {
  args: {
    orderCount: 19,
  },
};

export const NoOrderCountNoRating: StoryObj<CookStatsProps> = {
  args: {},
};

export const All: StoryObj<CookStatsProps> = {
  render: () => (
    <>
      <InlineStorySectionHeader title="WithEverything" />
      <CookStats
        {...(CookStatsMeta.args as CookStatsProps)}
        {...WithEverything.args}
      />
      <InlineStorySectionHeader title="NoOrderCount" />
      <CookStats
        {...(CookStatsMeta.args as CookStatsProps)}
        {...NoOrderCount.args}
      />
      <InlineStorySectionHeader title="NoRating" />
      <CookStats
        {...(CookStatsMeta.args as CookStatsProps)}
        {...NoRating.args}
      />
      <InlineStorySectionHeader title="NoOrderCountNoRating" />
      <CookStats
        {...(CookStatsMeta.args as CookStatsProps)}
        {...NoOrderCountNoRating.args}
      />
    </>
  ),
};
image
sregg commented 11 months ago

I can create a util function that I can reuse in my project:


export function renderAllStories<T>(meta: Meta<T>, stories: StoryObj<T>[]) {
  return (
    <>
      {stories.map((story, index) => (
        <Fragment key={index}>
          <InlineStorySectionHeader title={story.name ?? ''} />
          {meta.component && <meta.component {...meta.args} {...story.args} />}
        </Fragment>
      ))}
    </>
  );
}

and use it like so:

export const All: StoryObj<CookStatsProps> = {
  render: () =>
    renderAllStories(CookStatsMeta, [
      WithEverything,
      NoOrderCount,
      NoRating,
      NoOrderCountNoRating,
    ]),
};

Small question: how does Storybook get the name of the story objects (e.g. WithEverything)?

dannyhw commented 11 months ago

when you import/require a module it has all the named exports and the default export in there like

module.exports = {
 Name1: { stuff here}
default: {meta stuff here}
}

so when you import that file its like

const file = require("location")

this file has all of those things

so yeah you could actually do this more automated yet probably by getting the current module and looping through it

dannyhw commented 11 months ago

@sregg since you can get the current module from just accessing "module", you should be able to do this:

export const AllStory = {
  render: () => {
    return (
      <>
        {Object.entries(module.exports)
          .filter(([key, _val]) => key !== "default")
          .map(([key, val]) => {
            if (val.args) {
              return <Icon key={key} {...val.args} />;
            }
          })}
      </>
    );
  },
};
sregg commented 10 months ago

Sounds good. I'll see if I have time to build that into this package itself. Maybe with a showAllStoriesStory flag in Meta.