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>
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>
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.
// 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();
// 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();
// 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();
You can navigate into pages in pages, or pages in pages in pages.
Just set the href to the same ID as the 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>
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.
<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>
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();
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>
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.
<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>
Setting the id: property to '?' will make it
match anything. This is a useful catch-all with uses like error handling.
<div data-bind="page: {id: '?'}">
<h3>Error</h3>
<p>The page you requested does not exist.</p>
</div>
The wildcard does not need to be at the end. It can be in a sub-page with sub-pages.
<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>
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.
<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>
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.
<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>
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>
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>
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
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 () {
}
};
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.
<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>
There are 4 main alternatives when it comes to running custom JS:
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.
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.
click if you want to control a certain navigation. This is probably what you
want to do if you want to inject validation.
<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");
}
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.
<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();
}
});
};
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.
<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;
}
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>
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.
<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([])
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.
<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();
});
};
};
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!
<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 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.
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";
}
},