@sliphua/pjax

Easily enable fast AJAX navigation (using Fetch + pushState)

Usage no npm install needed!

<script type="module">
  import sliphuaPjax from 'https://cdn.skypack.dev/@sliphua/pjax';
</script>

README

ES6+ Pjax

Build Status npm Package Compressed Minified Size

Easily enable fast AJAX navigation (Fetch + pushState).

Pjax aims to deliver app-like browsing experiences. No more full page reloads, no more multiple HTTP requests. It does not rely on other libraries, like jQuery or similar. Written in pure TS, transpiled by Babel and Rollup.

A maintained, modern, and smaller version of MoOx/pjax.


đŸŋī¸ Jump to Usage, Options, Status, Q&A, or Contributing Guide.

Installation

Choose a source

jsDelivr

Visit https://www.jsdelivr.com/package/npm/@sliphua/pjax.

npm

Install package @sliphua/pjax:

npm install @sliphua/pjax

Git

Clone this repo and install:

git clone https://github.com/PaperStrike/Pjax.git
cd Pjax
npm install

Pick a script in dist folder

Each script file has a related .map file, known as the source map, for debugging. Browsers won't fetch them without DevTools opened, so it won't affect your users' experiences. For more information, click the link to find out.

To declare globally

Link to pjax.js or pjax.min.js in a separate <script> tag as:

<script src="./dist/pjax.js"></script>

To import as ES module

Import the default value from pjax.esm.js or pjax.esm.min.js as:

import Pjax from './dist/pjax.esm.js';

What Pjax Does

In short, ONE fetch with a pushState call.

Pjax fetches the new content, updates the URL, switches parts of your page, executes newly added scripts and scroll to the right position without refreshing the whole thing.

How Pjax Works

  1. Listen to simple redirections.
  2. Fetch the target page via fetch.
  3. Update current URL using pushState.
  4. Render the DOM tree using DOMParser.
  5. Check if defined selectors select the same amount of elements in current DOM and the new DOM.
    • If no, Pjax uses standard navigation.
    • If yes, Pjax switches the elements in index order.
  6. Execute all newly-added or targeted scripts in document order.
  7. Scroll to the defined position.

Overview

Just designate the elements that differs between navigations. Pjax handles all the rest things.

Consider the following page:

<!DOCTYPE html>
<html lang="">
<head>
  <title>My Cool Blog</title>
  <meta name="description" content="Welcome to My Cool Blog">
  <link href="/styles.css" rel="stylesheet">
</head>

<body>
<header class="header">
  <nav>
    <a href="/" class="is-active">Home</a>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
  </nav>
</header>

<section class="content">
  <h1>My Cool Blog</h1>
  <p>
    Thanks for stopping by!

    <a href="/about">Click Here to find out more about me.</a>
  </p>
</section>

<aside class="sidebar">
  <h3>Recent Posts</h3>
  <!-- sidebar content -->
</aside>

<footer class="footer">
  &copy; My Cool Blog
</footer>

<script src="onDomReady.js"></script>
</body>
</html>

We want Pjax to intercept the URL /about, and replace .content with the resulting content of the request.

Besides, we would like to replace the <nav> to show the active /about link, as well as update our page meta and the <aside> sidebar.

So all in all we want to update the page title and meta, header, content area, and sidebar, without reloading styles or scripts.

We can easily achieve this by telling Pjax to use such CSS selectors:

const pjax = new Pjax({
  selectors: [
    'title',
    'meta[name=description]',
    '.header',
    '.content',
    '.sidebar',
  ],
});

Now, when someone in a compatible browser clicks a link, the content selected above will switch to the specific content pieces found in the next page.

Magic! For real! Nothing server-side!

Compatibility

Browser Supported versions Release date
Chrome 66+ Apr 17, 2018
Edge 79+ Jan 15, 2020
Firefox 60+ May 9, 2018
Opera 53+ May 10, 2018
Safari 12.2+ Jul 22, 2019

Usage

Method Parameters Return Value
Pjax.constructor options?: Partial<Options> Pjax
load requestInfo: RequestInfo, overrideOptions?: Partial<Options> Promise<void>
weakLoad requestInfo: RequestInfo, overrideOptions?: Partial<Options> Promise<void>
switchDOM requestInfo: RequestInfo, overrideOptions?: Partial<Options> Promise<void>
preparePage switchesResult: SwitchesResult | null, overrideOptions?: Partial<Options> Promise<void>
Pjax.reload / void

constructor

The most basic way to get started.

When instantiating Pjax, you can pass options into the constructor as an object:

const pjax = new Pjax({
  selectors: [
    'title',
    '.header',
    '.content',
    '.sidebar',
  ],
});

This will enable Pjax on all links and forms, and designate the part to replace using CSS selectors 'title', '.header', '.content', and '.sidebar'.

To disable the default Pjax trigger, set defaultTrigger option to false.

load

A call to this method aborts the current Pjax action (if any), and navigates to the target resource in Pjax way.

Any error other than AbortError leads to the normal navigation (by window.location.assign). Note that AbortError happens on fetch timeout, too.

const pjax = new Pjax();

// use case 1
pjax.load('/your-url').catch(() => {});

// use case 2 (with options override)
pjax.load('/your-url', { timeout: 200 }).catch(() => {});

// use case 3 (with further action)
pjax.load('/your-url')
  .then(() => {
    onSuccess();
  })
  .catch(() => {
    onAbort();
  });

// use case 4 (with formed Request object)
const requestToSend = new Request('/your-url', {
  method: 'POST',
  body: 'example',
});
pjax.load(requestToSend);

// use case X, mix brakets above together

weakLoad

This method behaves almost the same as load, except that it throws regardless of the error's type.

Useful when you need to handle all the errors on your own.

const pjax = new Pjax();

// use case
pjax.weakLoad('/your-url')
  .then(() => {
    onSuccess();
  })
  .catch((e) => {
    onError(e);
  });

switchDOM

This method accepts the URL string or the Request object to fetch. The response should contain the target document.

It returns a promise that resolves when all the following steps have done:

  1. Call pushState to update the URL.
  2. Transform elements selected by selectors with switches defined in switches.
  3. Set focusCleared to true if previous step has cleared the page focus, otherwise, false.
  4. Call and await preparePage with a new SwitchesResult that contains focusCleared.

preparePage

This method accepts a nullable SwitchesResult.

It returns a promise that resolves when all the following steps have done:

  1. Focus the first autofocus if focusCleared (in the given switches result) evaluates to true.
  2. Execute all newly-loaded and targeted scripts in document order.
  3. Wait until the blocking scripts got executed (e.g., inline scripts).
  4. Scroll to the position given by scrollTo option.

Type SwitchesResult

interface SwitchesResult {
  focusCleared: boolean
}

reload

A helper shortcut for window.location.reload, a static member of Pjax.

Pjax.reload();

Options

Name Type Default Value
defaultTrigger boolean true
selectors string[] ['title', '.pjax']
switches Record<string, Switch> {}
scripts string script[data-pjax]
scrollTo number | [number, number] | boolean true
scrollRestoration boolean true
cache RequestCache 'default'
timeout number 0

defaultTrigger

When set to false, disable the default Pjax trigger.

The default trigger alters these redirections:

  • Activations of <a> and <area> that targets a link within the same origin.
  • Submissions of <form> that redirects to a link within the same origin.

Disable when you only need Pjax in some specific moments. E.g.,

// Set `defaultTrigger` to `false`.
const pjax = new Pjax({ defaultTrigger: false });

// Use `load` on your demand.
document.addEventListener('example', (event) => {
  if (!needsPjax) return;
  event.preventDefault();
  pjax.load('/bingo');
});

selectors

CSS selector list used to target contents to replace. E.g.,

const pjax = new Pjax({
  selectors: [
    'title',
    '.content',
  ],
});

If a query returns multiples items, it will just keep the index.

Every selector, in the current page and new page, must select the same amount of DOM elements. Otherwise, Pjax will fall back into normal page load.

switches

This contains callbacks (of type Switch) used to switch old elements with new elements.

The object keys should match one of the defined selectors (from the selectors option).

Examples:

const pjax = new Pjax({
  selectors: ['title', '.Navbar', '.pjax'],
  switches: {
    // default behavior
    'title': Pjax.switches.default,
    '.content': async (oldEle, newEle) => {
      // How you deal with the two elements.
    },
    '.pjax': Pjax.switches.innerText,
  },
});

Type Switch

type Switch<T extends Element = Element> = (oldEle: T, newEle: T) => (Promise<void> | void);

When it returns a promise, Pjax recognizes when the switch has done. Newly added scripts execute and labeled scripts re-execute after all switches finishes.

Existing Switch Callbacks

  • Pjax.switches.default — The default behavior, same as Pjax.switches.replaceWith.
  • Pjax.switches.innerHTML — Replace HTML contents by using Element.innerHTML.
  • Pjax.switches.textContent — Replace all text by using Node.textContent.
  • Pjax.switches.innerText — Replace readable text by using HTMLElement.innerText.
  • Pjax.switches.attributes — Rewrite all attributes, leaving inner HTML untouched.
  • Pjax.switches.replaceWith — Replace elements by using ChildNode.replaceWith.

Creating a Switch Callback

Your callback function can do whatever you want. But remember to keep the amount of the elements selected by the selectors option remain the same.

In the example below, .current class marks the only switching element, so that the switch elements' amount won't change. Before the returned promise resolves, Pjax will neither execute the script elements nor scroll the page.

const pjax = new Pjax({
  selectors: ['.sidebar.current'],
});

const customSwitch = (oldEle, newEle) => {
  oldEle.classList.remove('current');
  newEle.classList.add('current');
  oldEle.after(newEle);

  return new Promise((resolve) => {
    // Assume there's animations start right after inserting to DOM.
    newEle.addEventListener('animationend', () => {
      oldEle.remove();
      resolve();
    }, { once: true });
  });
};

NOTE: Pjax waits for the switches, but moves on to the next navigation, and forgets the previous one, on matter whether finished or not. Workarounds that try to block user actions may not work as well as hoped, because the user can always use the "back" button, and Pjax always reacts to it.

scripts

CSS selector used to target extra <script> to execute after a page switch. For multiple selectors, separate them by a comma. Use the empty string to target nothing. Like:

// Single selector
const pjax = new Pjax({
  scripts: 'script.pjax',
});
// Multiple selectors
const pjax = new Pjax({
  scripts: 'script.pjax, script.analytics',
});
// Only execute new scripts
const pjax = new Pjax({
  scripts: '',
});

NOTE: Pjax always executes the newly added scripts in a page switch. You don't have to mark them here.

scrollTo

When set to a number, this represents the vertical value (in px from the top of the page) to scroll to after a page switch.

When set to an array of 2 numbers ([x, y]), this contains the horizontal and vertical values to scroll to after a page switch.

Set this to true to make Pjax decide the scroll position. Pjax will try to act as the browsers' default behavior. For example, scroll the element into view when hash changing to its id, scroll to page left top when navigating to a new page without a valid hash.

Set this to false to disable all scrolling by Pjax.

NOTE: This does not affect the scroll restoration defined below.

scrollRestoration

When set to true, Pjax attempts to restore the page scroll status when navigating backward or forward.

cache

This contains the cache mode of Pjax requests, which shares the same available values with Request.cache.

timeout

The time in milliseconds to abort the fetch requests. Set to 0 to disable.

Status

Accessible by calling on the Pjax instance.

Name Type Default Value
location URL new URL(window.location.href)
abortController AbortController | null null

location

The last location recognized by Pjax.

abortController

The abort controller that can abort the Pjax handling navigation. When Pjax handles nothing, null.

For example, to abort Pjax on certain events:

const pjax = new Pjax();

document.addEventListener('example', () => {
  pjax.abortController?.abort();
});

Events

When calling Pjax to load a URL that within the same origin while with different path or search string, Pjax fires a number of events.

All events fire from the document, not the clicked anchor nor the caller function. You can get more detail of the event via event.detail.

An ordered list showing the types of these events, and the moment they happen:

  1. pjax:send event when Pjax sends the request.
  2. Pjax switches the DOM. See switchDOM method for details.
  3. pjax:error event if any error happens to step 2.
  4. pjax:complete event when step 2 finishes.
  5. pjax:success event if step 2 finishes without any error.

If you use a loading indicator (e.g. topbar), a pair of send and complete events may suit you well.

document.addEventListener('pjax:send', topbar.show);
document.addEventListener('pjax:complete', topbar.hide);

HTTP Headers

Pjax uses several custom headers when it sends HTTP requests.

  • X-Requested-With: Fetch
  • X-PJAX: true
  • X-PJAX-Selectors — A serialized JSON array of selectors, taken from selectors. You can use this to send back only the elements that Pjax will use to switch, instead of sending the whole page. Note that you may need to deserialize this on the server (Such as by using JSON.parse)

DOM Ready State

Most of the time, you will have code related to the current DOM that you only execute when the DOM became ready.

Since Pjax doesn't trigger the standard DOM ready events, you'll need to add code to re-trigger the DOM ready code. For example:

function whenDOMReady() {
  // do your stuff
}

document.addEventListener('DOMContentLoaded', whenDOMReady);

const pjax = new Pjax();

document.addEventListener('pjax:success', whenDOMReady);

NOTE: Don't instantiate Pjax in the whenDOMReady function.

Q&A

CONTRIBUTING

CHANGELOG

LICENSE