A dynamic page loader using async and fetch

Hello,

I just thought I’d share what I managed to cobble together by looking into the fetch API. Not sure it helps further understanding of how it works, but if you’ve been curious, this is one way of using fetch to only reload parts of a web page when browsing.

Hope it’s useful to someone!

/* 
 * Simple fetch script using async.
 * 
 * This script also includes dynamic page loading into the first element
 * of type def_target (getElementsByTagName(def_target)[0]]). Can be
 * modified and expanded to become more versatile.
 */

// This variable sets the element from which to read HTML from and
// insert HTML into.
var def_target = "main"

/* This snippet handles page loading when the user presses the
 * browser navigation buttons. It also scrolls to top.
 * 
 * States are pushed into the history from the anonymous function set on
 * each <a> element with relative href in the addListeners function.
 */
window.onpopstate = function(e) {
	loadPage(e.target.location.href); // href is set on pushState in anonymous function set by addListeners
	document.body.scrollTop = 0; // Scroll to top
	document.documentElement.scrollTop = 0; // Scroll to top on Safari
}

/* Since fetch can only return text or JSON currently, we need to parse
 * it into a DOM for easier extraction of data.
 * 
 * If your pages are humongous this function could be rewritten to parse
 * the text directly instead, but the gains will probably not be great.
 */
function parseRequest(text, target) {
	let td = new DOMParser().parseFromString(text, "text/html"); // create a DOM from text (td = to DOM)
	return [td.title, td.getElementsByTagName(target)[0]]; // return the DOM title and the first element of type target
}

/* loadPage is where the async part of the script comes into play.
 * This function fetches the target url HTML page as text (on separate
 * lines so the promise is reliably resolved) and then converts the
 * first element of type def_target into HTML before inserting it into
 * the first element in the document of type def_target.
 * 
 * This function can be updated to have a separate source and target, or
 * to fetch elements of certain classes or ids, rather than by tag.
 */
async function loadPage(url) {
	let req = await fetch(url); // fetch promise
	let data = await req.text(); // once promise is fulfilled, convert it to text
	data = await parseRequest(data, def_target); // get desired block of text as HTML DOM
	await document.getElementsByTagName(def_target)[0].replaceWith(data[1]); // replace desired block on page with DOM data
	document.title = data[0]; // set title of document to match target url
	await addListeners(def_target); // add listeners to the newly inserted HTML
}

/* addListeners adds anonymous functions to each relative URL in the 
 * specified scope (with def_target as default scope). Absolute URLs are
 * ignored.
 * 
 * This function targets every link in the specified scope, but it can
 * be updated to only target certain classes of links, or instead of
 * setting the scope by tag, it could be an id or class instead.
 */
function addListeners(scope = def_target) {
	for (let i of document.getElementsByTagName(scope)[0].getElementsByTagName("a")) { // for every a in scope
		if ( /^https?:\/\//.test(i.attributes.href.value) ) { continue } // if the href starts with http(s):// then skip to the next link
		i.addEventListener("click", function (e) {
			e.preventDefault(); // stop normal page load
			loadPage(this.href); // load page with loadPage instead (which also adds new listeners)
			if (window.location.href !== this.href) { // We don't want duplicate entries in the history
				window.history.pushState( {}, "", this.href); // push the new url into the history.
			}
		});
	}
}

function init() {
	addListeners("body"); // Add anonymous functions to all relative links on entire page on init.
}
init();
2 Likes