@sliphua/pjax
v2.4.0
Published
Easily enable fast AJAX navigation (using Fetch + pushState)
Downloads
186
Maintainers
Readme
ES6+ Pjax
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
- Listen to simple redirections.
- Fetch the target page via
fetch
. - Update current URL using
pushState
. - Render the DOM tree using
DOMParser
. - 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.
- Execute all newly-added or targeted scripts in document order.
- 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">
© 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:
- Call
pushState
to update the URL. - Transform elements selected by
selectors
with switches defined inswitches
. - Set focusCleared to
true
if previous step has cleared the page focus, otherwise,false
. - 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:
- Focus the first
autofocus
iffocusCleared
(in the given switches result) evaluates totrue
. - Execute all newly-loaded and targeted scripts in document order.
- Wait until the blocking scripts got executed (e.g., inline scripts).
- 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 | TriggerOptions | 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
hooks | Hooks | {}
defaultTrigger
When set to false
or an object with enable: 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');
});
Use the exclude
sub-option to disable the trigger only for specific elements:
const pjax = new Pjax({
defaultTrigger: {
exclude: 'a[data-no-pjax]',
},
});
Type TriggerOptions
interface TriggerOptions {
enable?: boolean,
exclude?: string,
}
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 asPjax.switches.replaceWith
.Pjax.switches.innerHTML
— Replace HTML contents by usingElement.innerHTML
.Pjax.switches.textContent
— Replace all text by usingNode.textContent
.Pjax.switches.innerText
— Replace readable text by usingHTMLElement.innerText
.Pjax.switches.attributes
— Rewrite all attributes, leaving inner HTML untouched.Pjax.switches.replaceWith
— Replace elements by usingChildNode.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.
hooks
This option defines hooks (each of type Hook) for modifying the request sent, the response received, the document parsed and the switchResult created in Pjax. An example for adding a custom header for Pjax requests:
const pjax = new Pjax({
hooks: {
request: (request) => {
request.headers.set('My-Custom-Header', 'ready');
},
},
});
Type Hook
A function that returns undefined
, the hooked input, or a promise resolving to one of them.
type Hook<T> = (input: T) => T | void | Promise<T | void>;
Type Hooks
interface Hooks {
request?: Hook<Request>;
response?: Hook<Response>;
document?: Hook<Document>;
switchesResult?: Hook<SwitchesResult>;
}
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, Pjax may fire 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:
pjax:send
event when Pjax sends the request.pjax:receive
event when Pjax receives the response.- Pjax switches the DOM. See
switchDOM
method for details. pjax:error
event if any error happens to previous steps.pjax:complete
event when previous steps finish.pjax:success
event if previous steps finish 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 fromselectors
. 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 usingJSON.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.