From e1c00542da8d171f3637a647a6cd34624a824d64 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 17 Oct 2022 15:49:33 +0200 Subject: [PATCH 1/4] Add basic router and wp-link directive --- src/runtime/directives.js | 43 +++++++++++++++++++++++++- src/runtime/index.js | 17 ++++------ src/runtime/router.js | 65 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 src/runtime/router.js diff --git a/src/runtime/directives.js b/src/runtime/directives.js index 3e7a42cd..6a0bfbf0 100644 --- a/src/runtime/directives.js +++ b/src/runtime/directives.js @@ -1,7 +1,8 @@ -import { useContext, useMemo } from 'preact/hooks'; +import { useContext, useMemo, useEffect } from 'preact/hooks'; import { useSignalEffect } from '@preact/signals'; import { directive } from './hooks'; import { deepSignal } from './deepsignal'; +import { prefetch, navigate } from './router'; import { getCallback } from './utils'; const raf = window.requestAnimationFrame; @@ -81,4 +82,44 @@ export default () => { }); } ); + + // The `wp-link` directive. + directive( + 'link', + ({ + directives: { + link: { default: link }, + }, + props: { href }, + element, + }) => { + useEffect(() => { + // Prefetch the page if it is in the directive options. + if (link?.prefetch) { + prefetch(href); + } + }); + + // Don't do anything if it's falsy. + if (link !== false) { + element.props.onclick = async (event) => { + event.preventDefault(); + + // Fetch the page (or return it from cache). + await navigate(href); + + // Update the scroll, depending on the option. True by default. + if (link?.scroll === 'smooth') { + window.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth', + }); + } else if (link?.scroll !== false) { + window.scrollTo(0, 0); + } + }; + } + } + ); }; diff --git a/src/runtime/index.js b/src/runtime/index.js index b72f82db..23fd3c0e 100644 --- a/src/runtime/index.js +++ b/src/runtime/index.js @@ -1,8 +1,6 @@ -import { hydrate } from 'preact'; import registerDirectives from './directives'; import registerComponents from './components'; -import toVdom from './vdom'; -import { createRootFragment } from './utils'; +import { init } from './router'; /** * Initialize the initial vDOM. @@ -11,14 +9,11 @@ document.addEventListener('DOMContentLoaded', async () => { registerDirectives(); registerComponents(); - // Create the root fragment to hydrate everything. - const rootFragment = createRootFragment( - document.documentElement, - document.body - ); - - const vdom = toVdom(document.body); - hydrate(vdom, rootFragment); + // Do this manually to test it out. + document + .querySelectorAll('a') + .forEach((node) => node.setAttribute('wp-link', '{"prefetch": true }')); + await init(); console.log('hydrated!'); }); diff --git a/src/runtime/router.js b/src/runtime/router.js new file mode 100644 index 00000000..ec5e4260 --- /dev/null +++ b/src/runtime/router.js @@ -0,0 +1,65 @@ +import { hydrate, render } from 'preact'; +import toVdom from './vdom'; +import { createRootFragment } from './utils'; + +// The root to render the vdom (document.body). +let rootFragment; + +// The cache of visited and prefetched pages. +export const pages = new Map(); + +// 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, 'http://a.bc'); + return u.pathname + u.search; +}; + +// Fetch a new page and convert it to a static virtual DOM. +const fetchPage = async (url) => { + const html = await window.fetch(url).then((res) => res.text()); + const dom = new window.DOMParser().parseFromString(html, 'text/html'); + return toVdom(dom.body); +}; + +// 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) => { + url = cleanUrl(url); + if (!pages.has(url)) { + pages.set(url, fetchPage(url)); + } +}; + +// Navigate to a new page. +export const navigate = async (href) => { + const url = cleanUrl(href); + prefetch(url); + const vdom = await pages.get(url); + render(vdom, rootFragment); + window.history.pushState({ wp: { clientNavigation: true } }, '', 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. + if (pages.has(url)) { + const vdom = await pages.get(url); + render(vdom, rootFragment); + } else { + window.location.reload(); + } +}); + +// Initialize the router with the initial DOM. +export const init = async () => { + const url = cleanUrl(window.location); // Remove hash. + + // Create the root fragment to hydrate everything. + rootFragment = createRootFragment(document.documentElement, document.body); + + const vdom = toVdom(document.body); + pages.set(url, Promise.resolve(vdom)); + hydrate(vdom, rootFragment); +}; From d486d7e883cacc3b5c7f6547bb58edb36f63b7d2 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 17 Oct 2022 18:12:23 +0200 Subject: [PATCH 2/4] Enqueue scripts only on the frontend --- wp-directives.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wp-directives.php b/wp-directives.php index f947ffde..e7e3997b 100644 --- a/wp-directives.php +++ b/wp-directives.php @@ -36,7 +36,7 @@ function wp_directives_register_scripts() wp_enqueue_script('wp-directive-runtime'); } -add_action('init', 'wp_directives_register_scripts'); +add_action('wp_enqueue_scripts', 'wp_directives_register_scripts'); function add_wp_link_attribute($block_content) { @@ -48,10 +48,10 @@ function add_wp_link_attribute($block_content) } $link = parse_url($w->get_attribute('href')); - if (is_null($link['host']) || ($link['host'] === $site_url['host'])) { + if (is_null($link['host']) || $link['host'] === $site_url['host']) { $w->set_attribute('wp-link', 'true'); } - }; + } return (string) $w; } From 3a98a8d21eecd489c6aed42570bc022ed4daca4c Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 17 Oct 2022 18:12:47 +0200 Subject: [PATCH 3/4] Remove testing code --- src/runtime/index.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/runtime/index.js b/src/runtime/index.js index 23fd3c0e..e89ac9d4 100644 --- a/src/runtime/index.js +++ b/src/runtime/index.js @@ -8,12 +8,6 @@ import { init } from './router'; document.addEventListener('DOMContentLoaded', async () => { registerDirectives(); registerComponents(); - - // Do this manually to test it out. - document - .querySelectorAll('a') - .forEach((node) => node.setAttribute('wp-link', '{"prefetch": true }')); - await init(); console.log('hydrated!'); }); From 602363d07f4166ae2befcfed05f5a9921b5d48d9 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 17 Oct 2022 18:59:24 +0200 Subject: [PATCH 4/4] Fix undefined index error --- wp-directives.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wp-directives.php b/wp-directives.php index e7e3997b..5298a26b 100644 --- a/wp-directives.php +++ b/wp-directives.php @@ -1,5 +1,4 @@ get_attribute('href')); - if (is_null($link['host']) || $link['host'] === $site_url['host']) { + if (!isset($link['host']) || $link['host'] === $site_url['host']) { $w->set_attribute('wp-link', 'true'); } }