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

Allow pages to be added to stories. #159

Open
dankellett opened this issue Oct 30, 2020 · 23 comments · May be fixed by #576
Open

Allow pages to be added to stories. #159

dankellett opened this issue Oct 30, 2020 · 23 comments · May be fixed by #576
Labels
type: enhancement New feature or request

Comments

@dankellett
Copy link

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 dankellett added the type: enhancement New feature or request label Oct 30, 2020
@bissolli
Copy link

bissolli commented Nov 4, 2020

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
Copy link

bissolli commented Nov 15, 2020

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?

@xinbenlv
Copy link

xinbenlv commented Nov 25, 2020

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.

@Rigo-m
Copy link

Rigo-m commented Apr 22, 2021

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

@blocka
Copy link

blocka commented May 11, 2021

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 :(

@Rigo-m
Copy link

Rigo-m commented May 11, 2021

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

@blocka
Copy link

blocka commented May 11, 2021

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.

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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.

@blocka
Copy link

blocka commented May 12, 2021

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)

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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

@blocka
Copy link

blocka commented May 12, 2021

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

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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

@blocka
Copy link

blocka commented May 12, 2021

Not sure I understand you:

That component usually looks like

<div><Nuxt/></div>

how exactly are you using it to "decorate"?

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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({})

@blocka
Copy link

blocka commented May 12, 2021

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

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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

@blocka
Copy link

blocka commented May 12, 2021

My suggestion was have the decorator replace the 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"

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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.

@blocka
Copy link

blocka commented May 12, 2021

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.

@Rigo-m
Copy link

Rigo-m commented May 12, 2021

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.

@tyom
Copy link

tyom commented Jul 12, 2021

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.

@ishikawa-yuya
Copy link

ishikawa-yuya commented Sep 21, 2021

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 }
    }
})

@GMartigny
Copy link

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>',
  })],
};

@danielroe danielroe added the v4 label Sep 11, 2023
@tobiasdiez tobiasdiez removed the v4 label Apr 20, 2024
@tobiasdiez tobiasdiez linked a pull request May 1, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants