PJAX (PushState + Ajax) navigation functionality using the native Fetch API
Fetch Pjax uses AJAX (via the fetch
API) to deliver a super fast browsing experience by loading HTML from your server and replacing only the relevant portions of the page with the Ajax'd HTML. This means the browser doesn't have to reload the page CSS/JavaScript on each request (as it does on a normal page request) which therefore delivers lighting fast page loads.
Fetch Pjax provides full url, back button and history support via liberal usage of history.pushState
and the window.onpopstate
event.
- URL updating and management
- Back button support
- Internal "hash" link support (inc. scroll position restoration)
- Choose multiple DOM update targets
- Form handling (inc.
POST
/GET
andenctypes
) - Highly configurable - customise behaviour via options
- Extensible by design - use callbacks to achieve your unique requirements
- Full ability to customise Fetch request details
- Test coverage for all key features
// npm users
npm install fetch-pjax --save
// yarn users
yarn add fetch-pjax
then...
import FetchPjax from 'fetch-pjax'
const FetchPjax = require('fetch-pjax');
<script src="dist/fetch-pjax.umd.js" />
// FetchPjax is now available on global namespace
FetchPjax
expects that you are using an environment that supports the fetch
API. If your environment does not support fetch natively, you will need to include a suitable polyfill before usage.
By default FetchPjax
will run "config-less" - that is, it has a set of sensible defaults designed to account for the majority of use cases. However, it is designed to be highly configurable, allowing you to extend and modify it's behaviour to suit your needs.
The basic usage signature of a FetchPjax
is:
new FetchPjax(options); // options is an plain JS object {}
For many users, creating an instance of FetchPjax
will be enough to get PJAX working on your site.
The example below assumes you have imported/included the fetch-pjax
library.
new FetchPjax(); // sensible defaults applied
For the above example, all clicks on <a>
elements will now be intercepted and replaced by an AJAX request issued to the url defined by the anchor's href
attribute. Assuming that the response is valid HTML, it will be parsed and the relevant sections of the page will be updated with the new HTML retrived from the PJAX request. In addition the browser's address bar will reflect the new page url. In short, the page will appear and behave in a "normal" manner.
By default the sections of the DOM matching the following selectors will be updated on each PJAX request:
main
- the<main>
element of the page. Usually assumed to contain the page's main contenttitle
- the page's<title>
element from the<head>
As everyone's site is different, you can easily customise this by providing the targets
option. For example:
new FetchPjax({
targets: { // define which portions of the current page should be replaced
title: 'title'
content: '#main-content',
sidebar: 'aside.sidebar',
specialArea: '.my-special-area-selector'
}
});
Note that when you provide a targets
option, the defaults are overidden, so you will need to include them manually.
As suggested by the name, FetchPjax
utilises fetch
under the hood. In some cases you may wish to customise the options provided to the underlying fetch()
call for each PJAX request. To do this simply provide the fetchOptions
option. For example:
const token = btoa('someuser:somepassword');
new FetchPjax({
fetchOptions: {
headers: {
'X-PJAX': true, // we recommend retaining this identify PJAX requests
'Authorization: `Bearer ${token}`' // send HTTP auth headers with every request
}
}
});
These options will be set on every PJAX request. If you need to update the options on a per request basis consider utilising the modifyFetchOptions
option.
FetchPjax
provides a number of callback functions which you can use to hook into key events in the plugin execution lifecycle. Events include:
onBeforePjax
onSuccessPjax
onErrorPjax
onCompletePjax
onBeforeRender
onAfterRender
onBeforeTargetRender
onAfterTargetRender
Making use of a callback is easy. Simply provide an object as the callbacks
option. For example:
new FetchPjax({
callbacks: {
onBeforePjax: () => {
// do something here before PJAX is dispatched
},
onErrorPjax: () => {
// do something here when PJAX encounters an error
}
}
});
Different callbacks receive different arguments. For more on callbacks, see the callback documentation.
The following options can be provided to customise the functionality of FetchPjax
.
Option | Type | Default | Description |
---|---|---|---|
autoInit |
boolean |
true |
determines whether FetchPjax should initialise itself by default when a new instance is created. Useful for situations where you might need to defer execution. |
eventType |
string |
click |
defines the event type on which to listen to trigger a PJAX cycle. For example, specifying touchstart would then only trigger PJAX for touchstart events. |
selector |
string |
a |
a valid DOM selector for the elements on which you wish to listen for clicks (or other valid eventType ) |
ignoreSelector |
string |
[data-fetch-pjax-ignore] |
a valid DOM selector for the elements which you wish to exclude from being handled by PJAX. |
handleForms |
boolean |
true |
determines whether or not FetchPjax should handle Form submit events |
formSelector |
string |
form |
a valid DOM selector for any Forms within the document which you wish to be handeled by PJAX (only applied if handleForms is truthy) |
targets
| object
| { content: 'main', title: 'title' }
| defines which portions of the page should be replaced by the Ajax'd content on each PJAX request. The object keys must be unique, but are entirely arbitary and can be anything that helps you to identify them. The object values must be a valid DOM selector for an element which must exist within the page DOM at the point of creating a FetchPjax
instance
popStateFauxLoadTime
| int
| 300
| a period (in milliseconds) to wait before cached content retrieved from the history.state
is used to update the DOM. This is intended to improve the user experience by simulating a network request delay. Without this content is replaced too quickly to be perceivable. Only utilised when the popStateUseContentCache
option is set to true
fetchOptions
| object
| { headers: { 'X-PJAX': true } }
| sets the options argument sent to the underlying fetch(url, options={})
call for each PJAX request. Must be a valid options object as provided to the second init
argument of the Fetch API. To overide on a per-request basis, see modifyFetchOptions
popStateUseContentCache
| boolean
| true
| determines whether to cache visted urls request HTML in the browser history.state
object for faster
trackInitialState
| boolean
| true
| determines whether the intial page is added to the window.history
state using history.replaceState()
. You probably don't want to disable this.
callbacks
| object
| - | see Callbacks
FetchPjax provides callback functions which run at Useful points in it's execution cycle. Callback functions should be passed as part of the callbacks
option object (see Example Callback Usage)
Note: all callbacks (with the exception of modifyFetchOptions
) are passed the current instance
of FetchPjax
as their first argument.
Callback | Args | Description |
---|---|---|
modifyFetchOptions |
fetchOptions (object) |
provides the ability to conditionally modify the fetchOptions object on a per request basis. You can conditionally choose to return an objects object from your supplied callback function. When you do, this object will be used as the [second] init options argument of the fetch() call for the PJAX request (refer to the Fetch API documentation for details) |
onBeforePjax |
instance [, fetchOptions (object) ] |
called before the PJAX request is fired. fetchOptions object may be undefined if called as part of popState cache handling |
onSuccessPjax |
instance , url (string), html (string) |
called upon the completion of the PJAX request when the response was successful. Receives the request url and the response html |
onErrorPjax |
instance , error (object) |
called upon the completion of the PJAX request when the request resulted in an error. Called with error object containing status and statusText properties |
onCompletePjax |
instance |
always called upon the completion of a PJAX request regardless of whether result is success or error . Useful for logic that must always run post-PJAX cycle. Called after onSuccessPjax and onErrorPjax . |
onBeforeRender |
instance |
called before the start of a render cycle (where the targets are updated in the DOM) |
onAfterRender |
instance |
called after the end of a render cycle (where the targets are updated in the DOM) |
onBeforeTargetRender |
instance , targetKey (string) , targetEl (DOM Node) , contentEl (DOM Node) [, renderer (function OR undefined)] |
called before an individual target is updated in the DOM. Called with the targetKey being updated, the targetEl in the page DOM, the contentEl in the Ajax'd content and [if it was provided] the custom renderer function. |
onAfterTargetRender |
instance , targetKey (string) , targetEl (DOM Node) , contentEl (DOM Node) [, renderer (function OR undefined)] |
called after an individual target is updated in the DOM. Called with the targetKey being updated, the targetEl in the page DOM, the contentEl in the Ajax'd content and [if it was provided] the custom renderer function. |
new FetchPjax({
callbacks: {
onBeforePjax: (instance, fetchOptions) => {
// do something here
},
onCompletePjax: (instance) => {
// do something here always
}
}
});
Problem: you need to modify the options passed to fetch()
on a per request basis.
Solution: utilise the modifyFetchOptions
option to conditionally modify the fetchOptions
based on the request.
Example:
new FetchPjax({
modifyFetchOptions: (fetchOptions) => {
// Set different fetchOptions for different urls
if (fetchOptions.url.includes('foo')) {
return { // notice we must return the object
headers: {
'X-SPECIAL-HEADER': true // note this will be *merged* into the default headers object
}
};
}
if (fetchOptions.url.includes('bar')) {
return { // notice we must return the object
headers: {
'X-SOME-DIFFERENT-HEADER': true // note this will be merged into the default headers object
}
};
}
}
});
Notes: the object returned from modifyFetchOptions
is merged into the existing fetchOptions
object. It will not overwrite the original fetchOptions
. Rather it is deep-merged into it using assign-deep
to ensure nested objects are retained.
To contribute to the development of FetchPjax first ensure you have node
installed on your system. Also ensure you have either npm
or yarn
available.
A integration test suite is available via Cypress.io. This should be run continuous during development to ensure your modifications do not adversely effect the existing functionality.
To start the tests run npm run test:dev
- a server will start and Cypress will load shortly after. In another terminal window start rollup
in watch
mode using npm run dev
. Start developing!
All new functionality must be tested, following the existing conventions for guidance.
See CHANGELOG.md
.
FetchPjax is the brain child of David Smith
- Twitter - @get_dave
- Website - aheadcreative.co.uk
- Github - https://github.com/getdave/
Distributed under the MIT license. See LICENSE
for more information.
- Ability to designate certain origins as being allowed for cross original requests - currently all cross origins blocked
- Reduce bundle size (currently too big)
- Focus management
- Accessibility testing and improvements (
aria-live
?) - Centralise PJAX logic and avoid multiple conditionals and optional handling across various methods
This project uses Gitflow. All pull requests should be branched from the develop
branch only.
- Fork it
- Checkout the
develop
branch - Create your feature branch (
git checkout -b feature/myfeature
) - Commit your changes (
git commit -am 'Add some myfeature'
) - Push to the branch (
git push origin feature/myfeature
) - Create a new Pull Request