storybook icon indicating copy to clipboard operation
storybook copied to clipboard

Allow pages to be added to stories.

Open dankellett opened this issue 5 years ago • 23 comments

Is your feature request related to a problem? Please describe.

We often use storybook stories to allow users to see built out pages using components.

Describe the solution you'd like

Allow pages in the Nuxt Pages directory to be added to stories.

Describe alternatives you've considered

Make copies of the pages as components.

Additional context

dankellett avatar Oct 30 '20 15:10 dankellett

Guys, I am also trying to find a way to expose my page component but no success till now.

I've posted it in the Discord channel, any updates I will share it here =)

Hey Guys, I am trying to add my pages components to Storybook as I have my design system as well, and I am wondering how to accomplish that! Do you guys have any working example? The issues that I am currently facing are:

  • How can I mock the data used by my Vuex? I see that it's available with initial data, but I would need to add some mock and even better to let user update it through the ControlPanel
  • It act as if my plugins doesn't exists. For example, I have a repository in place that I access through the NuxtContext from the composition api const { $repository } => useContext() but inside of storybook it says Cannot read property '$repository' of undefined - maybe it would be an issue with @nuxjs/composition-api and not the plugin itself?
  • Also, for some weird reason, everything I expose to the template from the setup method, is actually not available in the template

Good to mention that I am using latest version of everything (nuxt, storybook, composition-api) and also TypeScript.

Any help/hint is appreciated

bissolli avatar Nov 04 '20 09:11 bissolli

Guys, any idea on that?!

To be able to add my pages to Storybook I was thinking of having 2 components to compose a page like described below:

/pages/basket/Main.vue --> not exposed to storybook and the main file loaded by my router system

<template>
  <!-- Probably not much template will be left for this component -->
  <my-main-inner ...many-props />
</template>

<script>
// all the logic that cannot be handled by Storybook
// this will do all the needed communication with the MainInner through props and events
</script>

/pages/basket/MainInner.vue --> exposed to storybook and only called by the Main.vue component

<template>
  <div>Whatever I have to do here</div>
</template>

<script>
// This will be a 'dump-component' and all the this that cannot be handled by Storybook
// will be injected from the `Main.vue`
</script>

Any feedback on it?

bissolli avatar Nov 15 '20 07:11 bissolli

Trying to do the same, but could not have luck.

I also tried adding to nuxt.config.js

  storybook: {
    stories: [
      '../../components/**/*.stories.@(ts|js)',
      '../../pages/**/*.stories.@(ts|js)']
  }

It doesn't help.

xinbenlv avatar Nov 25 '20 02:11 xinbenlv

Any feedback on it?

Having a Props-based Page-like component that gets hydrated by storybook stories with mock-data and by nuxt's page component with real data is my go-to strategy as well. I think it's clean, it works well and it's straightforward. Did you follow up with it? I'm going to implement it in a project real soon

Rigo-m avatar Apr 22 '21 15:04 Rigo-m

Guys, any idea on that?!

To be able to add my pages to Storybook I was thinking of having 2 components to compose a page like described below:

/pages/basket/Main.vue --> not exposed to storybook and the main file loaded by my router system

<template>
  <!-- Probably not much template will be left for this component -->
  <my-main-inner ...many-props />
</template>

<script>
// all the logic that cannot be handled by Storybook
// this will do all the needed communication with the MainInner through props and events
</script>

/pages/basket/MainInner.vue --> exposed to storybook and only called by the Main.vue component

<template>
  <div>Whatever I have to do here</div>
</template>

<script>
// This will be a 'dump-component' and all the this that cannot be handled by Storybook
// will be injected from the `Main.vue`
</script>

Any feedback on it?

The naming stinks Main and MainInner :(

blocka avatar May 11 '21 14:05 blocka

The naming stinks Main and MainInner :(

IndexPage.vue -> Page component Index.vue -> route

BlogPostPage.vue -> Page component blog/_post.vue -> route

My "page components" live inside the components/pages directory, each page component accept a pageData prop, and each page component gets called by one route only. This way I can hydrate the pageData prop both by a story and from the actual data that I gather via asyncData or fetch inside the nuxt route component. Let me know if this sounds good to you guys

Rigo-m avatar May 11 '21 14:05 Rigo-m

What about the layout? Or you're just fine with stopping at the page.

I once tried to go down the rabbit hole of trying to figure out how to render the App.js file generated by nuxt in storybook, but didn't get very far.

blocka avatar May 11 '21 14:05 blocka

What do you mean by layout?

And why would you render the App.js file? Storybook is meant to validate and showcase your components/pages, it shouldn't be able to tinker with your entire application.

Rigo-m avatar May 12 '21 06:05 Rigo-m

In my regular vue applications (that i'd use for things like admin sections, etc.) I have top-level stories in which I mock the router and show the entire page as it would be rendered in the browser (so you'd see the header, menus, etc., in addition to whatever that route would render)

blocka avatar May 12 '21 06:05 blocka

Which, in my opinion, should be a page component + a global decorator with whatever the layout has (e.g: menu, footer etc)

Rigo-m avatar May 12 '21 07:05 Rigo-m

what do you do in the decorator...mock the <Nuxt /> component so that it shows the given page?

blocka avatar May 12 '21 07:05 blocka

I decorate the page with whatever component would be in the default.vue layout

Rigo-m avatar May 12 '21 07:05 Rigo-m

Not sure I understand you:

That component usually looks like

<div><Nuxt/></div>

how exactly are you using it to "decorate"?

blocka avatar May 12 '21 07:05 blocka

My layout: <div> <Menu/> <Nuxt /> <Footer /></div> My route (let's say, index.vue): <IndexPage /> My page component (IndexPage.vue): <div> <ComponentA /> <ComponentB /></div>

My IndexPage story:

import IndexPage from '~/components/IndexPage'

// Mimics the layout behaviour
const layoutDecorator = (story) => ({
  components: { story },
  template: `
    <div> <Menu/> <story /> <Footer /></div>
  `
})

export default {
  title: 'IndexPage',
  decorators: [layoutDecorator]
}

const Template = (args, { argTypes }) => ({
  components: IndexPage,
  template: '<IndexPage />'
})

export const Page = Template.bind({})

Rigo-m avatar May 12 '21 08:05 Rigo-m

ok...but you're duplicating your whole layout in the story...

blocka avatar May 12 '21 08:05 blocka

Yes. But as I said, layout behaviour should be separate from stories. Stories needs to be agnostic and introducing a ubercharged dynamic component like the <Nuxt /> component is bad practice in my opinion. Also, you can export the layoutDecorator from a decorators.js file and import it in your page's stories. So you will only write layoutDecorator once

Rigo-m avatar May 12 '21 08:05 Rigo-m

My suggestion was have the decorator replace the <Nuxt /> component with , basically...not to use it as is

So you will only write layoutDecorator once

I don't mean copying the layoutDecorator code...I mean duplicating the layout itself...if you change the layout code you'd have to change the decorator.

I suppose a potential way around that is to make "yet another" component for your layout which accepts a slot

so

// layouts/default.vue
<DefaultLayout><Nuxt /></DefaultLayout>
// components/layouts/default-layout.vue
<div><Menu/><slot /><Footer /></div>
// decarator 
const layoutDecorator = (story) => ({
  components: { story, DefaultLayout },
  template: `
    <DefaultLayout> <story /> <DefaultLayout>
  `
})

still, I miss the ability to just tell storybook "render me whatever is at router /foo/bar/23"

blocka avatar May 12 '21 08:05 blocka

Your solution is better.

If you want to render whatever is at router /foo/bar/23 run the nuxt sever 😅 If you want to use storybook as it's meant to be used (with mocked data, component statuses etc) the way we paved in the comments above is the right way IMHO.

Rigo-m avatar May 12 '21 08:05 Rigo-m

If you want to render whatever is at router /foo/bar/23 run the nuxt sever

without talking to a server...all data mocked with msw.

blocka avatar May 12 '21 08:05 blocka

Then again, I stand by my point. Using components composition and building stories on top of agnostic components (that can both work alone and inside nuxt supercharged components) is the way to go.

Rigo-m avatar May 12 '21 08:05 Rigo-m

I ended up with the solution based on @blocka's suggestion. I created a global decorator with the layout name-to-component map. As long as I declare the page component in the story's component prop it should get the layout value from the page component and map it to the appropriate layout component.

// preview.js
const layoutDecorator = (story, { parameters }) => {
  const layouts = {
    'my-custom': () => import('~/components/layout/MyCustomlayout.vue'),
  }
  const layoutFallbackComponent = {
    template: '<div class="layout-not-found"><slot /></div>',
  }
  const pageLayout = parameters.component?.options?.layout
  const LayoutComponent = layouts[pageLayout] ?? layoutFallbackComponent
  return {
    components: { story, LayoutComponent },
    template: '<LayoutComponent><story /></LayoutComponent>',
  }
}
export const decorators = [layoutDecorator]
// some.story.js
export default {
  title: 'pages/some-page',
  component: SomePage,
}

The downside is that you still have to maintain the layout map, but at least it's a simple key/value and could probably be extracted from the layouts directory by reading the raw files to go a step beyond.

tyom avatar Jul 12 '21 16:07 tyom

I could simulate pages on storybook to replace render() of nuxt component with a function which returns default slot of layout component.

/** .storybook/preview.js */

import Vue from 'vue'

Vue.component('nuxt', {
  render() {
    // this.$root.$children[0].$children[0] is <layout-component />
    return this.$root.$children[0].$children[0].$slots.default
  }
})

export * from '../.nuxt-storybook/storybook/preview.js'



/** pages/foo/bar.stories.ts */

import { defineComponent, useRouter } from '@nuxtjs/composition-api'

import PageComponent from '@/pages/foo/bar.vue'
const LayoutComponent = require(`@/layouts/${PageComponent.layout}.vue`).default

export default {
  title: 'pages/foo/bar'
  component: PageComponent
}

export const FooBar = () => defineComponent({
  components: { LayoutComponent, PageComponent },
  template: '<layout-component><page-component /><layout-component>'
})

But I could not deal with pages which can be shown after login process because a redirection after login avoided to display an expected pages.

I ended up with the solution not to use slot of layout component but to make a transition to target pages with router.push().

/** .storybook/preview.js */

export * from '../.nuxt-storybook/storybook/preview.js'


/** pages/foo/bar.stories.ts */

import { defineComponent, useRouter } from '@nuxtjs/composition-api'

import PageComponent from '@/pages/foo/bar.vue'
const LayoutComponent = require(`@/layouts/${PageComponent.layout}.vue`).default

export default {
  title: 'pages/foo/bar'
  component: PageComponent
}

export const FooBar = () => defineComponent({
    components: { LayoutComponent },
    template: '<layout-component v-if="!fetchState.pending" />'
    setup: () => {
      const router = useRouter()
      const { $auth } = useContext()
      const { fetch, fetchState } = useFetch(async () => {
        await $auth.logout()
        await $auth.loginWith('local', {userId: 'xxx', password: 'xxx'}) // I use msw to simulate mock api server.
        router.push({ path: '/foo/bar', query:  { id: '1' } })
      })
      fetch()
      return { fetchState }
    }
})

ishikawa-yuya avatar Sep 21 '21 16:09 ishikawa-yuya

Thanks to the @ishikawa-yuya-functional-inc comment above, I kinda manage to add pages to my storybook. However, I made a small change to the render function:

import Vue from 'vue';
// My auth pages layout
import auth from '@/layouts/auth';

// Override the Nuxt component
Vue.component('Nuxt', {
  render() {
    return this.$parent.$slots.default;
  },
});

export default {
  title: 'Pages/Login',
  decorators: [() => ({
    components: {
      auth,
    },
    // Surround stories with the layout
    template: '<auth><story /></auth>',
  })],
};

GMartigny avatar Feb 17 '22 14:02 GMartigny