@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
139 lines • 5.05 kB
JSX
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