Skip to content

Commit

Permalink
Add router with region-based client-side navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
luisherranz committed Aug 16, 2023
1 parent 35eede2 commit 3cf83bf
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 28 deletions.
22 changes: 0 additions & 22 deletions packages/interactivity/src/hydration.js

This file was deleted.

9 changes: 3 additions & 6 deletions packages/interactivity/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@
* Internal dependencies
*/
import registerDirectives from './directives';
import { init } from './hydration';
import { init } from './router';
import { rawStore, afterLoads } from './store';
export { store } from './store';
export { directive } from './hooks';
export { navigate, prefetch } from './router';
export { h as createElement } from 'preact';
export { useEffect, useContext, useMemo } from 'preact/hooks';
export { deepSignal } from 'deepsignal';

/**
* Initialize the Interactivity API.
*/
registerDirectives();

document.addEventListener( 'DOMContentLoaded', async () => {
registerDirectives();
await init();
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
} );
127 changes: 127 additions & 0 deletions packages/interactivity/src/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* External dependencies
*/
import { hydrate, render } from 'preact';
/**
* Internal dependencies
*/
import { toVdom, hydratedIslands } from './vdom';
import { createRootFragment } from './utils';
import { directivePrefix } from './constants';

// The cache of visited and prefetched pages.
const pages = new Map();

// Keep the same root fragment for each interactive region node.
const regionRootFragments = new WeakMap();
const getRegionRootFragment = ( region ) => {
if ( ! regionRootFragments.has( region ) ) {
regionRootFragments.set(
region,
createRootFragment( region.parentElement, region )
);
}
return regionRootFragments.get( region );
};

// Helper to remove domain and hash from the URL. We are only interesting in
// caching the path and the query.
const cleanUrl = ( url ) => {
const u = new URL( url, window.location );
return u.pathname + u.search;
};

// Fetch a new page and convert it to a static virtual DOM.
const fetchPage = async ( url, { html } ) => {
try {
if ( ! html ) {
const res = await window.fetch( url );
if ( res.status !== 200 ) return false;
html = await res.text();
}
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
return regionsToVdom( dom );
} catch ( e ) {
return false;
}
};

// Return an object with VDOM trees of those HTML regions marked with a
// `navigation-id` directive.
const regionsToVdom = ( dom ) => {
const regions = {};
const attrName = `data-${ directivePrefix }-navigation-id`;
dom.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => {
const id = region.getAttribute( attrName );
regions[ id ] = toVdom( region );
} );

return { regions };
};

// Prefetch a page. We store the promise to avoid triggering a second fetch for
// a page if a fetching has already started.
export const prefetch = ( url, options = {} ) => {
url = cleanUrl( url );
if ( options.force || ! pages.has( url ) ) {
pages.set( url, fetchPage( url, options ) );
}
};

// Render all interactive regions contained in the given page.
const renderRegions = ( page ) => {
const attrName = `data-${ directivePrefix }-navigation-id`;
document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => {
const id = region.getAttribute( attrName );
const fragment = getRegionRootFragment( region );
render( page.regions[ id ], fragment );
} );
};

// 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 );
window.history[ options.replace ? 'replaceState' : 'pushState' ](
{},
'',
href
);
} else {
window.location.assign( href );
}
};

// Listen to the back and forward buttons and restore the page if it's in the
// cache.
window.addEventListener( 'popstate', async () => {
const url = cleanUrl( window.location ); // Remove hash.
const page = pages.has( url ) && ( await pages.get( url ) );
if ( page ) {
renderRegions( page );
} else {
window.location.reload();
}
} );

// Initialize the router with the initial DOM.
export const init = async () => {
document
.querySelectorAll( `[data-${ directivePrefix }-interactive]` )
.forEach( ( node ) => {
if ( ! hydratedIslands.has( node ) ) {
const fragment = getRegionRootFragment( node );
const vdom = toVdom( node );
hydrate( vdom, fragment );
}
} );

// Cache the current regions.
pages.set(
cleanUrl( window.location ),
Promise.resolve( regionsToVdom( document ) )
);
};

0 comments on commit 3cf83bf

Please sign in to comment.