489b13d00e
This handles the case where the children on a head element are undefined and not a string or an array of strings. This doesn't currently handle sub-children on head elements so additional handling will be needed if this is a feature we would like to support although can be discussed/investigated separately from this fix. Fixes: https://github.com/vercel/next.js/issues/17364 Fixes: https://github.com/vercel/next.js/issues/6388 Closes: https://github.com/vercel/next.js/pull/16751
113 lines
2.8 KiB
TypeScript
113 lines
2.8 KiB
TypeScript
import { createElement } from 'react'
|
|
import { HeadEntry } from '../next-server/lib/utils'
|
|
|
|
const DOMAttributeNames: Record<string, string> = {
|
|
acceptCharset: 'accept-charset',
|
|
className: 'class',
|
|
htmlFor: 'for',
|
|
httpEquiv: 'http-equiv',
|
|
}
|
|
|
|
function reactElementToDOM({ type, props }: JSX.Element): HTMLElement {
|
|
const el = document.createElement(type)
|
|
for (const p in props) {
|
|
if (!props.hasOwnProperty(p)) continue
|
|
if (p === 'children' || p === 'dangerouslySetInnerHTML') continue
|
|
|
|
// we don't render undefined props to the DOM
|
|
if (props[p] === undefined) continue
|
|
|
|
const attr = DOMAttributeNames[p] || p.toLowerCase()
|
|
el.setAttribute(attr, props[p])
|
|
}
|
|
|
|
const { children, dangerouslySetInnerHTML } = props
|
|
if (dangerouslySetInnerHTML) {
|
|
el.innerHTML = dangerouslySetInnerHTML.__html || ''
|
|
} else if (children) {
|
|
el.textContent =
|
|
typeof children === 'string'
|
|
? children
|
|
: Array.isArray(children)
|
|
? children.join('')
|
|
: ''
|
|
}
|
|
return el
|
|
}
|
|
|
|
function updateElements(
|
|
elements: Set<Element>,
|
|
components: JSX.Element[],
|
|
removeOldTags: boolean
|
|
) {
|
|
const headEl = document.getElementsByTagName('head')[0]
|
|
const oldTags = new Set(elements)
|
|
|
|
components.forEach((tag) => {
|
|
if (tag.type === 'title') {
|
|
let title = ''
|
|
if (tag) {
|
|
const { children } = tag.props
|
|
title =
|
|
typeof children === 'string'
|
|
? children
|
|
: Array.isArray(children)
|
|
? children.join('')
|
|
: ''
|
|
}
|
|
if (title !== document.title) document.title = title
|
|
return
|
|
}
|
|
|
|
const newTag = reactElementToDOM(tag)
|
|
const elementIter = elements.values()
|
|
|
|
while (true) {
|
|
// Note: We don't use for-of here to avoid needing to polyfill it.
|
|
const { done, value } = elementIter.next()
|
|
if (value?.isEqualNode(newTag)) {
|
|
oldTags.delete(value)
|
|
return
|
|
}
|
|
|
|
if (done) {
|
|
break
|
|
}
|
|
}
|
|
|
|
elements.add(newTag)
|
|
headEl.appendChild(newTag)
|
|
})
|
|
|
|
oldTags.forEach((oldTag) => {
|
|
if (removeOldTags) {
|
|
oldTag.parentNode!.removeChild(oldTag)
|
|
}
|
|
elements.delete(oldTag)
|
|
})
|
|
}
|
|
|
|
export default function initHeadManager(initialHeadEntries: HeadEntry[]) {
|
|
const headEl = document.getElementsByTagName('head')[0]
|
|
const elements = new Set<Element>(headEl.children)
|
|
|
|
updateElements(
|
|
elements,
|
|
initialHeadEntries.map(([type, props]) => createElement(type, props)),
|
|
false
|
|
)
|
|
|
|
let updatePromise: Promise<void> | null = null
|
|
|
|
return {
|
|
mountedInstances: new Set(),
|
|
updateHead: (head: JSX.Element[]) => {
|
|
const promise = (updatePromise = Promise.resolve().then(() => {
|
|
if (promise !== updatePromise) return
|
|
|
|
updatePromise = null
|
|
updateElements(elements, head, true)
|
|
}))
|
|
},
|
|
}
|
|
}
|