Saturday, 1 February 2014

jQuery Mobile: document ready vs page events

Related:

This article can also be found as a part of my blog HERE.

$(document).on('pageinit') vs $(document).ready()

The first thing you learn in jQuery is to call code inside the $(document).ready() function so everything will execute as soon as the DOM is loaded. However, in jQuery Mobile, Ajax is used to load the contents of each page into the DOM as you navigate. Because of this $(document).ready()will trigger before your first page is loaded and every code intended for page manipulation will be executed after a page refresh. This can be a very subtle bug. On some systems it may appear that it works fine, but on others it may cause erratic, difficult to repeat weirdness to occur.
Classic jQuery syntax:
$(document).ready(function() { 

});
To solve this problem (and trust me this is a problem) jQuery Mobiledevelopers created page events. In a nutshell page events are events triggered in a particular point of page execution. One of those page events is a pageinit event and we can use it like this:
$(document).on('pageinit', function() {

});
We can go even further and use a page id instead of document selector. Lets say we have jQuery Mobile page with an id index:
<div data-role="page" id="index">
<div data-theme="a" data-role="header">
<h3>
First Page
</h3>
<a href="#second" class="ui-btn-right">Next</a>
</div>

<div data-role="content">
<a href="#" data-role="button" id="test-button">Test button</a>
</div>

<div data-theme="a" data-role="footer" data-position="fixed">

</div>
</div>
To execute a code that will only available to the index page we could use this syntax:
$('#index').on('pageinit', function() {

});
Pageinit event will be executed every time page is about be be loaded and shown for the first time. It will not trigger again unless page is manually refreshed or ajax page loading is turned off. In case you want code to execute every time you visit a page it is better to use pagebeforeshow event.
Here's a working example : http://jsfiddle.net/Gajotres/Q3Usv/ to demonstrate this problem.
Few more notes on this question. No matter if you are using 1 html multiple pages or multiple html files paradigm it is advised to separate all of your custom javascript page handling into a single separate js file. This will note make your code any better but you will have much better code overview, especially while creating a jQuery Mobile application.
There's also another special jQuery Mobile event and it is called mobileinit.When jQuery Mobile starts, it triggers a mobileinit event on the document object. To override default settings, bind them to mobileinit. One of a good examples of mobileinit usage is turning off ajax page loading, or changing default ajax loader behavior.
$(document).on("mobileinit", function(){
//apply overrides here
});

Page events transition order

First all events can be found here: http://api.jquerymobile.com/category/events/
Lets say we have a page A and a page B, this is a unload/load order:
  1. page B - event pagebeforecreate
  2. page B - event pagecreate
  3. page B - event pageinit
  4. page A - event pagebeforehide
  5. page B - event pagebeforeshow
  6. page A - event pageremove
  7. page A - event pagehide
  8. page B - event pagebeforeshow
  9. page B - event pageshow
For better page events understanding read this:
  • pagebeforeload, pageload and pageloadfailed are fired when an external page is loaded
  • pagebeforechange, pagechange and pagechangefailed are page change events. These events are fired when a user is navigating between pages in the applications.
  • pagebeforeshow, pagebeforehide, pageshow and pagehide are page transition events. These events are fired before, during and after a transition and are named.
  • pagebeforecreate, pagecreate and pageinit are for page initialization.
  • pageremove can be fired and then handled when a page is removed from the DOM
Page loading jsFiddle example: http://jsfiddle.net/Gajotres/QGnft/
If AJAX is not enabled, some events may not fire.

Prevent page transition

If for some reason page transition needs to be prevented on some condition it can be done with this code:
$(document).on('pagebeforechange', function(e, data){  
var to = data.toPage,
from = data.options.fromPage;

if (typeof to === 'string') {
var u = $.mobile.path.parseUrl(to);
to
= u.hash || '#' + u.pathname.substring(1);
if (from) from = '#' + from.attr('id');

if (from === '#index' && to === '#second') {
alert
('Can not transition from #index to #second!');
e
.preventDefault();
e
.stopPropagation();

// remove active status on a button, if transition was triggered with a button
$
.mobile.activePage.find('.ui-btn-active').removeClass('ui-btn-active ui-focus ui-btn');;
}
}
});
This example will work in any case because it will trigger at a begging of every page transition and what is most important it will prevent page change before page transition can occur.
Here's a working example:

Prevent multiple event binding/triggering

jQuery Mobile works in a different way then classic web applications. Depending on how you managed to bind your events each time you visit some page it will bind events over and over. This is not an error, it is simply how jQuery Mobile handles its pages. For example, take a look at this code snipet:
$(document).on('pagebeforeshow','#index' ,function(e,data){    
$
(document).on('click', '#test-button',function(e) {
alert
('Button click');
});
});
Working jsFiddle example: http://jsfiddle.net/Gajotres/CCfL4/
Each time you visit page #index click event will is going to be bound to button #test-button. Test it by moving from page 1 to page 2 and back several times. There are few ways to prevent this problem:

Solution 1

Best solution would be to use pageinit to bind events. If you take a look at an official documentation you will find out that pageinitwill trigger ONLY once, just like document ready, so there's no way events will be bound again. This is best solution because you don't have processing overhead like when removing events with off method.
Working jsFiddle example: http://jsfiddle.net/Gajotres/AAFH8/
This working solution is made on a basis of a previous problematic example.

Solution 2

Remove event before you bind it:
$(document).on('pagebeforeshow', '#index', function(){       
$
(document).off('click', '#test-button').on('click', '#test-button',function(e) {
alert
('Button click');
});
});
Working jsFiddle example: http://jsfiddle.net/Gajotres/K8YmG/

Solution 3

Use a jQuery Filter selector, like this:
$('#carousel div:Event(!click)').each(function(){
//If click is not bind to #carousel div do something
});
Because event filter is not a part of official jQuery framework it can be found here: http://www.codenothing.com/archives/2009/event-filter/
In a nutshell, if speed is your main concern then Solution 2 is much better then Solution 1.

Solution 4

A new one, probably an easiest of them all.
$(document).on('pagebeforeshow', '#index', function(){       
$
(document).on('click', '#test-button',function(e) {
if(e.handled !== true) // This will prevent event triggering more then once
{
alert
('Clicked');
e
.handled = true;
}
});
});
Working jsFiddle example: http://jsfiddle.net/Gajotres/Yerv9/
Tnx to the sholsinger for this solution: http://sholsinger.com/archive/2011/08/prevent-jquery-live-handlers-from-firing-multiple-times/
pageChange event quirks - triggering twice
Sometimes pagechange event can trigger twice and it does not have anything to do with the problem mentioned before.
The reason the pagebeforechange event occurs twice is due to the recursive call in changePage when toPage is not a jQuery enhanced DOM object. This recursion is dangerous, as the developer is allowed to change the toPage within the event. If the developer consistently sets toPage to a string, within the pagebeforechange event handler, regardless of whether or not it was an object an infinite recursive loop will result. The pageload event passes the new page as the page property of the data object (This should be added to the documentation, it's not listed currently). The pageload event could therefore be used to access the loaded page.
In few words this is happening because you are sending additional parameters through pageChange.
Example:
<a data-role="button" data-icon="arrow-r" data-iconpos="right" href="#care-plan-view?id=9e273f31-2672-47fd-9baa-6c35f093a800&amp;name=Sat"><h3>Sat</h3></a>
To fix this problem use any page event listed in Page events transition order.

Page Change Times

As mentioned, when you change from one JQuery Mobile page to another, typically either through clicking on a link to another JQuery Mobile page that already exists in the DOM, or by manually calling $.mobile.changePage, several events and subsequent actions occur. At a high level the following actions occur:
  • A page change process is begun
  • A new page is loaded
  • The content for that page is “enhanced” (styled)
  • A transition (slide/pop/etc) from the existing page to the new page occurs
This is a average page transition benchmark:
Page load and processing: 3msPage enhance: 45msTransition: 604ms
Total time: 670ms *These values are in milliseconds.
So as you can see a transition event is eating almost 90% of execution time.

Data/Parameters manipulation between page transitions

It is possible to send a parameter/s from one page to another during page transition. It can be done in few ways.
Reference: http://stackoverflow.com/a/13932240/1848600
Solution 1:
You can pass values with changePage:
$.mobile.changePage('page2.html', { dataUrl : "page2.html?paremeter=123", data : { 'paremeter' : '123' }, reloadPage : true, changeHash : true });
And read them like this:
$(document).on('pagebeforeshow', "#index", function (event, data) {
var parameters = $(this).data("url").split("?")[1];;
parameter
= parameters.replace("parameter=","");
alert
(parameter);
});

Example:

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="widdiv=device-widdiv, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>
</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
<script src="http://www.dragan-gaic.info/js/jquery-1.8.2.min.js">
</script>
<script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
<script>
$
(document).on('pagebeforeshow', "#index",function () {
$
(document).on('click', "#changePage",function () {
$
.mobile.changePage('second.html', { dataUrl : "second.html?paremeter=123", data : { 'paremeter' : '123' }, reloadPage : false, changeHash : true });
});
});

$
(document).on('pagebeforeshow', "#second",function () {
var parameters = $(this).data("url").split("?")[1];;
parameter
= parameters.replace("parameter=","");
alert
(parameter);
});
</script>
</head>
<body>
<!-- Home -->
<div data-role="page" id="index">
<div data-role="header">
<h3>
First Page
</h3>
</div>
<div data-role="content">
<a data-role="button" id="changePage">Test</a>
</div> <!--content-->
</div><!--page-->

</body>
</html>
second.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="widdiv=device-widdiv, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>
</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
<script src="http://www.dragan-gaic.info/js/jquery-1.8.2.min.js">
</script>
<script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
</head>
<body>
<!-- Home -->
<div data-role="page" id="second">
<div data-role="header">
<h3>
Second Page
</h3>
</div>
<div data-role="content">

</div> <!--content-->
</div><!--page-->

</body>
</html>
Solution 2:
Or you can create a persistent javascript object for a storage purpose. As long ajax is used for page loading (and page is not reloaded in any way) that object will stay active.
var storeObject = {
firstname
: '',
lastname
: ''
}
Example: http://jsfiddle.net/Gajotres/9KKbx/
Solution 3:
You can also access data from the previous page like this:
$('#index').live('pagebeforeshow', function (e, data) {
alert
(data.prevPage.attr('id'));
});
prevPage object holds a complete previous page.
Solution 4:
As a last solution we have a nifty HTML implementation of localStorage. It only works with HTML5 browsers (including Android and iOS browsers) but all stored data is persistent through page refresh.
if(typeof(Storage)!=="undefined") {
localStorage
.firstname="Dragan";
localStorage
.lastname="Gaic";
}
Example: http://jsfiddle.net/Gajotres/J9NTr/
Probably best solution but it will fail in some versions of iOS 5.X. It is a well know error.

Don’t Use .live() / .bind() /.delegate()

I forgot to mention (and tnx andleer for reminding me) use on/off for event binding/unbinding, live/die and bind/unbind are deprecated.
The .live() method of jQuery was seen as a godsend when it was introduced to the API in version 1.3. In a typical jQuery app there can be a lot of DOM manipulation and it can become very tedious to hook and unhook as elements come and go. The .live() method made it possible to hook an event for the life of the app based on its selector. Great right? Wrong, the .live() method is extremely slow. The .live() method actually hooks its events to the document object, which means that the event must bubble up from the element that generated the event until it reaches the document. This can be amazingly time consuming.
It is now deprecated. The folks on the jQuery team no longer recommend its use and neither do I. Even though it can be tedious to hook and unhook events, your code will be much faster without the .live() method than with it.
Instead of .live() you should use .on(). .on() is about 2-3x faster then .live(). Take a look at this event binding benchmark: http://jsperf.com/jquery-live-vs-delegate-vs-on/34, everything will be clear from there.

Benchmarking:

There's an excellent script made for jQuery Mobile page events benchmarking. It can be found here: https://github.com/jquery/jquery-mobile/blob/master/tools/page-change-time.js. But before you do anything with it I advise you to remove its alert notification system (each “change page” is going to show you this data by halting the app) and change it to console.log function.
Basically this script will log all your page events and if you read this article carefully (page events descriptions) you will know how much time jQm spent of page enhancements, page transitions ....

Final notes

Always, and I mean always read official jQuery Mobiledocumentation. It will usually provide you with needed information, and unlike some other documentation this one is rather good, with enough explanations and code examples.

Changes:

  • 30.01.2013 - Added a new method of multiple event triggering prevention
  • 31.01.2013 - Added a better clarification for chapter Data/Parameters manipulation between page transitions
  • 03.02.2013 - Added new content/examples to the chapter Data/Parameters manipulation between page transitions
  • 22.05.2013 - Added a solution for page transition/change prevention and added links to the official page events API documentation
  • 18.05.2013 - Added another solution against multiple event binding

0 comments:

Post a Comment

Twitter Delicious Facebook Digg Stumbleupon Favorites More