Show start page by default

This is the default start page. It is so because the page ID is set to 'start'. Every other element with a page binding (data-bind="page: {id: ...}") will be hidden by default. Set title: 'Some Title' if you want to update the page title as the user navigates to the page (look up at the title as you navigate this tutorial).

<div data-bind="page: {id: 'start', title: 'Pager.js Demo Start'}">
    ...<a href="#structure">Read about the structure.</a>...
</div>
        

Structure

A page is an element with a page custom binding. They can be placed anywhere in your site and will work as pages that you can jump between. The ID of the page corresponds to the fragment identifier in the URL (the thingy after #).
All the main pages in this demo are placed inside a main container div.

<html>
<body>
<div class="container" style="padding-top: 30px;">

    <div data-bind="page: {id: 'start'}">
        <!-- the previous page -->
    </div>
    <div data-bind="page: {id: 'structure'}">
        <!-- this page -->
    </div>
</div>
</body>
</html>
        

Setup

Pager.js depends on KnockoutJS, jQuery and Underscore.js (or Lo-Dash). Include either pager.js or pager.min.js. Choose between a naïve history manager (pager.start([id: String])), a history manager based on jQuery hashchange (pager.startHashChange([id: String])), a history manager based on History.js (pager.startHistoryJs([id: String])) or write your own.

Naïve history manager

// extend your view-model with pager.js specific data
pager.extendWithPage(viewModel);
// apply the view-model using KnockoutJS as normal
ko.applyBindings(viewModel);
// start pager.js
pager.start();
    

jQuery Hashchange as history manager

// use #!/ instead of the default #
pager.Href.hash = '#!/';
// extend your view-model with pager.js specific data
pager.extendWithPage(viewModel);
// apply the view-model using KnockoutJS as normal
ko.applyBindings(viewModel);
// start pager.js
pager.startHashChange();
    

History.js as history manager

// use HTML5 history
pager.useHTML5history = true;
// use History instead of history
pager.Href5.history = History;
// extend your view-model with pager.js specific data
pager.extendWithPage(viewModel);
// apply the view-model using KnockoutJS as normal
ko.applyBindings(viewModel);
// start pager.js
pager.startHistoryJs();
    

Deep Navigation

You can navigate into pages in pages, or pages in pages in pages. Just set the href to the same ID as the page.

This is the default sub page. Show the second page.
This is the second sub page. Go back to the first sub page.
<div data-bind="page: {id: 'deep_navigation'}">
    ...
    <div data-bind="page: {id: 'start'}">
        ...
        <a href="#deep_navigation/second">Show the second page</a>.
    </div>
    <div data-bind="page: {id: 'second'}">
        ...
        <a href="#deep_navigation">Go back to the first sub page</a>.
    </div>
    ...<a href="#load_external_content">Go on</a>...
</div>
        

Relative path

It can be tricky to calculate the absolute path of a link - especially if you know the page will be loaded form somewhere else.
Just use the page-href custom binding instead of specifying a href directly!
The page-href link will be relative to the page where the element is. So if an anchor tag is in the page "user" a value pelle will be resolved to user/pelle. The binding also understands ../ if you need to track back up the page hierarchy!
pager.Href.hash is by default # but can be changed to #!/ (or any other string). All page-href bindings will use the prefix from pager.Href.hash.

Close this tab
<div data-bind="page: {id: 'start'}">
    <ul class="nav nav-tabs">
        <li class="active">
            <a data-bind="page-href: 'fry'">Fry</a>
        </li>
        <li class="active">
            <a data-bind="page-href: 'interesting'">Interesting</a>
        </li>
    </ul>

    <div data-bind="page: {id: 'fry'}">
        <img src="fry.jpg"/>
    </div>
    <div data-bind="page: {id: 'interesting'}">
        <a data-bind="page-href: '../../'">Close this tab</a>
        <br/>
        <img src="interesting.jpg"/>
    </div>
</div>

        

HTML5 History API

The easiest way to use the HTML5 History API is to include History.js. Five lines of code is all it takes after that!

// use HTML5 history
pager.useHTML5history = true;
// use History instead of history
pager.Href5.history = History;

// these two lines are as normal
pager.extendWithPage(viewModel);
ko.applyBindings(viewModel);

// start pager.js
pager.startHistoryJs();

Change Binding Context

Use with: to change the binding context - similar to the standard KnockoutJS with binding.

<div data-bind="page: {id: 'user', 'with': user}">
    <span data-bind="text: name"></span>
</div>
<script type="text/javascript">
    viewModel = {
        user:{
            name:ko.observable("Amy")
        }
    };
</script>
    

Change Binding Context on Show

When constructing an application with multiple pages, you might want to delay data binding the pages until they are first displayed. Use withOnShow: to asynchronously data bind a page when it is first displayed.

Show page and lazy load view model

<div data-bind="page: {id: 'start'}">
    <p>
        <a class="btn" data-bind="page-href: '../invention'">Show page and lazy load view model</a>
    </p>
</div>

<div class="well" data-bind="page: {id: 'invention', withOnShow: requireVM('invention')}">
    <h3 data-bind="text: name"></h3>
    <p data-bind="text: description"></p>
    <a data-bind="page-href: '../'">Hide this page</a>
</div>

<script type="text/javascript>
var requireVM = function (module) {
    return function (callback) {
        require([module], function (mod) {
            callback(mod.getVM());
        });
    };
};
</script>
    

Matching Wildcards

Setting the id: property to '?' will make it match anything. This is a useful catch-all with uses like error handling.

Go to some random site

Error

The page you requested does not exist.

Go back

<div data-bind="page: {id: '?'}">
    <h3>Error</h3>
    <p>The page you requested does not exist.</p>
</div>
    

Deep Navigation with Wildcards

The wildcard does not need to be at the end. It can be in a sub-page with sub-pages.

Go to Leela

Character

Leela
<div data-bind="page: {id: 'start'}">
    <a class="btn" href="#deep_navigation_with_wildcards/wild/leela">Go to Leela</a>
</div>

<div data-bind="page: {id: '?'}">
    <h3>Character</h3>

    <div data-bind="page: {id: 'leela'}">
        Leela
    </div>
</div>
    

Sending the Wildcard to Source

The page ID that matched the wildcard can be sent to the source: or sourceOnShow:. Just add {1} inside the source: or sourceOnShow: and {1} will be replaced by the page ID.

Go to Fry

Character

<div data-bind="page: {id: 'start'}">
    <a href="#send_wildcard_to_source/character/fry">Go to Fry</a>
</div>

<div data-bind="page: {id: 'character'}">
    <h3>Character</h3>

    <div data-bind="page: {id: '?', sourceOnShow: 'character/{1}.html'}">
    </div>
</div>
    

Deep Load Content into Wildcard

Normally a sourceOnShow will only use the first page of the route as its URL, but by specifying deep: true the entire route will be used to identify the content to load. This makes it possible to load arbitrarily nested content into the same page-element.

Go to Fry

Character

<div data-bind="page: {id: 'start'}">
    <a href="#deep_load_content_into_wildcard/deep/character/fry">Go to Fry</a>
</div>

<div data-bind="page: {id: 'deep'}">
    <h3>Character</h3>

    <div data-bind="page: {id: '?', deep: true, sourceOnShow: '{1}.html'}">
    </div>
</div>
    

Loading Content into iframe

Sometimes it can be sound to use iframes instead of loading content directly into the page.
Just use frame: 'iframe' to load the content from source: or sourceOnShow: into an iframe.

<div data-bind="page: {id: 'start', frame: 'iframe', source: 'character/fry.html'}"></div>
    

Configure an iframe

Often you might need to configure the iframe element used. Just include an iframe element inside the page and that iframe will be used.

<div data-bind="page: {id: 'start', frame: 'iframe', source: 'character/fry.html'}">
    <iframe sandbox="" height="100" width="920"></iframe>
</div>
    

Custom Widgets

pager.js is based on sound object oriented principles, making it easy to extend with new custom widgets.
In this tutorial the custom binding page-accordion-item is demonstrated.

The page-accordion-item works like

Zoidberg
Zoidberg Information
Hermes
Hermes Information
and is constructed with the markup
<div data-bind="page-accordion-item: {id: 'zoidberg'}">
    <a href="#custom_widgets/start/zoidberg">Zoidberg</a>

    <div>Zoidberg Information</div>
</div>
<div data-bind="page-accordion-item: {id: 'hermes'}">
    <a href="#custom_widgets/start/hermes">Hermes</a>

    <div>Hermes Information</div>
</div>
    

First we need to create a class that inherits from pager.Page.

pager.PageAccordionItem = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    pager.Page.apply(this, arguments);
};
pager.PageAccordionItem.prototype = new pager.Page();
    

The Page class has these methods to override:

init : void
getValue : void
getPage : void
sourceUrl : String
loadSource : String
show: void
showElement: void
hideElement : void
    

We want our custom binding to always show the first child element and only hide the second child element. Thus we need to override showElement and hideElement. showElement should show the second child and hideElement should hide the second child.

// get second child
pager.PageAccordionItem.prototype.getAccordionBody = function () {
    return $(this.element).children()[1];
};

// hide second child
pager.PageAccordionItem.prototype.hideElement = function () {
    // use hide if it is the first time the page is hidden
    if (!this.pageAccordionItemHidden) {
        this.pageAccordionItemHidden = true;
        $(this.getAccordionBody()).hide();
    } else { // else use a slideUp animation
        $(this.getAccordionBody()).slideUp();
    }
};

// show the second child using a slideDown animation
pager.PageAccordionItem.prototype.showElement = function () {
    $(this.getAccordionBody()).slideDown();
};
    

Finally you need to add the new class as a new custom binding.

ko.bindingHandlers['page-accordion-item'] = {
    init:function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var pageAccordionItem = new pager.PageAccordionItem(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
        pageAccordionItem.init();
    },
    update:function () {
    }
};
   

Modals

It is possible to navigate into pages that are further up but still give the impression you are navigating deeper down. This is useful for modal windows that you might want to show on multiple pages. Just append modal: true and the page is accessible for sub-pages to sibling-pages.

Show first sub-page Show second sub-page
First sub-page Show an alert!
Second sub-page Show an alert!
<div data-bind="page: {id: 'start'}">
    <a data-bind="page-href: 'first'" class="btn">Show first sub-page</a>
    <a data-bind="page-href: 'second'" class="btn">Show second sub-page</a>

    <div data-bind="page: {id: 'first'}" class="well">
        First sub-page
        <a class="btn" data-bind="page-href: 'alert'">Show an alert!</a>
    </div>
    <div data-bind="page: {id: 'second'}" class="well">
        Second sub-page
        <a class="btn" data-bind="page-href: 'alert'">Show an alert!</a>
    </div>
</div>
<div data-bind="page: {id: 'alert', modal: true}" class="modal hide">
    <div class="modal-header">
        <a data-bind="page-href: '../'" class="close" data-dismiss="modal" aria-hidden="true">×</a>

        <h3>Alert</h3>
    </div>
    <div class="modal-body">
        <p>Lorem Ipsum</p>
    </div>
    <div class="modal-footer">
        <a data-bind="page-href: '../'" class="btn">This button get an auto-calculated href based on modals current location</a>
    </div>
</div>
        
        
    

Custom JS when Navigating

There are 4 main alternatives when it comes to running custom JS:

  1. Listen to pager.childManager if you want to be notified whenever a navigation has happened. It is not recommended to try to stop the navigation at this point since it would involve changing the history.
  2. Bind to global click if you want to be notified when a navigation is about to happen and you want a chance to stop it. Just do not return true; in order to stop the navigation.
  3. Bind to local click if you want to control a certain navigation. This is probably what you want to do if you want to inject validation.
  4. Bind to local before/after show/hide if you just want to be notified about a navigation.
Show Fry
Fry Hide Fry

<div data-bind="page: {id: 'fry', beforeHide: beforeFryIsHidden, afterShow: afterFryIsDisplayed}">
    Fry
    <a class="btn" href="#custom_js_when_navigating">Hide Fry</a>
</div>
    
afterFryIsDisplayed:function () {
    $('body').css("background-color", "#FF9999");
}
beforeFryIsHidden:function () {
    $('body').stop().css("background-color", "#FFFFFF");
}
    

Custom Hide- and Show-methods

You might want to use a custom animation or custom behaviour for a page. This is easily done by using the showElement and hideElement-properties. These properties should be supplied with a method that takes 2 arguments: the first is the page and the second is a callback that should be executed.

Show Fry

Fry

Hide Fry
<div data-bind="page: {id: 'fry', showElement: showFry, hideElement: hideFry}">
    <h2>Fry</h2>
    <a class="btn" data-bind="page-href: '../'">Hide Fry</a>
</div>
    

where

showFry:function (page, callback) {
    $(page.element).fadeIn(1000, callback);
};
hideFry:function (page, callback) {
    $(page.element).fadeOut(1000, function () {
        $(page.element).hide();
        if (callback) {
            callback();
        }
    });
};
    

Loaders

It is possible to specify both locally and globally a loader to be used. Use loader: loader for specifying a loader for a page and pager.loader = function(page, element) { ... } for specifying a default-loader for all pages. The loader-method should return an object with 2 methods: load and unload.

Load Sabrepulse
<div data-bind="page: {id: 'Sabrepulse', title: 'Sabrepulse', loader: textLoader, sourceOnShow: 'https://embed.spotify.com/?uri=spotify:album:43tqiFDcU8JcMVSYj6NTi3'}">
    <iframe  width="300" height="380" frameborder="0" allowtransparency="true"></iframe>
</div>        
    

where

textLoader: function(page, element) {
    var loader = {};
    var txt = $("<div></div>",
        {text: 'Loading ' + page.getValue().title}
    );
    loader.load = function() {
        $(element).append(txt);
    };
    loader.unload = function() {
        txt.remove();
    };
    return loader;
}
    

Tab Panel

There is no special tab panel widget shipping with pager.js. It is however very easy to construct one using the children observableArray and isVisible observable property.

<div>
    <ul class="nav nav-tabs" data-bind="foreach: $page.children">
        <li data-bind="css: {active: isVisible}"><a data-bind="text: $data.val('title'), page-href: $data"></a></li>
    </ul>

    <div data-bind="page: {id: 'Slagsmålsklubben', title: 'Slagsmålsklubben', sourceOnShow: 'https://embed.spotify.com/?uri=spotify:album:66KBDVJnA6c0DjHeSZYaHb', frame: 'iframe'}" class="hero-unit">
        <iframe width="300" height="380" frameborder="0" allowtransparency="true"></iframe>
    </div>

    <div data-bind="page: {id: 'Binärpilot', title: 'Binärpilot', sourceOnShow: 'https://embed.spotify.com/?uri=spotify:album:67LKycg4jAoC06kZgjvbNd', frame: 'iframe'}" class="hero-unit">
        <iframe width="300" height="380" frameborder="0" allowtransparency="true"></iframe>
    </div>
</div>
    

Reacting to Failed Navigations

When pager or a page does not find a matching sub-page for a route a callback is called. Either set pager.navigationFailed or supply navigationFailed to the page configuration with a function that takes 2 arguments: first the page and second the missing route.
This makes it easy to identify missing pages for routes and take actions.

Go to random sub-page
<div data-bind="page: {id: 'random', navigationFailed: randomFailed}">
    <ul class="nav nav-tabs" data-bind="foreach: $page.children">
        <li data-bind="css: {active: isVisible}"><a
                data-bind="text: getId(), page-href: getId()"></a></li>
    </ul>

    <div data-bind="foreach: newChildren">
        <div data-bind="page: {id: childId}">
            <span data-bind="text: childId"></span>
        </div>
    </div>
</div>        
    
where
randomFailed:function (page, route) {
    viewModel.newChildren.push({
        childId:route[0]
    });
    page.showPage(route);
},
newChildren:ko.observableArray([])
    

Load View using Custom Method

In order to facilitate programming in the large it is useful to be able to extract views as separate components. These views should not be forced to be stored as HTML fragments or be loaded with jQuery.
Thus a way to inject custom views should be possible. This is done using the source- or sourceOnShow-properties. Just supply a method instead of a string!
These properties take a method that should take a pager.Page as first argument, a callback, and return nothing.

Load custom view using requireView-method
<div data-bind="page: {id: 'zoidberg', sourceOnShow: requireView('zoidberg')}"/>        
    
where
window.requireView = function(viewModule) {
    return function(page, callback) {
        require([viewModule], function(viewString) {
            $(page.element).html(viewString);
            callback();
        });
    };
};
    

Binding URI Parameters

A page is able to access the information in the current route and change the view-model.
Using params: ['x','y'] it is possible to pick up pseudo parameters from the URL and bind them as observable variables in the page. If the parameter already exists on the viewModel it is bound to that observable instead.
These parameters can even be nested!

Search for a $200 TV
@ $
See Details See Lorem Ipsum
Details about the search
Lorem Ipsum
<div data-bind="page: {id: 'search', params: ['product','price']}" class="well">
    <span data-bind="text: product"></span> @ $ <span data-bind="text: price"></span>
    <br/>

    <a class="btn" data-bind="page-href: 'details'">See Details</a>
    <a class="btn" data-bind="page-href: 'lorem'">See Lorem Ipsum</a>

    <div data-bind="page: {id: 'details'}">
        Details about the search
    </div>
    <div data-bind="page: {id: 'lorem'}">
        Lorem Ipsum
    </div>
</div>
    

Guards

Guards are methods that are run before the page navigation takes place and that can stop the navigation from displaying a certain page.
Use the property guard: someMethod to apply the guard. The method takes 3 parameters: page, route, and callback. If the callback is called the navigation takes place - otherwise it is stopped.
Use cases are login, validating steps in state machines, etc.
The reason the guard takes a callback as the third argument is simply because the guard might be asynchronous, e.g., accessing a webserver for login details or asking if a valid shopping cart exists, etc.

Go to admin panel
This page is only accessible if the user is logged in. Logout

You are not logged in. Check the checkbox and try again.


<div data-bind="page: {id: 'admin', guard: isLoggedIn}" class="well">
    This page is only accessible if the user is logged in.
    <a href="#guards" data-bind="click: logout">Logout</a>
</div>
<div data-bind="page: {id: 'login'}" class="well">
    <p class="alert alert-error">You are not logged in. Check the checkbox and try again.</p>

    <form>
        <label class="checkbox">Login <input type="checkbox" data-bind="checked: loggedIn"/> </label>
    </form>
</div>
    
where
loggedIn: ko.observable(false),
isLoggedIn: function(page, route, callback) {
    if(viewModel.loggedIn()) {
        callback();
    } else {
        window.location.href = "login";
    }
},