UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

139 lines 5.05 kB
import * as Vue from 'vue'; import { isServer } from '@tanstack/router-core/isServer'; import { Asset } from './Asset'; import { useTags } from './headContentUtils'; import { useRouter } from './useRouter'; function attrsMatch(attrs, element) { let expectedAttrCount = 0; for (const name in attrs) { const value = attrs[name]; if (value === undefined || value === false || value === null) { continue; } expectedAttrCount++; const attrName = name.toLowerCase(); if (value === true) { if (!element.hasAttribute(attrName)) { return false; } continue; } if (element.getAttribute(attrName) !== String(value)) { return false; } } return expectedAttrCount === element.attributes.length; } function reconcileHydratedHead(tags, preservedHeadTagElements) { if (typeof document === 'undefined') { return; } const matchedHeadElements = new Set(); const hydratedLinks = document.head.querySelectorAll('link'); for (const tag of tags) { if (tag.tag !== 'link') { continue; } const attrs = tag.attrs; const rel = typeof attrs?.rel === 'string' ? attrs.rel.toLowerCase() : undefined; if (rel !== 'stylesheet' && rel !== 'preload' && rel !== 'modulepreload') { continue; } for (const element of hydratedLinks) { if (!matchedHeadElements.has(element) && attrsMatch(attrs, element)) { matchedHeadElements.add(element); const key = JSON.stringify(tag); (preservedHeadTagElements[key] || (preservedHeadTagElements[key] = [])).push(element); break; } } } } function cleanupInactivePreservedHeadElements(preservedHeadTagElements, activeElements) { for (const key in preservedHeadTagElements) { const elements = preservedHeadTagElements[key]; let nextElements; for (const element of elements) { if (activeElements.has(element)) { ; (nextElements || (nextElements = [])).push(element); } else if (!shouldRemoveInactivePreservedHeadElement(element)) { ; (nextElements || (nextElements = [])).push(element); } else { element.remove(); } } if (nextElements) { preservedHeadTagElements[key] = nextElements; } else { delete preservedHeadTagElements[key]; } } } function shouldRemoveInactivePreservedHeadElement(element) { const rel = element.getAttribute('rel')?.toLowerCase(); return rel === 'preload' || rel === 'modulepreload'; } /** * @description The `HeadContent` component is used to render meta tags, links, and scripts for the current route. * It should be rendered in the `<head>` of your document. */ export const HeadContent = Vue.defineComponent({ name: 'HeadContent', props: { assetCrossOrigin: { type: [String, Object], default: undefined, }, }, setup(props) { const router = useRouter(); const tags = useTags(props.assetCrossOrigin); if (isServer ?? router.isServer) { return () => { return tags().map((tag) => { const key = JSON.stringify(tag); return Vue.h(Asset, { ...tag, key: `tsr-meta-${key}`, }); }); }; } const preservedHeadTagElements = {}; let activePreservedHeadElements = new Set(); if (router.ssr) { reconcileHydratedHead(tags(), preservedHeadTagElements); } Vue.onUpdated(() => { cleanupInactivePreservedHeadElements(preservedHeadTagElements, activePreservedHeadElements); }); Vue.onUnmounted(() => { cleanupInactivePreservedHeadElements(preservedHeadTagElements, new Set()); }); return () => { const renderedPreservedHeadTagKeys = {}; activePreservedHeadElements = new Set(); const renderedTags = tags().map((tag) => { const key = JSON.stringify(tag); const renderedCount = renderedPreservedHeadTagKeys[key] || 0; const preservedElement = preservedHeadTagElements[key]?.[renderedCount]; if (preservedElement?.isConnected) { renderedPreservedHeadTagKeys[key] = renderedCount + 1; activePreservedHeadElements.add(preservedElement); return null; } return Vue.h(Asset, { ...tag, key: `tsr-meta-${key}`, }); }); return renderedTags; }; }, }); //# sourceMappingURL=HeadContent.jsx.map