History navigation example

This is an example of a website composed only of three pages (first_page.php, second_page.php and third_page.php). To see how it works, please create the following files (or git clone https://github.com/giabao/mdn-ajax-nav-example.git ):

Note: For fully integrating the <form> elements within this mechanism, please take a look at the paragraph Submitting forms and uploading files.

first_page.php:

php

<?php
    $page_title = "First page";

    $as_json = false;
    if (isset($_GET["view_as"]) && $_GET["view_as"] == "json") {
        $as_json = true;
        ob_start();
    } else {
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<?php
        include "include/header.php";
        echo "<title>" . $page_title . "</title>";
?>
</head>

<body>

<?php include "include/before_content.php"; ?>

<p>This paragraph is shown only when the navigation starts from <strong>first_page.php</strong>.</p>

<div id="ajax-content">
<?php } ?>

    <p>This is the content of <strong>first_page.php</strong>.</p>

<?php
    if ($as_json) {
        echo json_encode(array("page" => $page_title, "content" => ob_get_clean()));
    } else {
?>
</div>

<p>This paragraph is shown only when the navigation starts from <strong>first_page.php</strong>.</p>

<?php
        include "include/after_content.php";
        echo "</body>\n</html>";
    }
?>

second_page.php:

php

<?php
    $page_title = "Second page";

    $as_json = false;
    if (isset($_GET["view_as"]) && $_GET["view_as"] == "json") {
        $as_json = true;
        ob_start();
    } else {
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<?php
        include "include/header.php";
        echo "<title>" . $page_title . "</title>";
?>
</head>

<body>

<?php include "include/before_content.php"; ?>

<p>This paragraph is shown only when the navigation starts from <strong>second_page.php</strong>.</p>

<div id="ajax-content">
<?php } ?>

    <p>This is the content of <strong>second_page.php</strong>.</p>

<?php
    if ($as_json) {
        echo json_encode(array("page" => $page_title, "content" => ob_get_clean()));
    } else {
?>
</div>

<p>This paragraph is shown only when the navigation starts from <strong>second_page.php</strong>.</p>

<?php
        include "include/after_content.php";
        echo "</body>\n</html>";
    }
?>

third_page.php:

php

<?php
    $page_title = "Third page";
    $page_content = "<p>This is the content of <strong>third_page.php</strong>. This content is stored into a PHP variable.</p>";

    if (isset($_GET["view_as"]) && $_GET["view_as"] == "json") {
        echo json_encode(array("page" => $page_title, "content" => $page_content));
    } else {
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<?php
        include "include/header.php";
        echo "<title>" . $page_title . "</title>";
?>
</head>

<body>

<?php include "include/before_content.php"; ?>

<p>This paragraph is shown only when the navigation starts from <strong>third_page.php</strong>.</p>

<div id="ajax-content">
<?php echo $page_content; ?>
</div>

<p>This paragraph is shown only when the navigation starts from <strong>third_page.php</strong>.</p>

<?php
        include "include/after_content.php";
        echo "</body>\n</html>";
    }
?>

css/style.css:

css

#ajax-loader {
  position: fixed;
  display: table;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

#ajax-loader > div {
  display: table-cell;
  width: 100%;
  height: 100%;
  vertical-align: middle;
  text-align: center;
  background-color: #000000;
  opacity: 0.65;
}

include/after_content.php:

php

<p>This is the footer. It is shared between all Ajax pages.</p>

include/before_content.php:

php

<p>
[ <a class="ajax-nav" href="first_page.php">First example</a>
| <a class="ajax-nav" href="second_page.php">Second example</a>
| <a class="ajax-nav" href="third_page.php">Third example</a>
| <a class="ajax-nav" href="unexisting.php">Unexisting page</a> ]
</p>

include/header.php:

html

<meta charset="UTF-8" />
<script src="js/ajax_nav.js"></script>
<link rel="stylesheet" href="css/style.css" />

js/ajax_nav.js:

js

"use strict";

const ajaxRequest = new (function () {
    let req;
    let isLoading = false;
    let updateURL = false;

    /* customizable constants */
    const targetId = "ajax-content";
    const viewKey = "view_as";
    const ajaxClass = "ajax-nav";

    /* not customizable constants */
    const searchRegex = /\?.*$/;
    const hostRegex = /^[^?]*\?*&*/;
    const viewRegex = new RegExp(`&${viewKey}\\=[^&]*|&*$`, "i");
    const endQstMarkRegex = /\?$/;
    const loadingBox = document.createElement("div");
    const cover = document.createElement("div");
    const loadingImg = new Image();
    const pageInfo = {
        title: null,
        url: location.href,
    };
    /* http://www.iana.org/assignments/http-status-codes/http-status-codes.xml */
    const HTTP_STATUS = {
        100: "Continue",
        101: "Switching Protocols",
        102: "Processing",
        200: "OK",
        201: "Created",
        202: "Accepted",
        203: "Non-Authoritative Information",
        204: "No Content",
        205: "Reset Content",
        206: "Partial Content",
        207: "Multi-Status",
        208: "Already Reported",
        226: "IM Used",
        300: "Multiple Choices",
        301: "Moved Permanently",
        302: "Found",
        303: "See Other",
        304: "Not Modified",
        305: "Use Proxy",
        306: "Reserved",
        307: "Temporary Redirect",
        308: "Permanent Redirect",
        400: "Bad Request",
        401: "Unauthorized",
        402: "Payment Required",
        403: "Forbidden",
        404: "Not Found",
        405: "Method Not Allowed",
        406: "Not Acceptable",
        407: "Proxy Authentication Required",
        408: "Request Timeout",
        409: "Conflict",
        410: "Gone",
        411: "Length Required",
        412: "Precondition Failed",
        413: "Request Entity Too Large",
        414: "Request-URI Too Long",
        415: "Unsupported Media Type",
        416: "Requested Range Not Satisfiable",
        417: "Expectation Failed",
        422: "Unprocessable Content",
        423: "Locked",
        424: "Failed Dependency",
        425: "Unassigned",
        426: "Upgrade Required",
        427: "Unassigned",
        428: "Precondition Required",
        429: "Too Many Requests",
        430: "Unassigned",
        431: "Request Header Fields Too Large",
        500: "Internal Server Error",
        501: "Not Implemented",
        502: "Bad Gateway",
        503: "Service Unavailable",
        504: "Gateway Timeout",
        505: "HTTP Version Not Supported",
        506: "Variant Also Negotiates (Experimental)",
        507: "Insufficient Storage",
        508: "Loop Detected",
        509: "Unassigned",
        510: "Not Extended",
        511: "Network Authentication Required",
    };

    function closeReq() {
        loadingBox.parentNode && document.body.removeChild(loadingBox);
        isLoading = false;
    }
    req.abort();
    closeReq();
  }

  function ajaxError() {
    alert("Unknown error.");
  }

  function ajaxLoad() {
    let msg;
    const status = this.status;
    switch (status) {
      case 200:
        msg = JSON.parse(this.responseText);
        document.title = pageInfo.title = msg.page;
        document.getElementById(targetId).innerHTML = msg.content;
        if (updateURL) {
          history.pushState(pageInfo, pageInfo.title, pageInfo.url);
          updateURL = false;
        }
        break;
      default:
        msg = `${status}: ${HTTP_STATUS[status] || "Unknown"}`;
        switch (Math.floor(status / 100)) {
          /*
                    case 1:
                        // Informational 1xx
                        console.log("Information code " + vMsg);
                        break;
                    case 2:
                        // Successful 2xx
                        console.log("Successful code " + vMsg);
                        break;
                    case 3:
                        // Redirection 3xx
                        console.log("Redirection code " + vMsg);
                        break;
                    */
          case 4:
            /* Client Error 4xx */
            alert(`Client Error #${msg}`);
            break;
          case 5:
            /* Server Error 5xx */
            alert(`Server Error #${msg}`);
            break;
          default:
            /* Unknown status */
            ajaxError();
        }
    }
    closeReq();
  }

  function filterURL(url, viewMode) {
    return (
      url.replace(searchRegex, "") +
      `?${url
        .replace(hostRegex, "&")
        .replace(viewRegex, viewMode ? `&${viewKey}=${viewMode}` : "")
        .slice(1)}`.replace(endQstMarkRegex, "")
    );
  }

  function getPage(page) {
    if (isLoading) {
      return;
    }
    req = new XMLHttpRequest();
    isLoading = true;
    req.onload = ajaxLoad;
    req.onerror = ajaxError;
    if (page) {
      pageInfo.url = filterURL(page, null);
    }
    req.open("get", filterURL(pageInfo.url, "json"), true);
    req.send();
    loadingBox.parentNode || document.body.appendChild(loadingBox);
  }

  function requestPage(url) {
    if (history.pushState) {
      updateURL = true;
      getPage(url);
    } else {
      /* Ajax navigation is not supported */
      location.assign(url);
    }
  }

  function processLink() {
    if (this.className === ajaxClass) {
      requestPage(this.href);
      return false;
    }
    return true;
  }

  function init() {
    pageInfo.title = document.title;
    history.replaceState(pageInfo, pageInfo.title, pageInfo.url);
    for (const link of document.links) {
      link.onclick = processLink;
    }
  }

  loadingBox.id = "ajax-loader";
  cover.onclick = abortReq;
  loadingImg.src =
    "data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==";
  cover.appendChild(loadingImg);
  loadingBox.appendChild(cover);

  onpopstate = (event) => {
    updateURL = false;
    pageInfo.title = event.state.title;
    pageInfo.url = event.state.url;
    getPage();
  };

  window.addEventListener("load", init, false);

  // Public methods

  this.open = requestPage;
  this.stop = abortReq;
  this.rebuildLinks = init;
})();

For more information, please see: Working with the History API.

See also