@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
127 lines • 5.02 kB
JSX
import { isServer } from '@tanstack/router-core/isServer';
import { createEffect } from 'solid-js';
import { useRouter } from './useRouter';
export function Asset({ tag, attrs, children, }) {
switch (tag) {
case 'title':
return <Title attrs={attrs} children={children}/>;
case 'meta':
return <meta {...attrs}/>;
case 'link':
return <link {...attrs}/>;
case 'style':
if (typeof children === 'string') {
return <style {...attrs} innerHTML={children}/>;
}
return <style {...attrs}/>;
case 'script':
return <Script attrs={attrs} children={children}/>;
default:
return null;
}
}
function Title(props) {
const router = useRouter();
const attrs = props.attrs;
const children = props.children;
// Server: render <title> normally
if (isServer ?? router.isServer) {
return <title {...attrs}>{children}</title>;
}
// Client: imperatively set document.title so it updates during
// client-side navigation (JSX <title> in <head> doesn't reliably
// update the browser's document.title).
createEffect(() => children, (titleText) => {
document.title = typeof titleText === 'string' ? titleText : '';
});
// Still render the <title> element in the DOM for consistency,
// but the imperative assignment above is what actually drives the update.
return <title {...attrs}>{children}</title>;
}
function Script(props) {
const router = useRouter();
const attrs = props.attrs;
const children = props.children;
const dataScript = typeof attrs?.type === 'string' &&
attrs.type !== '' &&
attrs.type !== 'text/javascript' &&
attrs.type !== 'module';
// --- Server rendering ---
if (isServer ?? router.isServer) {
if (attrs?.src) {
return <script {...attrs}/>;
}
if (typeof children === 'string') {
return <script {...attrs} innerHTML={children}/>;
}
return null;
}
// --- Client rendering ---
// Data scripts (e.g. application/ld+json) are rendered in the tree;
// they don't need to execute.
if (dataScript && typeof children === 'string') {
return <script {...attrs} innerHTML={children}/>;
}
// For executable scripts, use imperative DOM injection so the browser
// actually executes them during client-side navigation.
createEffect(() => ({ attrs, children, dataScript }), ({ attrs, children, dataScript }) => {
if (dataScript)
return;
let script;
if (attrs?.src) {
const normSrc = (() => {
try {
const base = document.baseURI || window.location.href;
return new URL(attrs.src, base).href;
}
catch {
return attrs.src;
}
})();
const existingScript = Array.from(document.querySelectorAll('script[src]')).find((el) => el.src === normSrc);
if (existingScript) {
return;
}
script = document.createElement('script');
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined && value !== false) {
script.setAttribute(key, typeof value === 'boolean' ? '' : String(value));
}
}
document.head.appendChild(script);
}
else if (typeof children === 'string') {
const typeAttr = typeof attrs?.type === 'string' ? attrs.type : 'text/javascript';
const nonceAttr = typeof attrs?.nonce === 'string' ? attrs.nonce : undefined;
const existingScript = Array.from(document.querySelectorAll('script:not([src])')).find((el) => {
if (!(el instanceof HTMLScriptElement))
return false;
const sType = el.getAttribute('type') ?? 'text/javascript';
const sNonce = el.getAttribute('nonce') ?? undefined;
return (el.textContent === children &&
sType === typeAttr &&
sNonce === nonceAttr);
});
if (existingScript) {
return;
}
script = document.createElement('script');
script.textContent = children;
if (attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined && value !== false) {
script.setAttribute(key, typeof value === 'boolean' ? '' : String(value));
}
}
}
document.head.appendChild(script);
}
return () => {
if (script?.parentNode) {
script.parentNode.removeChild(script);
}
};
});
return null;
}
//# sourceMappingURL=Asset.jsx.map