11import { TagsWithInnerContent , createElement } from 'zhead'
2- import type { BeforeRenderContext , DomRenderTagContext , Unhead } from '@unhead/schema'
3- import { setAttributesWithSideEffects } from './setAttributesWithSideEffects'
2+ import type { BeforeRenderContext , DomRenderTagContext , HeadEntry , HeadTag , Unhead } from '@unhead/schema'
43
54export interface RenderDomHeadOptions {
65 /**
@@ -14,82 +13,126 @@ export interface RenderDomHeadOptions {
1413 */
1514export async function renderDOMHead < T extends Unhead < any > > ( head : T , options : RenderDomHeadOptions = { } ) {
1615 const dom : Document = options . document || window . document
17-
1816 const tags = await head . resolveTags ( )
1917
20- const context : BeforeRenderContext = { shouldRender : true , tags }
21- await head . hooks . callHook ( 'dom:beforeRender' , context )
18+ const ctx : BeforeRenderContext = { shouldRender : true , tags }
19+ await head . hooks . callHook ( 'dom:beforeRender' , ctx )
2220 // allow integrations to block to the render
23- if ( ! context . shouldRender )
21+ if ( ! ctx . shouldRender )
2422 return
2523
26- for ( const tag of context . tags ) {
27- const renderCtx : DomRenderTagContext = { shouldRender : true , tag }
28- await head . hooks . callHook ( 'dom:renderTag' , renderCtx )
29- if ( ! renderCtx . shouldRender )
30- continue
31-
32- const entry = head . headEntries ( ) . find ( e => e . _i === Number ( tag . _e ) ) !
33-
24+ // queue everything to be deleted, and then we'll conditionally remove side effects which we don't want to fire
25+ const queuedSideEffects = head . _popSideEffectQueue ( )
26+ head . headEntries ( )
27+ . map ( entry => entry . _sde )
28+ . forEach ( ( sde ) => {
29+ Object . entries ( sde ) . forEach ( ( [ key , fn ] ) => {
30+ queuedSideEffects [ key ] = fn
31+ } )
32+ } )
33+
34+ const renderTag = ( tag : HeadTag , entry : HeadEntry < any > ) => {
3435 if ( tag . tag === 'title' && tag . children ) {
3536 // we don't handle title side effects
3637 dom . title = tag . children
37- continue
38+ return
39+ }
40+
41+ const markSideEffect = ( key : string , fn : ( ) => void ) => {
42+ key = `${ tag . _s || tag . _p } :${ key } `
43+ entry . _sde [ key ] = fn
44+ delete queuedSideEffects [ key ]
45+ }
46+
47+ /**
48+ * Set attributes on a DOM element, while adding entry side effects.
49+ */
50+ const setAttrs = ( $el : Element ) => {
51+ // add new attributes
52+ Object . entries ( tag . props ) . forEach ( ( [ k , value ] ) => {
53+ value = String ( value )
54+ const attrSdeKey = `attr:${ k } `
55+ // class attributes have their own side effects to allow for merging
56+ if ( k === 'class' ) {
57+ for ( const c of value . split ( ' ' ) ) {
58+ const classSdeKey = `${ attrSdeKey } :${ c } `
59+ // always clear side effects
60+ markSideEffect ( classSdeKey , ( ) => $el . classList . remove ( c ) )
61+
62+ if ( ! $el . classList . contains ( c ) )
63+ $el . classList . add ( c )
64+ }
65+ return
66+ }
67+ // always clear side effects
68+ if ( ! k . startsWith ( 'data-h-' ) )
69+ markSideEffect ( attrSdeKey , ( ) => $el . removeAttribute ( k ) )
70+
71+ if ( $el . getAttribute ( k ) !== value )
72+ $el . setAttribute ( k , value )
73+ } )
3874 }
3975
4076 if ( tag . tag === 'htmlAttrs' || tag . tag === 'bodyAttrs' ) {
41- setAttributesWithSideEffects ( head , dom [ tag . tag === 'htmlAttrs' ? 'documentElement' : 'body' ] , entry , tag )
42- continue
77+ setAttrs ( dom [ tag . tag === 'htmlAttrs' ? 'documentElement' : 'body' ] )
78+ return
4379 }
4480
45- const sdeKey = `${ tag . _s || tag . _p } :el`
46- const $newEl = createElement ( tag , dom )
47- let $previousEl : Element | null = null
81+ let $newEl = createElement ( tag , dom )
82+ let $previousEl : Element | undefined
4883 // optimised scan of children
4984 for ( const $el of dom [ tag . tagPosition ?. startsWith ( 'body' ) ? 'body' : 'head' ] . children ) {
50- if ( $el . hasAttribute ( `${ tag . _s } ` ) ) {
85+ if ( $el . hasAttribute ( `${ tag . _s } ` ) || $el . isEqualNode ( $newEl ) ) {
5186 $previousEl = $el
5287 break
5388 }
5489 }
5590
5691 // updating an existing tag
5792 if ( $previousEl ) {
58- // safe to ignore removal
59- head . _removeQueuedSideEffect ( sdeKey )
93+ markSideEffect ( 'el' , ( ) => $previousEl ?. remove ( ) )
6094
61- if ( $newEl . isEqualNode ( $previousEl ) )
62- continue
95+ // @todo test around empty tags
6396 if ( Object . keys ( tag . props ) . length === 0 ) {
6497 $previousEl . remove ( )
65- continue
98+ return
6699 }
67- setAttributesWithSideEffects ( head , $previousEl , entry , tag )
100+ if ( $newEl . isEqualNode ( $previousEl ) )
101+ return
102+
103+ setAttrs ( $previousEl )
68104 if ( TagsWithInnerContent . includes ( tag . tag ) )
69105 $previousEl . innerHTML = tag . children || ''
70-
71- // may be a duplicate but it's okay
72- entry . _sde [ sdeKey ] = ( ) => $previousEl ?. remove ( )
73- continue
106+ return
74107 }
75108
76109 switch ( tag . tagPosition ) {
77110 case 'bodyClose' :
78- dom . body . appendChild ( $newEl )
111+ $newEl = dom . body . appendChild ( $newEl )
79112 break
80113 case 'bodyOpen' :
81- dom . body . insertBefore ( $newEl , dom . body . firstChild )
114+ $newEl = dom . body . insertBefore ( $newEl , dom . body . firstChild )
82115 break
83116 case 'head' :
84117 default :
85- dom . head . appendChild ( $newEl )
118+ $newEl = dom . head . appendChild ( $newEl )
86119 break
87120 }
88- entry . _sde [ sdeKey ] = ( ) => $newEl ?. remove ( )
121+
122+ markSideEffect ( 'el' , ( ) => $newEl ?. remove ( ) )
123+ }
124+
125+ for ( const tag of ctx . tags ) {
126+ const renderCtx : DomRenderTagContext = { shouldRender : true , tag }
127+ await head . hooks . callHook ( 'dom:renderTag' , renderCtx )
128+ if ( ! renderCtx . shouldRender )
129+ continue
130+
131+ renderTag ( tag , head . headEntries ( ) . find ( e => e . _i === Number ( tag . _e ) ) ! )
89132 }
90133
91- // run side effect cleanup
92- head . _flushQueuedSideEffects ( )
134+ // clear all side effects still pending
135+ Object . values ( queuedSideEffects ) . forEach ( fn => fn ( ) )
93136}
94137
95138/**
0 commit comments