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

fix: delay patching until first dom render #510

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react/test/ReactiveTitle.test.tsx
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ describe('unheadProvider', () => {
getByText('Update').click()
})

await new Promise(resolve => setTimeout(resolve, 10))

expect(getByText('Updated')).toBeDefined()
const res = await renderSSRHead(head)
expect(res.headTags).toEqual(`<title>Updated</title>`)
5 changes: 5 additions & 0 deletions packages/react/test/useSeoMeta.test.tsx
Original file line number Diff line number Diff line change
@@ -60,6 +60,9 @@ describe('useSeoMeta hook', () => {
const input = getByRole('textbox') as HTMLInputElement
fireEvent.change(input, { target: { value: 'Updated Title' } })

// wait for second update
await new Promise(resolve => setTimeout(resolve, 10))

const { headTags } = await renderSSRHead(head)
expect(headTags).toContain('<title>Updated Title</title>')
})
@@ -86,6 +89,8 @@ describe('useSeoMeta hook', () => {
</UnheadProvider>,
)

await new Promise(resolve => setTimeout(resolve, 10))

const { headTags } = await renderSSRHead(head)
expect(headTags).toContain('<title>Updated Title</title>')
})
6 changes: 5 additions & 1 deletion packages/unhead/src/types/head.ts
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ export interface ActiveHeadEntry<Input> {
*
* Will first clear any side effects for previous input.
*/
patch: (input: Input) => void
patch: (input: Input, force?: boolean) => void
/**
* Dispose the entry, removing it from the active head.
*
@@ -91,6 +91,10 @@ export interface ActiveHeadEntry<Input> {
* @internal
*/
_poll: (rm?: boolean) => void
/**
* Hook side effect persisted for deduping and clean up.
*/
_h?: () => void
}

export type PropResolver = (key: string, value: any, tag?: HeadTag) => any
11 changes: 9 additions & 2 deletions packages/unhead/src/unhead.ts
Original file line number Diff line number Diff line change
@@ -73,17 +73,24 @@ export function createUnhead<T = ResolvableHead>(resolvedOptions: CreateHeadOpti
if (entries.delete(_i)) {
_._poll(true)
}
_._h?.()
},
// a patch is the same as creating a new entry, just a nice DX
patch(input) {
patch(input, force = false) {
if (!options.mode || (options.mode === 'server' && ssr) || (options.mode === 'client' && !ssr)) {
if (!ssr && !force && !head._dom) {
// dom has not been rendered we need to queue the patch so that removals will work
_._h?.()
_._h = head.hooks.hookOnce('dom:rendered', () => _.patch(input, true))
return
}
inst.input = input
entries.set(_i, inst)
_._poll()
}
},
}
_.patch(input)
_.patch(input, true)
return _
},
async resolveTags() {
37 changes: 36 additions & 1 deletion packages/vue/test/unit/dom/classes.test.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
import { renderDOMHead } from '@unhead/dom'
import { useHead } from '@unhead/vue'
import { describe, it } from 'vitest'
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useDom } from '../../../../unhead/test/fixtures'
import { csrVueAppWithUnhead } from '../../util'

@@ -43,4 +43,39 @@ describe('vue dom classes', () => {
<body class="active-navbar-body"><div id="app" data-v-app=""><div>hello world</div></div></body></html>"
`)
})
it('toggle class', async () => {
const dom = useDom()
dom.window.document.body.className = 'loading'

const isLoaded = ref(false)
const head = csrVueAppWithUnhead(dom, () => {
useHead({
bodyAttrs: {
class: computed(() => {
return !isLoaded.value ? 'loading' : ''
}),
},
})
onMounted(() => {
isLoaded.value = true
})
})
await renderDOMHead(head, { document: dom.window.document })

expect(dom.serialize()).toMatchInlineSnapshot(`
"<html><head>

</head>
<body class="loading"><div id="app" data-v-app=""><div>hello world</div></div></body></html>"
`)

await new Promise(resolve => setTimeout(resolve, 10))
await renderDOMHead(head, { document: dom.window.document })
expect(dom.serialize()).toMatchInlineSnapshot(`
"<html><head>

</head>
<body class=""><div id="app" data-v-app=""><div>hello world</div></div></body></html>"
`)
})
})