Skip to content

Commit

Permalink
First implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
DAreRodz committed Aug 31, 2023
1 parent 9f34da9 commit 9baff87
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 5 deletions.
63 changes: 63 additions & 0 deletions packages/interactivity/src/head.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const getTagId = ( tag ) => tag.id || tag.outerHTML;

const canBePreloaded = ( e ) => e.src || ( e.href && e.rel !== 'preload' );

const loadAsset = ( a ) => {
const loader = document.createElement( 'link' );
loader.rel = 'preload';
if ( a.nodeName === 'SCRIPT' ) {
loader.as = 'script';
loader.href = a.getAttribute( 'src' );
} else if ( a.nodeName === 'LINK' ) {
loader.as = 'style';
loader.href = a.getAttribute( 'href' );
}

const p = new Promise( ( resolve, reject ) => {
loader.onload = () => resolve( loader );
loader.onerror = () => reject( loader );
} );

document.head.appendChild( loader );
return p;
};

const activateScript = ( n ) => {
if ( n.nodeName !== 'SCRIPT' ) return n;
const s = document.createElement( n.nodeName );
s.innerText = n.innerText;
for ( const attr of n.attributes ) {
s.setAttribute( attr.name, attr.value );
}
return s;
};

export const updateHead = async ( newHead ) => {
// Map incoming head tags by their content.
const newHeadMap = new Map();
for ( const child of newHead.children ) {
newHeadMap.set( getTagId( child ), child );
}

const toRemove = [];

// Detect nodes that should be added or removed.
for ( const child of document.head.children ) {
const id = getTagId( child );
if ( newHeadMap.has( id ) ) newHeadMap.delete( id );
else if ( child.nodeName !== 'SCRIPT' ) toRemove.push( child );
}

// Prepare new assets.
const toAppend = [ ...newHeadMap.values() ];

// Wait for all new assets to be loaded.
const loaders = await Promise.all(
toAppend.filter( canBePreloaded ).map( loadAsset )
);

// Apply the changes.
toRemove.forEach( ( n ) => n.remove() );
loaders.forEach( ( l ) => l && l.remove() );
document.head.append( ...toAppend.map( activateScript ) );
};
22 changes: 17 additions & 5 deletions packages/interactivity/src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { hydrate, render } from 'preact';
import { toVdom, hydratedIslands } from './vdom';
import { createRootFragment } from './utils';
import { directivePrefix } from './constants';
import { updateHead } from './head';

// The cache of visited and prefetched pages.
const pages = new Map();
Expand Down Expand Up @@ -40,7 +41,10 @@ const fetchPage = async ( url, { html } ) => {
html = await res.text();
}
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
return regionsToVdom( dom );
return {
head: dom.querySelector( 'head' ),
regions: regionsToVdom( dom ),
};
} catch ( e ) {
return false;
}
Expand All @@ -56,7 +60,7 @@ const regionsToVdom = ( dom ) => {
regions[ id ] = toVdom( region );
} );

return { regions };
return regions;
};

// Prefetch a page. We store the promise to avoid triggering a second fetch for
Expand All @@ -78,13 +82,17 @@ const renderRegions = ( page ) => {
} );
};

const nextTick = ( fn ) =>
new Promise( ( resolve ) => setTimeout( () => resolve( fn() ) ) );

// Navigate to a new page.
export const navigate = async ( href, options = {} ) => {
const url = cleanUrl( href );
prefetch( url, options );
const page = await pages.get( url );
if ( page ) {
renderRegions( page );
await updateHead( page.head );
await nextTick( () => renderRegions( page ) );
window.history[ options.replace ? 'replaceState' : 'pushState' ](
{},
'',
Expand All @@ -101,7 +109,8 @@ window.addEventListener( 'popstate', async () => {
const url = cleanUrl( window.location ); // Remove hash.
const page = pages.has( url ) && ( await pages.get( url ) );
if ( page ) {
renderRegions( page );
await updateHead( page.head );
await nextTick( () => renderRegions( page ) );
} else {
window.location.reload();
}
Expand All @@ -122,6 +131,9 @@ export const init = async () => {
// Cache the current regions.
pages.set(
cleanUrl( window.location ),
Promise.resolve( regionsToVdom( document ) )
Promise.resolve( {
head: document.head,
regions: regionsToVdom( document ),
} )
);
};

0 comments on commit 9baff87

Please sign in to comment.