Partial Page Loading

1 Introduction

Partial Page Loading is a means to reduce the amount of data sent by the server to the client when navigating from one page to another. The new content is requested via AJAX.

Partial Page Loading is enabled with the following entry in conf/config.xml:

<config>
  <acceleration>
    <partial-page-loading/>
  </acceleration>
</config>

If Partial Page Loading is enabled, registering Adaptive Components conditionally in your flow may result in incomplete initialization of the respective Adaptive Components, e.g. if the destination page registers an AC that was not registered for the referring page, that AC’s JavaScript may be missing.

We strongly recommend registering ACs unconditionally if PPL is enabled.

Links (a elements) are not followed with Partial Page Loading

  • when marked with ai-ppl="false", or
  • when considered external according to ai.urls.isExternal().

Forms (form elements) are not submitted with Partial Page Loading

  • when marked with ai-ppl="false",
  • when the action is considered external according to ai.urls.isExternal(),
  • when the enctype is multipart/formdata and the Browser does not support the JavaScript FormData object

During the transition from one page to another, the child elements of the head element are replaced except for the following child elements: script, style, iframe and stylesheet link.

script elements, style elements and stylesheet link elements are only added if they have an ai-load attribute with the value true. If they have the same id attribute value as an element in the old head, the old element is replaced by the new one. Otherwise the new element is appended to the head.

PPL responses will only be sent to clients who support JavaScript. Some clients do not respect noscript elements if they are dynamically inserted, therefore all noscript elements are removed from PPL responses.

2.1 The ai-ppl attribute

The ai-ppl attribute can be used on a and form elements. Defined values are true (the default) and false. ai-ppl="false" prevents PPL to follow the link or submit the form.

Note: ai-ppl="true" does not cause links to be followed or forms to be submitted via PPL when partial-page-loading is not enabled in config.xml.

Note: The ai-ppl attribute appears as data-ai-ppl in the client-side DOM.

2.2 The ai-load attribute

The ai-load attribute can be used on script, style and stylesheet link elements. Defined values are true and false (the default). An element with ai-load="true" is added to the head element.

Note: The ai-load attribute appears as data-ai-load in the client-side DOM.

3 Partial Page Loading with Fragments

When a site consists of pages sharing content items (e.g. headers or footers), Partial Page Loading with Fragments can be used to further reduce the content sent by the server.

Example:

<body>
  <header>
    ... The header is the same on all pages ...
  </header>
  <ai-fragment id="article" provides="article_0">
    <article>
      ... The article content differs from page to page ...
    </article>
  </ai-fragment>
  <footer>
    ... The footer is the same on all pages ...
  </footer>
</body>

3.1 The ai-fragment element

Contexts in which this element can be used:
As a descendant of the body element
Content model:
Transparent
Content attributes:
id (required)
provides (optional)

The ai-fragment element wraps content that has to be replaced while navigating from one page to another in a container.

The id attribute identifies the fragment container. In addition to the restrictions for id attribute values from the HHTML5 specification, the value must not contain any EQUALS SIGN (‘=’) or SOLIDUS (‘/‘) characters.

The provides attribute identifies the content of the fragment container (without nested fragment containers). If the attribute is present its value must not be empty and must not contain any space, EQUALS SIGN (‘=’) or SOLIDUS (‘/‘) characters. If the provides attribute value for a fragment on two pages is the same, the fragment’s content will not be replaced; however, content of nested fragments may be replaced.

Note:

Start and end tags of the ai-fragment element are replaced by comments on the server. Therefore the two p elements in

...
<p id="outside">outside a fragment</p>
<ai-fragment id="myId" provides="myProvides">
  <p id="inside">inside a fragment</p>
</ai-fragment>
...

become siblings in the client DOM:

...
<p id="outside">outside a fragment</p>
<!--ai-fragment myId=myProvides-->
<p id="inside">inside a fragment</p>
<!--ai-fragment-end-->
...

When partial page loading is not enabled, ai-fragment start and end tags are removed.

3.2 Examples

Example 1

page 1:

<body>
  ...
  <p>This text is the same on all pages; it is not within an ai-fragment element.</p>
  ...
  <ai-fragment id="outer" provides="outer_1">
    <p>Some text in outer</p>
  </ai-fragment>
  ...
</body>

page 2:

<body>
  ...
  <p>This text is the same on all pages; it is not within an ai-fragment element.</p>
  ...
  <ai-fragment id="outer" provides="outer_2">
    <h2>A headline</h2>
    <p>This is some other text in the 'outer' fragment.</p>
  </ai-fragment>
  ...
</body>

When navigating from page 1 to page 2, only the content of fragment ‘outer’ is replaced.

Example 2

page 1:

<body>
  ...
  <p>This text is the same on all pages; it is not within an ai-fragment element.</p>
  ...
  <ai-fragment id="outer" provides="outer_1">
    <p>Some text in outer</p>
    <ai-fragment id="inner1" provides="p1_1">
      <p>Inner 1 - 1</p>
    </ai-fragment>
    <ai-fragment id="inner2" provides="p2_1">
      <p>Inner 2 - 1</p>
    </ai-fragment>
  </ai-fragment>
  ...
</body>

page 2:

<body>
  ...
  <p>This text is the same on all pages; it is not within an ai-fragment element.</p>
  ...
  <ai-fragment id="outer" provides="outer_1">
    <p>Some text in outer</p>
    <ai-fragment id="inner1" provides="p1_2">
      <p>Inner 1 - 2</p>
    </ai-fragment>
    <ai-fragment id="inner2" provides="p2_2">
      <p>Inner 2 - 2</p>
    </ai-fragment>
  </ai-fragment>
  ...
</body>

When navigating from page 1 to page 2, only the content of fragments ‘inner1’ and ‘inner2’ is replaced, because the values of their provides attributes differ. Since the ‘outer’ fragment’s provides attribute values on both pages are the same, this fragment’s content is not updated.

4 JavaScript API

The API script is loaded asynchronously. So testing for ai.ppl is no safe indicator for partial-page-loading being enabled.

This function starts a Partial Page Loading transition to a document with the given URL.

Signature:

ai.ppl.followLink(url)

Arguments:

url
{string} The URL of the link to follow.

Returns {boolean} true if the link was followed via Partial Page Loading, false otherwise.

4.2 Function ai.ppl.submitForm

This function submits the given form via Partial Page Loading.

Signature:

ai.ppl.submitForm(formElement)

Arguments:

formElement
{Element} The form element representing the form to submit.

Returns {boolean} true if the form was submitted via Partial Page Loading, false otherwise.

5 JavaScript Events

5.1 Initialization: pplready and pplinit

The pplready event is triggered immediately when the Partial Page Loading session is initialized, even before the load event.

The data parameter passed to listeners is of type ai.ppl.ReadyData. Properties:

startUrl
{string} The URL of the document initializing the Partial Page Loading session.

Note: Accessing the DOM on pplready in any way may cause a JavaScript error and the site may fail to load.

The pplinit event is triggered when the Partial Page Loading session is initialized and the DOM is ready (load event dependent).

The data parameter supplied for listeners is of type ai.ppl.InitData. Properties:

startUrl
{string} The URL of the document initializing the Partial Page Loading session.

Both, the pplready and the pplinit event are triggerOnRegister event types.

5.2 Navigation Start: pplnavstart

This event is triggered when the transition from one document to another starts. The data parameter passed to listeners is of type ai.ppl.NavStartData.

Properties:

oldUrl
{string} The old document’s URL.
newUrl
{string} The visible URL of the new document.
method
{string} The HTTP method to be used for the request (GET or POST).
formParams
{String[]|FormData} The form parameters either as an array of ‘name=value‘ strings, or a FormData object. This property is absent when triggered with ai.ppl.followLink().

For backwards-compatibility, PPL always fires a pplstart event together with a pplnavstart event. The data passed to listeners for pplstart and pplnavstart are the same.

5.3 Content is ready: pplcontentready

This event is triggered when the content of the new document is available. The data parameter passed to listeners is of type ai.ppl.ContentReadyData.

Properties:

oldNodes
{Node[]} Array containing the old body element or the top-level nodes of the old fragment (depending on the value of withFragments).
newNodes
{Node[]} Array containing the new body element or the top-level nodes of the new fragment (depending on the value of withFragments).
withFragments
{boolean} true if the fragment content is involved, false if the body element is involved.
Methods:
isDefaultPrevented()
Indicates whether the default replacement technique should be prevented. Returns {boolean}
preventDefault()
Prevents the default replacement technique.

5.4 Navigation End: pplnavend

This event is triggered when the transition from one document to another is finished. The data parameter passed to listeners is of type ai.ppl.NavEndData. The event data object contains everything the ai.ppl.EndData object contains.

Properties:

newUrl
{string} The URL of the new document.

For backwards-compatibility, PPL always fires a pplend event together with a pplnavend event. The data passed to listeners for pplend and pplnavend are the same.

5.5 Example

The following code shows a simple load overlay when transitioning from one document to another. This is just an example showing how PPL events can be used. It is not meant to be code ready to be used as shown in every circumstance.

CSS:

.loading { color: #000; background: #ddd; opacity: 0.7; width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
.loading h1 { text-align: center; margin-top: 30%; }

JS:

(function() {
  var
    /** @type Element */
    _load = null
    /** @type Element */
  , _body = null
  ;

  function _createLoad() {
    var
      /** @type Element */
      load = document.createElement('div')
      /** @type Element */
    , h = document.createElement('h1')
      /** @type Text */
    , loadText = document.createTextNode('Loading...')
    ;
    h.appendChild(loadText);
    load.setAttribute('class', 'loading');
    load.appendChild(h);
    return load;
  }

  /** * Handler for pplready. * * @param {ai.ppl.ReadyData} data The Partial Page Loading ready event data. */
  function _pplreadyHandler(data) {
    ai.addEventListener('pplnavstart', _pplnavstartHandler);
    ai.addEventListener('pplcontentready', _pplcontentreadyHandler);
    ai.addEventListener('pplnavend', _pplnavendHandler);
  }

  /** * Handler for pplinit. * * @param {ai.ppl.InitData} data The Partial Page Loading init event data. */
  function _pplinitHandler(data) {
    _load = _createLoad();
    _body = document.body;
  }

  /** * Handler for pplnavstart. * * @param {ai.ppl.NavStartData} data The Partial Page Loading navstart event data. */
  function _pplnavstartHandler(data) {
    _body.appendChild(_load);
  }

  /** * Removes the old body attributes. * * @param {Element} oldBody The old body element. */
  function _removeBodyAttributes(oldBody) {
    var
      /* @type NamedNodeMap */
      attrs = oldBody.attributes
    /* @type number */
    , l = attrs.length
    /* @type number */
    , i = 0
    ;
    for (i = 0; i < l; i++) {
      oldBody.removeAttribute(attrs.item(i));
    }
  }

  /** * Sets the new body attributes. * * @param {Element} oldBody The old body element. * @param {Element} newBody The new body element. */
  function _setNewBodyAttributes(oldBody, newBody) {
    var
      /* @type NamedNodeMap */
      attrs = newBody.attributes
    /* @type number */
    , l = attrs.length
    /* @type number */
    , i = 0
    /* @type Attr */
    , attr = null
    ;
    for (i = 0; i < l; i++) {
      attr = attrs.item(i);
      oldBody.setAttribute(attr.name, attr.value);
    }
  }

  /** * Handler for pplcontentready. * * @param {ai.ppl.ContentReadyData} data The Partial Page Loading contentready event data. */
  function _pplcontentreadyHandler(data) {
    var
      /* @type Element */
      newNode = data.newNodes[0]
    /* @type Element */
    , oldNodes = data.oldNodes[0]
    /* @type DocumentFragment */
    , df = null
    ;
    if (!data.withFragments) {
      /* no fragments */
      _removeBodyAttributes(oldNode);
      _setNewBodyAttributes(oldNode, newNode);
      df = document.createDocumentFragment();
      while (newNode.hasChildNodes()) {
        df.appendChild(newNode.firstChild);
      }
      while (oldNode.hasChildNodes() && oldNode.firstChild !== _load) {
        oldNode.removeChild(oldNode.firstChild);
      }
      oldNode.insertBefore(df, _load);
      data.preventDefault();
    }
  }

  /** * Handler for pplnavend. * * @param {ai.ppl.NavEndData} data The Partial Page Loading end event data. */
  function _pplnavendHandler(data) {
    _body.removeChild(_load);
  }

  ai.addEventListener('pplready', _pplreadyHandler);
  ai.addEventListener('pplinit', _pplinitHandler);
})();