JesusTheHun / storybook-addon-remix-react-router

Use your app router in your stories. A decorator made for Remix React Router and Storybook
Apache License 2.0
45 stars 11 forks source link

Feature request: arbitrary layout routes #13

Closed ivanjonas closed 11 months ago

ivanjonas commented 1 year ago

Perhaps I'll turn this into a PR, but for time reasons I've had to simply code up my own solution. I'd like to share that in case anyone wants to contribute this as a feature before I can get to it.

My app is making heavy use of layout routes. The stories I'm writing need to be able to specify layouts around the story. Unfortunately, this can't currently be accomplished with storybook-addon-react-router-v6, but I think it could be done in the same way I did it. Note that my solution is simple, so it omits all of the other parameters that make this addon so helpful for other stories.

import { DecoratorFn } from '@storybook/react';
import { MemoryRouter, Route, Routes } from 'react-router';

import { Base } from '../lib/page/base';

export const withLayoutRouter: DecoratorFn = (Story, context) => {
  const { layouts, disableLayouts } = context.parameters as LayoutsParameter;

  if (disableLayouts) {
    return <Story />;
  }

  let childRoutes = <Route index element={Story()} />;
  if (layouts) {
    // Create nested `<Route>`s for each Layout component in the array
    childRoutes = layouts.reduceRight((inner, next) => <Route element={next}>{inner}</Route>, childRoutes);
  }

  return (
    <MemoryRouter initialEntries={['/']}>
      <Routes>
        <Route path="/" element={<Base />}>
          {childRoutes}
        </Route>
      </Routes>
    </MemoryRouter>
  );
};

export interface LayoutsParameter {
  /**
   * React Router pathless layout route elements that should wrap the components in your story.
   *
   * @type {React.ReactNode[]}
   * @memberof LayoutsParameter
   */
  layouts?: React.ReactNode[];
  /**
   * If true, disables the entire route tree and simply renders the Story. Convenient for taking full control over a particular story.
   *
   * @type {boolean}
   * @memberof LayoutsParameter
   */
  disableLayouts?: boolean;
}

So, in a stories.tsx file, you can call the decorator and specify which layouts to render and in what order:

export default {
  title: 'myComponent',
  component: MyComponent,
  decorators: [withLayoutRouter],
} as ComponentMeta<typeof MyComponent>;

const Template: ComponentStory<typeof MyComponent> = (args) => <MyComponent {...args} />;

export const DefaultState = Template.bind({});
DefaultState.parameters = {
  layouts: [<LayoutA/>, <LayoutB/>],
};

And this will ultimately render a component tree like this:

<MemoryRouter initialEntries={['/']}>
  <Routes>
    <Route path="/" element={<Base />}>
      <Route element={<Layout1 />}>
        <Route element={<Layout2 />}>
          <Route path="/" element={<DefaultStateStory/>} />
        </Route>
      </Route>
    </Route>
  </Routes>
</MemoryRouter>

Please let me know if anything is unclear, if you like the idea, etc. I hope to create a PR, and if you'd like that please let me know if you have any contribution guidelines. Thanks!

JesusTheHun commented 1 year ago

Hi 👋 thank you for opening an issue.

If I understand correctly it's basically the inverse of the outlet property of this plugin, correct ? Instead of defining the outlet of the story you define the parent routes outlets.

This can be added. It would also be nice to add support for multiple child outlets in the same PR.

So yes, please open a PR for this. Add two properties :

Theoutlets property is just like outlet but supports an array. We will remove the singular property on the next major release.

The layouts property is an array. If the property is nullish or the length of the array is 0, then ignore it.

No contribution guideline, just do your best and try to mimic the codes style around you.

Thanks!

ivanjonas commented 1 year ago

Just wanted to drop by and say that I've had this tab open for a couple months with the intent of doing it. Currently very busy but hoping to contribute in the next couple months 🤞

JesusTheHun commented 11 months ago

This is now supported out of the box : https://github.com/JesusTheHun/storybook-addon-react-router-v6/releases/tag/v2.0.0-rc.0

ivanjonas commented 11 months ago

Nice! Thanks for that.

BrunoQuaresma commented 4 months ago

Hey @JesusTheHun is this documented anywhere? I didn't find an easy way to define a layout for a route.

JesusTheHun commented 4 months ago

@BrunoQuaresma you can do this by manually defining the routes or by using a routing helper

BrunoQuaresma commented 4 months ago

Hm... I tried to figure out a way to wrap a Story with two nested layouts without success using the helpers. Would you have any easy examples of how to achieve that using them?

JesusTheHun commented 4 months ago

@BrunoQuaresma you can find all the stories used in the tests of this addon here : https://github.com/JesusTheHun/storybook-addon-remix-react-router/blob/main/src/stories/v2.

In your case, if I'm not mistaken, you are looking at nested ancestors :

export const RoutingNestedAncestors = {
  render: ({ title }) => {
    const [count, setCount] = useState(0);

    return (
      <section>
        <h1>{title}</h1>
        <button onClick={() => setCount((count) => count + 1)}>Increase</button>
        <div role={'status'}>{count}</div>
      </section>
    );
  },
  args: {
    title: 'Story',
  },
  parameters: {
    reactRouter: reactRouterParameters({
      routing: reactRouterNestedAncestors([
        <>
          <p>Ancestor level 1</p>
          <Outlet />
        </>,
        <>
          <p>Ancestor level 2</p>
          <Outlet />
        </>,
        <>
          <p>Ancestor level 3</p>
          <Outlet />
        </>,
      ]),
    }),
  },
} satisfies StoryObj<{ title: string }>;