|
1 | | -import { createElement } from 'zhead' |
2 | | -import type { DomRenderTagContext, HeadClient, SideEffectsRecord } from '../../types' |
3 | | -import { setAttributes } from './setAttributes' |
4 | | - |
5 | | -let domUpdatePromise: Promise<void> | null = null |
| 1 | +import { TagsWithInnerContent, createElement } from 'zhead' |
| 2 | +import type { DomRenderTagContext, HeadClient } from '../../types' |
| 3 | +import { setAttributesWithSideEffects } from './setAttributesWithSideEffects' |
6 | 4 |
|
7 | 5 | export interface RenderDomHeadOptions { |
| 6 | + /** |
| 7 | + * Document to use for rendering. Allows stubbing for testing. |
| 8 | + */ |
8 | 9 | document?: Document |
9 | 10 | } |
10 | 11 |
|
11 | | -export const renderDOMHead = async<T extends HeadClient<any>>(head: T, options: RenderDomHeadOptions = {}) => { |
| 12 | +/** |
| 13 | + * Render the head tags to the DOM. |
| 14 | + */ |
| 15 | +export async function renderDOMHead<T extends HeadClient<any>>(head: T, options: RenderDomHeadOptions = {}) { |
12 | 16 | const dom: Document = options.document || window.document |
13 | 17 |
|
14 | 18 | const tags = await head.resolveTags() |
15 | 19 |
|
16 | 20 | await head.hooks.callHook('dom:beforeRender', { head, tags, document: dom }) |
17 | 21 |
|
18 | | - // start with a clean slate |
19 | | - head._flushDomSideEffects() |
| 22 | + // remove |
| 23 | + head._flushQueuedSideEffectFns() |
20 | 24 |
|
21 | | - const sideEffectMap: Record<number, SideEffectsRecord> = {} |
22 | 25 | // default is to only create tags, not to resolve state |
23 | 26 | for (const tag of tags) { |
24 | | - sideEffectMap[tag._e!] = sideEffectMap[tag._e!] || {} |
| 27 | + const entry = head.headEntries().find(e => e._i === Number(tag._e))! |
| 28 | + const sdeKey = `${tag._s || tag._p}:el` |
25 | 29 | // if we can hydrate an element via the selector id, do that instead of creating a new one |
26 | | - let $el = tag._s ? dom.querySelector(`[${tag._s}]`) : null |
27 | | - const renderCtx: DomRenderTagContext = { tag, document: dom, $el, head } |
| 30 | + // creating element with side effects |
| 31 | + const $newEl = createElement(tag, dom) |
| 32 | + const $el = tag._s ? dom.querySelector(`[${tag._s}]`) : null |
| 33 | + const renderCtx: DomRenderTagContext = { tag, document: dom, head } |
28 | 34 | await head.hooks.callHook('dom:renderTag', renderCtx) |
29 | 35 | // updating an existing tag |
30 | 36 | if ($el) { |
31 | 37 | if (Object.keys(tag.props).length === 0) { |
32 | 38 | $el.remove() |
33 | 39 | continue |
34 | 40 | } |
35 | | - sideEffectMap[tag._e!] = { |
36 | | - ...sideEffectMap[tag._e!], |
37 | | - ...setAttributes($el, tag), |
38 | | - } |
39 | | - $el.innerHTML = tag.children || '' |
40 | | - sideEffectMap[tag._e!][`${tag._p}:el:remove`] = () => $el?.remove() |
| 41 | + setAttributesWithSideEffects($el, entry, tag) |
| 42 | + if (TagsWithInnerContent.includes(tag.tag)) |
| 43 | + $el.innerHTML = tag.children || '' |
| 44 | + |
| 45 | + // may be a duplicate but it's okay |
| 46 | + entry._sde[sdeKey] = () => $el?.remove() |
41 | 47 | continue |
42 | 48 | } |
43 | 49 |
|
44 | 50 | if (tag.tag === 'title' && tag.children) { |
| 51 | + // we don't handle title side effects |
45 | 52 | dom.title = tag.children |
46 | 53 | continue |
47 | 54 | } |
48 | 55 |
|
49 | 56 | if (tag.tag === 'htmlAttrs' || tag.tag === 'bodyAttrs') { |
50 | | - sideEffectMap[tag._e!] = { |
51 | | - ...sideEffectMap[tag._e!], |
52 | | - ...setAttributes(dom[tag.tag === 'htmlAttrs' ? 'documentElement' : 'body'], tag), |
53 | | - } |
| 57 | + setAttributesWithSideEffects(dom[tag.tag === 'htmlAttrs' ? 'documentElement' : 'body'], entry, tag) |
54 | 58 | continue |
55 | 59 | } |
56 | 60 |
|
57 | | - $el = createElement(tag, dom) |
58 | | - |
59 | 61 | switch (tag.tagPosition) { |
60 | 62 | case 'bodyClose': |
61 | | - dom.body.appendChild($el) |
| 63 | + dom.body.appendChild($newEl) |
62 | 64 | break |
63 | 65 | case 'bodyOpen': |
64 | | - dom.body.insertBefore($el, dom.body.firstChild) |
| 66 | + dom.body.insertBefore($newEl, dom.body.firstChild) |
65 | 67 | break |
66 | 68 | case 'head': |
67 | 69 | default: |
68 | | - dom.head.appendChild($el) |
| 70 | + dom.head.appendChild($newEl) |
69 | 71 | break |
70 | 72 | } |
71 | 73 |
|
72 | | - sideEffectMap[tag._e!][`${tag._p}:el:remove`] = () => $el?.remove() |
73 | | - } |
74 | | - |
75 | | - // add side effects once we've rendered |
76 | | - for (const k in sideEffectMap) { |
77 | | - const entry = head.headEntries().find(e => e._i === Number(k))! |
78 | | - entry._sde = { |
79 | | - ...entry._sde, |
80 | | - ...sideEffectMap[k], |
81 | | - } |
| 74 | + entry._sde[sdeKey] = () => $newEl?.remove() |
82 | 75 | } |
83 | 76 | } |
84 | 77 |
|
85 | | -export const debouncedUpdateDom = async<T extends HeadClient<any>>(delayedFn: (fn: () => void) => void, head: T, options: RenderDomHeadOptions = {}) => { |
| 78 | +/** |
| 79 | + * Global instance of the dom update promise. Used for debounding head updates. |
| 80 | + */ |
| 81 | +export let domUpdatePromise: Promise<void> | null = null |
| 82 | + |
| 83 | +/** |
| 84 | + * Queue a debounced update of the DOM head. |
| 85 | + */ |
| 86 | +export async function debouncedRenderDOMHead<T extends HeadClient<any>>(delayedFn: (fn: () => void) => void, head: T, options: RenderDomHeadOptions = {}) { |
86 | 87 | // within the debounced dom update we need to compute all the tags so that watchEffects still works |
87 | 88 | function doDomUpdate() { |
88 | 89 | domUpdatePromise = null |
|
0 commit comments