diff --git a/playground/pages/reactive-head.vue b/playground/pages/reactive-head.vue
new file mode 100644
index 00000000..4789f9dc
--- /dev/null
+++ b/playground/pages/reactive-head.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Reactive head
+
+
+
+
+ Increment
+
+
+ {{ count }}
+
+
+
+
diff --git a/src/runtime/composables/head.ts b/src/runtime/composables/head.ts
index 96bcf666..08e71d21 100644
--- a/src/runtime/composables/head.ts
+++ b/src/runtime/composables/head.ts
@@ -1,7 +1,8 @@
import { onIonViewDidEnter, onIonViewDidLeave } from '@ionic/vue'
import type { ActiveHeadEntry, UseHeadInput, UseHeadOptions } from '@unhead/vue/types'
import type { useHead as _useHead } from '@unhead/vue'
-import { getCurrentInstance, onBeforeUnmount } from 'vue'
+import { VueResolver, walkResolver } from '@unhead/vue/utils'
+import { getCurrentInstance, getCurrentScope, onBeforeUnmount, watchEffect } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { injectHead } from '#imports'
@@ -23,6 +24,13 @@ export function useHead>(obj: UseHeadInput, _?:
const currentPath = (instance && useRoute().path) || ''
let innerObj = obj
+
+ // Reactive input has to be resolved before it reaches unhead, as `clientUseHead` does
+ const resolveInput = (input: UseHeadInput) => walkResolver(input, VueResolver) as UseHeadInput
+
+ // The map keeps the raw input as its key, only what we hand to unhead is resolved
+ const findActiveEntry = () => headMap.get(currentPath)?.find(headVal => headVal[0] === innerObj)?.[1]
+
const __returned: Omit>, '_poll'> = {
dispose() {
// Can just easily mutate the array instead of wasting little CPU to slice/spread it :P
@@ -41,7 +49,7 @@ export function useHead>(obj: UseHeadInput, _?:
if (headArrIndex === -1) return
const [, headToPatch] = headArr[headArrIndex]!
innerObj = newObj
- headToPatch?.patch(innerObj)
+ headToPatch?.patch(resolveInput(innerObj))
headArr.splice(headArrIndex, 1, [innerObj, headToPatch])
headMap.set(currentPath, headArr)
},
@@ -50,15 +58,29 @@ export function useHead>(obj: UseHeadInput, _?:
/* Initially assign the head to the respected slots in the map
because Ionic components don't unmount the way we expect them to */
if (!headMap.has(currentPath)) {
- const headObj = activeHead?.push(obj)
+ const headObj = activeHead?.push(resolveInput(obj))
headMap.set(currentPath, [[obj, headObj]])
}
else {
- const headObj = activeHead?.push(obj)
+ const headObj = activeHead?.push(resolveInput(obj))
const metaArr = headMap.get(currentPath) || []
headMap.set(currentPath, [...metaArr, [obj, headObj]])
}
+ /* Keep the entry in sync with the input, looking it up on each run
+ because `onIonViewDidEnter` disposes and re-pushes it */
+ if (getCurrentScope()) {
+ let isInitialRun = true
+ watchEffect(() => {
+ const resolved = resolveInput(innerObj)
+ if (isInitialRun) {
+ isInitialRun = false
+ return
+ }
+ findActiveEntry()?.patch(resolved)
+ })
+ }
+
// Only use lifecycle hooks if called inside component setup
if (instance) {
const router = useRouter()
@@ -97,7 +119,7 @@ export function useHead>(obj: UseHeadInput, _?:
if (headArr) {
headArr = headArr.map(([obj, head]) => {
head?.dispose()
- const newHead = activeHead?.push(obj)
+ const newHead = activeHead?.push(resolveInput(obj))
return [obj, newHead]
})
headMap.set(currPath, headArr)
diff --git a/test/e2e/ion-head.spec.ts b/test/e2e/ion-head.spec.ts
index 2096826f..b62f0d38 100644
--- a/test/e2e/ion-head.spec.ts
+++ b/test/e2e/ion-head.spec.ts
@@ -1,6 +1,6 @@
import { fileURLToPath } from 'node:url'
import { setup, createPage, url } from '@nuxt/test-utils/e2e'
-import { describe, it } from 'vitest'
+import { describe, expect, it } from 'vitest'
import type { Page } from 'playwright-core'
function expectTitleToBe(page: Page, title: string) {
@@ -76,4 +76,23 @@ describe('Nuxt Ionic useHead', async () => {
await page.close()
})
+
+ it('useHead should resolve reactive input on the client', { timeout: 120_000 }, async () => {
+ const page = await createPage()
+ const errors: string[] = []
+ page.on('pageerror', error => errors.push(error.message))
+
+ await page.goto(url('/reactive-head'), { waitUntil: 'hydration' })
+ await expectTitleToBe(page, 'Reactive Head - 0')
+
+ await page.waitForFunction(() => document.getElementById('reactive-head-style')?.textContent?.includes('--count: 0'))
+
+ await page.click('.reactive-head-increment')
+ await expectTitleToBe(page, 'Reactive Head - 1')
+ await page.waitForFunction(() => document.getElementById('reactive-head-style')?.textContent?.includes('--count: 1'))
+
+ expect(errors).toEqual([])
+
+ await page.close()
+ })
})