Skip to content

Commit

Permalink
fix: Nuxt App created only when story vue app rendered first time, (#830
Browse files Browse the repository at this point in the history
)

<!--
☝️ PR title should follow conventional commits
(https://conventionalcommits.org).
In particular, the title should start with one of the following types:

- docs: 📖 Documentation (updates to the documentation or readme)
- fix: 🐞 Bug fix (a non-breaking change that fixes an issue)
- feat: ✨ New feature/enhancement (a non-breaking change that adds
functionality or improves existing one)
- feat!/fix!: ⚠️ Breaking change (fix or feature that would cause
existing functionality to change)
- chore: 🧹 Chore (updates to the build process or auxiliary tools and
libraries)
-->

### 🔗 Linked issue

<!-- If it resolves an open issue, please link the issue here. For
example "Resolves #123" -->

### 📚 Description

The issue arose because, in the setup function, the code was checking if
the Nuxt context with storyContext.id was already used by the Nuxt app.
This prevented the recreation of the Nuxt app, which led to a bug where
revisiting the same story caused its Vue app to lack a Nuxt instance.

The bug specifically occurred when using modules that register plugins
with the Vue app, such as i18n. On the first render, the story was
correctly displayed with the registered plugin (e.g., translations).
However, on subsequent renders, the Vue app couldn't find the registered
plugin.

Solution:
Use canvasElement.id as a unique key for differentiation.
Create a new Nuxt app each time a story’s Vue app is rendered.
Remove the check for context.tryUse() since a new Vue app is always
created and mounted for each story.
This ensures that every story render gets a fresh and fully initialized
Nuxt app.

Changes:
I added i18n  example to playground and example/showcase
I configured Vite bundler to alias "vue" to
"vue/dist/vue.esm-bundler.js, to solve the runtime compilation issue for
custom string template
  • Loading branch information
chakAs3 authored Dec 28, 2024
1 parent c429594 commit 74cb005
Show file tree
Hide file tree
Showing 15 changed files with 1,264 additions and 81 deletions.
36 changes: 36 additions & 0 deletions examples/showcase/components/MyI18n.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Meta, StoryObj } from '@storybook/vue3'

import MyComponent from './MyI18n.vue'

// More on how to set up stories at: https://storybook.js.org/docs/vue/writing-stories/introduction

const meta = {
title: 'Modules/I18n',
component: MyComponent,
argTypes: {
lang: { control: 'select', options: ['en', 'fr', 'ar'] },
},

// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/vue/writing-docs/autodocs
tags: ['autodocs'],
} satisfies Meta<typeof MyComponent>

export default meta
type Story = StoryObj<typeof meta>
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/vue/api/csf
* to learn how to use render functions.
*/

export const FrenchStory: Story = {
args: { lang: 'fr' },
}

export const EnglishStory: Story = {
args: { lang: 'en' },
}

export const ArabicStory: Story = {
args: { lang: 'ar' },
}
47 changes: 47 additions & 0 deletions examples/showcase/components/MyI18n.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import '../assets/button.css'
const props = defineProps<{
lang: string
message?: string
}>()
const { t, locale } = useI18n()
const rtl = computed(() => props.lang === 'ar')
locale.value = props.lang
watch(
() => props.lang,
(lang: unknown) => {
locale.value = lang
},
)
</script>

<template>
<div class="storybook lang-selector">
<button
class="storybook-button storybook-button--small"
@click="locale = 'en'"
>
en
</button>
<button
class="storybook-button storybook-button--small"
@click="locale = 'fr'"
>
fr
</button>
<button
class="storybook-button storybook-button--small"
@click="locale = 'ar'"
>
ar
</button>
</div>
<div class="storybook welcome" :style="{ direction: rtl ? 'rtl' : 'ltr' }">
<div>{{ t('welcome', { name: 'Storybook' }) }}</div>
<div>{{ t('welcome', { name: 'Nuxt' }) }}</div>
<div>{{ t('welcome', { name: 'I18n' }) }}</div>
</div>

<p>language : {{ lang }}</p>
</template>
19 changes: 19 additions & 0 deletions examples/showcase/components/MyNuxtWelcome.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/vue3'

import MyNuxtWelcome from './MyWelcome.vue'

// More on how to set up stories at: https://storybook.js.org/docs/vue/writing-stories/introduction

const meta = {
title: 'Example/NuxtWelcome',
component: MyNuxtWelcome,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/vue/writing-docs/autodocs
tags: ['autodocs'],
} satisfies Meta<typeof MyNuxtWelcome>

export default meta
type Story = StoryObj<typeof meta>

export const NuxtWelcomeStory: Story = {
args: {},
}
40 changes: 40 additions & 0 deletions examples/showcase/components/MyWelcome.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<h1>Welcome Nuxt to Storybook</h1>
<NuxtWelcome />
</template>

<style>
.readmore {
-webkit-text-size-adjust: 100%;
tab-size: 4;
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e0e0e0;
--tw-ring-inset: var(--tw-empty,);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(14, 165, 233, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
font-family:
'Nunito Sans',
-apple-system,
'.SFNSText-Regular',
'San Francisco',
BlinkMacSystemFont,
'Segoe UI',
'Helvetica Neue',
Helvetica,
Arial,
sans-serif;
margin: 0;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
line-height: 24px;
color: #029cfd;
text-decoration: none;
font-size: 14px;
}
</style>
16 changes: 16 additions & 0 deletions examples/showcase/i18n.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
defaultLocale: 'en',
messages: {
en: {
welcome: 'Welcome to Storybook ❤️ {name} ',
},
fr: {
welcome: 'Bienvenue a Storybook ❤️ {name} ',
},
ar: {
welcome: ' ناكست ❤️ {name} ❤️ مرحبا بكم في ستوري بوك ',
},
},
}))
7 changes: 6 additions & 1 deletion examples/showcase/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['@nuxtjs/storybook', '@nuxt/image', '@pinia/nuxt'],
modules: ['@nuxtjs/storybook', '@nuxt/image', '@pinia/nuxt', '@nuxtjs/i18n'],

pinia: {
autoImports: ['defineStore', 'acceptHMRUpdate'],
},

i18n: {
locales: ['en', 'fr', 'ar'],
defaultLocale: 'en',
},

imports: {
dirs: ['./stores'],
},
Expand Down
3 changes: 2 additions & 1 deletion examples/showcase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"nuxt": "3.14.1592",
"pinia": "2.3.0",
"vue": "3.5.13",
"vue-router": "4.5.0"
"vue-router": "4.5.0",
"@nuxtjs/i18n": "8.3.1"
},
"devDependencies": {
"@chromatic-com/storybook": "3.2.3",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"playground:storybook:dev": "pnpm run --filter=./playground/** storybook",
"playground:storybook:build": "cd playground && pnpm run build-storybook",
"playground:storybook:publish": "chromatic --exit-zero-on-changes --build-script-name playground:storybook:build --project-token=chpt_d7cf5e98426e11e",
"example:starter:dev": "pnpm run --filter=./examples/starter/** examples/starter dev",
"example:starter:dev": "pnpm run --filter=./examples/starter/** dev",
"example:starter:build": "pnpm run --filter=./examples/starter/** build",
"example:starter:storybook:build": "pnpm run --filter=./examples/starter/** build-storybook",
"example:starter:storybook:publish": "chromatic --exit-zero-on-changes --build-script-name example:starter:storybook:build --project-token=chpt_dc04103f8a32bfa",
Expand Down
9 changes: 4 additions & 5 deletions packages/storybook-addon/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,17 @@ setup(async (vueApp, storyContext) => {
// This is not totally correct, since the storybook vue renderer actually uses the canvas element
// Also this doesn't allow to "forceRemount"
// TODO: Improve this (needs PR to storybook to pass the necessary infos to this function)
const key = storyContext?.id

// use storyContext.canvasElement.id as key as it's unique for each rendered story
// storyContext.id is same for 2 stories in Docs mode, Primary story and the first story in stories are the same story and have the same id
const key = storyContext?.canvasElement.id
if (!key) {
throw new Error('StoryContext is not provided')
}

// Create a new nuxt app for each story
const storyNuxtAppId = `nuxt-app-${key}`
const storyNuxtCtx = getContext(storyNuxtAppId)
if (storyNuxtCtx.tryUse()) {
// Nothing to do, the Nuxt app is already created
return
}

// Provide the config of the Nuxt app
// @ts-expect-error internal Nuxt property
Expand Down
16 changes: 16 additions & 0 deletions playground/i18n.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
defaultLocale: 'en',
messages: {
en: {
welcome: 'Welcome to Storybook ❤️ {name} ',
},
fr: {
welcome: 'Bienvenue a Storybook ❤️ {name} ',
},
ar: {
welcome: ' ناكست ❤️ {name} ❤️ مرحبا بكم في ستوري بوك ',
},
},
}))
11 changes: 10 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['../packages/nuxt-module/src/module', '@nuxt/test-utils/module'],
modules: [
'../packages/nuxt-module/src/module',
'@nuxt/test-utils/module',
'@nuxtjs/i18n',
],

storybook: {
// Very verbose logs for debugging
logLevel: Number.POSITIVE_INFINITY,
},

i18n: {
locales: ['en', 'fr', 'ar'],
defaultLocale: 'en',
},

compatibilityDate: '2024-08-03',
})
3 changes: 2 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"nuxt": "3.14.1592",
"vue": "3.5.13",
"storybook": "8.4.7",
"vite-plugin-inspect": "0.10.3"
"vite-plugin-inspect": "0.10.3",
"@nuxtjs/i18n": "^8.3.1"
}
}
36 changes: 36 additions & 0 deletions playground/stories/MyI18n.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Meta, StoryObj } from '@storybook/vue3'

import MyComponent from './MyI18n.vue'

// More on how to set up stories at: https://storybook.js.org/docs/vue/writing-stories/introduction

const meta = {
title: 'Plugins/I18n',
component: MyComponent,
argTypes: {
lang: { control: 'select', options: ['en', 'fr', 'ar'] },
},

// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/vue/writing-docs/autodocs
tags: ['autodocs'],
} satisfies Meta<typeof MyComponent>

export default meta
type Story = StoryObj<typeof meta>
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/vue/api/csf
* to learn how to use render functions.
*/

export const FrenchStory: Story = {
args: { lang: 'fr' },
}

export const EnglishStory: Story = {
args: { lang: 'en' },
}

export const ArabicStory: Story = {
args: { lang: 'ar' },
}
47 changes: 47 additions & 0 deletions playground/stories/MyI18n.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import './button.css'
const props = defineProps<{
lang: string
message?: string
}>()
const { t, locale } = useI18n()
const rtl = computed(() => props.lang === 'ar')
locale.value = props.lang
watch(
() => props.lang,
(lang: unknown) => {
locale.value = lang
},
)
</script>

<template>
<div class="storybook lang-selector">
<button
class="storybook-button storybook-button--small"
@click="locale = 'en'"
>
en
</button>
<button
class="storybook-button storybook-button--small"
@click="locale = 'fr'"
>
fr
</button>
<button
class="storybook-button storybook-button--small"
@click="locale = 'ar'"
>
ar
</button>
</div>
<div class="storybook welcome" :style="{ direction: rtl ? 'rtl' : 'ltr' }">
<div>{{ t('welcome', { name: 'Storybook' }) }}</div>
<div>{{ t('welcome', { name: 'Nuxt' }) }}</div>
<div>{{ t('welcome', { name: 'I18n' }) }}</div>
</div>

<p>language : {{ lang }}</p>
</template>
Loading

0 comments on commit 74cb005

Please sign in to comment.