Open dankellett opened 3 years ago
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
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?
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.
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
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 :(
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
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.
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.
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)
Which, in my opinion, should be a page component + a global decorator with whatever the layout has (e.g: menu, footer etc)
what do you do in the decorator...mock the
I decorate the page with whatever component would be in the default.vue
layout
Not sure I understand you:
That component usually looks like
<div><Nuxt/></div>
how exactly are you using it to "decorate"?
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({})
ok...but you're duplicating your whole layout in the story...
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
My suggestion was have the decorator replace the
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"
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.
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.
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.
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.
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 }
}
})
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>',
})],
};
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