Skip to content

Commit c6d8c82

Browse files
committed
feat: improved hooks and DOM triggering
1 parent 64b013a commit c6d8c82

File tree

18 files changed

+191
-93
lines changed

18 files changed

+191
-93
lines changed

packages/dom/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# @unhead/dom
2+
3+
## Install
4+
5+
```bash
6+
# yarn add @unhead/dom
7+
npm install @unhead/dom
8+
```
9+
10+
## Documentation
11+
12+
See the [@unhead/dom](https://unhead.harlanzw.com/guide/getting-started/how-it-works#unheaddom) for how this works.

packages/dom/src/renderDOMHead.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TagsWithInnerContent, createElement } from 'zhead'
2-
import type { DomRenderTagContext, HeadClient } from '@unhead/schema'
2+
import type { BeforeRenderContext, DomRenderTagContext, Unhead } from '@unhead/schema'
33
import { setAttributesWithSideEffects } from './setAttributesWithSideEffects'
44

55
export interface RenderDomHeadOptions {
@@ -12,16 +12,22 @@ export interface RenderDomHeadOptions {
1212
/**
1313
* Render the head tags to the DOM.
1414
*/
15-
export async function renderDOMHead<T extends HeadClient<any>>(head: T, options: RenderDomHeadOptions = {}) {
15+
export async function renderDOMHead<T extends Unhead<any>>(head: T, options: RenderDomHeadOptions = {}) {
1616
const dom: Document = options.document || window.document
1717

1818
const tags = await head.resolveTags()
1919

20-
await head.hooks.callHook('dom:beforeRender', { head, tags, document: dom })
20+
const context: BeforeRenderContext = { shouldRender: true, tags }
21+
await head.hooks.callHook('dom:beforeRender', context)
22+
// allow integrations to block to the render
23+
if (!context.shouldRender)
24+
return
2125

22-
for (const tag of tags) {
23-
const renderCtx: DomRenderTagContext = { tag, document: dom, head }
26+
for (const tag of context.tags) {
27+
const renderCtx: DomRenderTagContext = { shouldRender: true, tag }
2428
await head.hooks.callHook('dom:renderTag', renderCtx)
29+
if (!renderCtx.shouldRender)
30+
return
2531

2632
const entry = head.headEntries().find(e => e._i === Number(tag._e))!
2733

@@ -94,12 +100,13 @@ export let domUpdatePromise: Promise<void> | null = null
94100
/**
95101
* Queue a debounced update of the DOM head.
96102
*/
97-
export async function debouncedRenderDOMHead<T extends HeadClient<any>>(delayedFn: (fn: () => void) => void, head: T, options: RenderDomHeadOptions = {}) {
103+
export async function debouncedRenderDOMHead<T extends Unhead<any>>(head: T, options: RenderDomHeadOptions & { delayFn?: (fn: () => void) => void } = {}) {
98104
// within the debounced dom update we need to compute all the tags so that watchEffects still works
99105
function doDomUpdate() {
100106
domUpdatePromise = null
101107
return renderDOMHead(head, options)
102108
}
103-
104-
return domUpdatePromise = domUpdatePromise || new Promise(resolve => delayedFn(() => resolve(doDomUpdate())))
109+
// we want to delay for the hydration chunking
110+
const delayFn = options.delayFn || (fn => setTimeout(fn, 25))
111+
return domUpdatePromise = domUpdatePromise || new Promise(resolve => delayFn(() => resolve(doDomUpdate())))
105112
}

packages/dom/src/setAttributesWithSideEffects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { HeadClient, HeadEntry, HeadTag } from '@unhead/schema'
1+
import type { HeadEntry, HeadTag, Unhead } from '@unhead/schema'
22

33
/**
44
* Set attributes on a DOM element, while adding entry side effects.
55
*/
6-
export function setAttributesWithSideEffects(head: HeadClient, $el: Element, entry: HeadEntry<any>, tag: HeadTag) {
6+
export function setAttributesWithSideEffects(head: Unhead, $el: Element, entry: HeadEntry<any>, tag: HeadTag) {
77
const attrs = tag.props || {}
88
const sdeKey = `${tag._p}:attr`
99

packages/schema/src/head.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ export type SideEffectsRecord = Record<string, () => void>
1212

1313
export type RuntimeMode = 'server' | 'client' | 'all'
1414

15-
export interface HeadEntry<T> {
15+
export interface HeadEntry<Input> {
1616
/**
1717
* User provided input for the entry.
1818
*/
19-
input: T
19+
input: Input
2020
/**
2121
* The mode that the entry should be used in.
2222
*
@@ -42,13 +42,13 @@ export type HeadPlugin = Omit<CreateHeadOptions, 'plugins'>
4242
/**
4343
* An active head entry provides an API to manipulate it.
4444
*/
45-
export interface ActiveHeadEntry<T> {
45+
export interface ActiveHeadEntry<Input> {
4646
/**
4747
* Updates the entry with new input.
4848
*
4949
* Will first clear any side effects for previous input.
5050
*/
51-
patch: (resolvedInput: T) => void
51+
patch: (input: Input) => void
5252
/**
5353
* Dispose the entry, removing it from the active head.
5454
*
@@ -58,6 +58,8 @@ export interface ActiveHeadEntry<T> {
5858
}
5959

6060
export interface CreateHeadOptions {
61+
domDelayFn?: (fn: () => void) => void
62+
document?: Document
6163
plugins?: HeadPlugin[]
6264
hooks?: NestedHooks<HeadHooks>
6365
}
@@ -66,15 +68,15 @@ export interface HeadEntryOptions {
6668
mode?: RuntimeMode
6769
}
6870

69-
export interface HeadClient<T extends {} = Head> {
71+
export interface Unhead<Input extends {} = Head> {
7072
/**
7173
* The active head entries.
7274
*/
73-
headEntries: () => HeadEntry<T>[]
75+
headEntries: () => HeadEntry<Input>[]
7476
/**
7577
* Create a new head entry.
7678
*/
77-
push: (entry: T, options?: HeadEntryOptions) => ActiveHeadEntry<T>
79+
push: (entry: Input, options?: HeadEntryOptions) => ActiveHeadEntry<Input>
7880
/**
7981
* Resolve tags from head entries.
8082
*/

packages/schema/src/hooks.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { HeadClient, HeadEntry } from './head'
1+
import type { HeadEntry, Unhead } from './head'
22
import type { HeadTag } from './tags'
33

44
export type HookResult = Promise<void> | void
@@ -12,20 +12,22 @@ export interface SSRHeadPayload {
1212
}
1313

1414
export interface EntryResolveCtx<T> { tags: HeadTag[]; entries: HeadEntry<T>[] }
15-
export interface DomRenderTagContext { head: HeadClient; tag: HeadTag; document: Document }
15+
export interface DomRenderTagContext { shouldRender: boolean; tag: HeadTag }
16+
export interface BeforeRenderContext { shouldRender: boolean; tags: HeadTag[] }
17+
export interface SSRRenderContext { tags: HeadTag[]; html: SSRHeadPayload }
1618

1719
export interface HeadHooks {
18-
'init': (ctx: HeadClient<any>) => HookResult
19-
'entries:updated': (ctx: HeadClient<any>) => HookResult
20+
'init': (ctx: Unhead<any>) => HookResult
21+
'entries:updated': (ctx: Unhead<any>) => HookResult
2022
'entries:resolve': (ctx: EntryResolveCtx<any>) => HookResult
2123
'tag:normalise': (ctx: { tag: HeadTag; entry: HeadEntry<any> }) => HookResult
2224
'tags:resolve': (ctx: { tags: HeadTag[] }) => HookResult
2325

2426
// @unhead/dom
25-
'dom:beforeRender': (ctx: { head: HeadClient; tags: HeadTag[]; document: Document }) => HookResult
27+
'dom:beforeRender': (ctx: BeforeRenderContext) => HookResult
2628
'dom:renderTag': (ctx: DomRenderTagContext) => HookResult
2729

2830
// @unhead/ssr
29-
'ssr:beforeRender': (ctx: { tags: HeadTag[] }) => HookResult
30-
'ssr:render': (ctx: { tags: HeadTag[]; html: SSRHeadPayload }) => HookResult
31+
'ssr:beforeRender': (ctx: BeforeRenderContext) => HookResult
32+
'ssr:render': (ctx: SSRRenderContext) => HookResult
3133
}

packages/ssr/src/renderSSRHead.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { ssrRenderTags } from 'zhead'
2-
import type { HeadClient, SSRHeadPayload } from '@unhead/schema'
2+
import type { BeforeRenderContext, SSRHeadPayload, SSRRenderContext, Unhead } from '@unhead/schema'
33

4-
export async function renderSSRHead<T extends {}>(ctx: HeadClient<T>) {
5-
const tags = await ctx.resolveTags()
6-
const beforeRenderCtx = { tags }
7-
await ctx.hooks.callHook('ssr:beforeRender', beforeRenderCtx)
4+
export async function renderSSRHead<T extends {}>(head: Unhead<T>) {
5+
const tags = await head.resolveTags()
6+
const beforeRenderCtx: BeforeRenderContext = { shouldRender: true, tags }
7+
if (!beforeRenderCtx.shouldRender) {
8+
return {
9+
headTags: '',
10+
bodyTags: '',
11+
bodyTagsOpen: '',
12+
htmlAttrs: '',
13+
bodyAttrs: '',
14+
}
15+
}
16+
await head.hooks.callHook('ssr:beforeRender', beforeRenderCtx)
817
const html: SSRHeadPayload = ssrRenderTags(beforeRenderCtx.tags)
9-
const renderCXx = { tags, html }
10-
await ctx.hooks.callHook('ssr:render', renderCXx)
11-
return renderCXx.html
18+
const renderCtx: SSRRenderContext = { tags, html }
19+
await head.hooks.callHook('ssr:render', renderCtx)
20+
return renderCtx.html
1221
}

packages/unhead/src/createHead.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createHooks } from 'hookable'
2-
import type { CreateHeadOptions, Head, HeadClient, HeadEntry, HeadHooks, HeadPlugin, HeadTag, SideEffectsRecord } from '@unhead/schema'
2+
import type { CreateHeadOptions, Head, HeadEntry, HeadHooks, HeadPlugin, HeadTag, SideEffectsRecord, Unhead } from '@unhead/schema'
33
import { setActiveHead } from './runtime/state'
4-
import { DedupesTagsPlugin, DeprecatedTagAttrPlugin, SortTagsPlugin, TitleTemplatePlugin } from './plugin'
4+
import { DedupesTagsPlugin, DeprecatedTagAttrPlugin, PatchDomOnEntryUpdatesPlugin, SortTagsPlugin, TitleTemplatePlugin } from './plugin'
55
import { normaliseEntryTags } from './normalise'
66

77
export function createHead<T extends {} = Head>(options: CreateHeadOptions = {}) {
@@ -11,7 +11,7 @@ export function createHead<T extends {} = Head>(options: CreateHeadOptions = {})
1111
// counter for keeping unique ids of head object entries
1212
let entryId = 0
1313
const hooks = createHooks<HeadHooks>()
14-
if (options.hooks)
14+
if (options?.hooks)
1515
hooks.addHooks(options.hooks)
1616

1717
const plugins: HeadPlugin[] = [
@@ -20,13 +20,14 @@ export function createHead<T extends {} = Head>(options: CreateHeadOptions = {})
2020
DedupesTagsPlugin(),
2121
SortTagsPlugin(),
2222
TitleTemplatePlugin(),
23+
PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn }),
2324
]
2425
plugins.push(...(options.plugins || []))
2526
plugins.forEach(plugin => hooks.addHooks(plugin.hooks || {}))
2627

2728
const triggerUpdate = () => hooks.callHook('entries:updated', head)
2829

29-
const head: HeadClient<T> = {
30+
const head: Unhead<T> = {
3031
_removeQueuedSideEffect(key) {
3132
delete _sde[key]
3233
},

packages/unhead/src/plugin/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './sortTagsPlugin'
33
export * from './titleTemplatePlugin'
44
export * from './deprecatedTagAttrPlugin'
55
export * from './hydratesStatePlugin/hydratesStatePlugin'
6+
export * from './patchDomOnEntryUpdatesPlugin'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { defineHeadPlugin } from '../defineHeadPlugin'
2+
import type { RenderDomHeadOptions } from '@unhead/dom'
3+
4+
interface TriggerDomPatchingOnUpdatesPluginOptions extends RenderDomHeadOptions {
5+
delayFn?: (fn: () => void) => void
6+
}
7+
8+
export const PatchDomOnEntryUpdatesPlugin = (options?: TriggerDomPatchingOnUpdatesPluginOptions) => {
9+
return defineHeadPlugin({
10+
hooks: {
11+
'entries:updated': function (head) {
12+
// only if we have a document, avoid unhead/dom in ssr
13+
if (typeof options?.document === 'undefined' && typeof window === 'undefined')
14+
return
15+
let delayFn = options?.delayFn
16+
if (!delayFn && typeof requestAnimationFrame !== 'undefined')
17+
delayFn = requestAnimationFrame
18+
19+
// async load the renderDOMHead function
20+
import('@unhead/dom').then(({ debouncedRenderDOMHead }) => {
21+
debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn })
22+
})
23+
},
24+
},
25+
})
26+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { HeadClient } from '@unhead/schema'
1+
import type { Unhead } from '@unhead/schema'
22

3-
export let activeHead: HeadClient<any> | undefined
3+
export let activeHead: Unhead<any> | undefined
44

5-
export const setActiveHead = <T extends HeadClient> (head: T | undefined) => (activeHead = head)
5+
export const setActiveHead = <T extends Unhead> (head: T | undefined) => (activeHead = head)
66

7-
export const getActiveHead = <T extends HeadClient> () => activeHead as T
7+
export const getActiveHead = <T extends Unhead> () => activeHead as T

0 commit comments

Comments
 (0)