Giter Site home page Giter Site logo

pjax's Introduction

ENGLISH, 简体中文

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 | 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 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.

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:

  1. pjax:send event when Pjax sends the request.
  2. pjax:receive event when Pjax receives the response.
  3. Pjax switches the DOM. See switchDOM method for details.
  4. pjax:error event if any error happens to previous steps.
  5. pjax:complete event when previous steps finish.
  6. 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 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.

pjax's People

Contributors

dependabot[bot] avatar paperstrike avatar yahav avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pjax's Issues

Wrong HTTPS status error messages are displayed in browser / Pjax repeats request even if HTML present

Describe the bug

Hi, this issue is caused by Pjax revisits as described here: #342

The problem is worse however. For example, when user uploads too large file, the server response "413 Request Entity Too Large" should be displayed to user. For example, this is returned by nginx:

<html>
<head><title>413 Request Entity Too Large</title></head>
<body>
<center><h1>413 Request Entity Too Large</h1></center>
<hr><center>nginx</center>
</body>
</html>

But now Pjax tries to revisit that URL and that will produce this error instead:

Oops! An Error Occurred
The server returned a "405 Method Not Allowed".

The core of the problem is that Pjax makes additional request without user consent (revisit).

To reproduce

  1. Make any form POST request to URL in a way that generates status code >=400 and where the URL is POST-only
  2. See the error message
  3. The error message is wrong (Pjax tries to GET that URL which is POST-only)

Expected behavior

  • Correct server error message should be displayed in the browser.
  • Pjax should not repeat a request if there is HTML content to show in the first response.

Possible solutions

Pjax probably checks mime type or something in header or status code to decide if to repeat request.

One solution to this problem is to check if there is any HTML content even if there is error status and then NOT repeat request (revisit) and instead run document.write(htm_error). Or user-provided handler for error output can be run (where I would call document.write and show the HTML of the error message).

Module not found: Error: Package path ./dist/pjax.esm.js is not exported from package

Describe the bug

After upgrading to Laravel Mix 6/PostCSS 8/Webpack 5.
When trying to comple i'm getting the following error:

ERROR in ./resources/js/services/sliphuaPjax.js 7:0-50
Module not found: Error: Package path ./dist/pjax.esm.js is not exported from package C:\projectName\node_modules\@sliphua\pjax (see exports field in C:\projectName\node_
modules\@sliphua\pjax\package.json)

At /resources/js/services/sliphuaPjax.js i simply import the package as stated at the docs:
import Pjax from '@sliphua/pjax/dist/pjax.esm.js

It used to work perfectly up until the point in which i've upgraded Laravel Mix/Webpack.

To reproduce

No response

Expected behavior

No response

Environment

- OS: Windows 10
- Browser: Not relevant
- Pjax: Latest

Anything else?

No response

Pjax visits URL after server returned error 500 on form submission

Describe the bug

Hi, when a form is submitted and server return code 500 then Pjax tries to visit the form URL with GET again. This can result in 405 Method not allowed. See:

Screenshot from 2022-07-30 15-41-12

To reproduce

  1. Submit a form with method POST and URL with Pjax
  2. Return 500 from server
  3. See that Pjax visits the URL with GET method

Expected behavior

Response is displayed and no second request is made.

Environment

- Browser: Firefox ESR 91.12.0
- Pjax: 2.4.0

Anything else?

No response

关于逻辑

目前对标签中带有target="_blank"节点的也会默认进行pjax跳转
但我认为这是不合理的
因为这个属性本义就是打开新的窗口.
虽然最后也会正常跳转 但是也多了一个fetch请求

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.