Watch, Follow, &
Connect with Us

Adrian Chaves

Where It Was, Part Three

This article is a follow up of Where It Was, Part Two.

In the last post, we got our application to keep a database of places, and made it possible to add new places to the list. Now we will make it possible to open places on a second page, and to delete them from there.

Second Page Design

Currently, our database is only storing two fields for each place: an identifier, just for logical purposes, and a name, which is the way users can identify the different places on the list. But we plan to add geolocation to the database later, and we have to be prepared to add even more data in the future: images, videos…

We cannot include any of those on the list, it would be too cluttered, so we will need to let the user tap a place on the list, and then open a page with all the information available for that place. We will begin by designing that second page.

With our project open on RadPHP, go to File > New > Other… > RadPHP Project > PHP Files, select iPhone Page and click OK. A second page should be added to your project, and it should be displayed on the Designer. I suggest you change its name. Filenames will not be visible for the user, but short and meaningful names can make your code more readable, and we will use filenames when moving between pages from code. I decided to name the main page main and this new page place-viewer.

In this part of the tutorial we are not yet adding additional data to places database, so we will only be able to display the name of the place on the second page. For that, we will use a Label component. Then, we will also need a couple of MButton components, one to get back to the list of places, and another to delete the current place.

Place the components as you wish on the Designer, and give them descriptive names (use their Name property). For example, I named them lPlaceName, bBack and bDelete. You should also customize a bit the Font property for the label, given its content will be the title of the page. As for the buttons, you should give a value to their Caption property (for example: Back and Delete), and also make sure you set their ButtonType property to btNormal, since their behaviour will be controlled through JavaScript.

Second page of our application on the Designer.

Second page of our application on the Designer.

From the Main Page to the Second Page

Time to make the items on the list of places react to tapping.

jQuery Mobile page navigation does not work the usual way, loading the new page on the browser replacing the previous one. Instead, it loads every page on the document object model (DOM) of the main page on demand. Some Android devices have trouble loading JavaScript files referenced from pages loaded through AJAX, so only the JavaScript files loaded from the initial page will be executed. Since ours is a simple application, we will concentrate the logic on the first page, hence supporting those devices. In the future, however, we should probably refactore the code, and drop support for those devices as they become outdated.

On the main page of our application, at the end of the PageStateJSDeviceReady() function where we already load the database into the list of places, we will add a listener for the tap event on any li element (list items), and we will use that event to open the second page:

jQuery('li').live('tap', function() {
  jQuery.mobile.changePage('place-viewer.html');
});

You might notice we are passing the second page filename with the extension modified, from .php to .html. This is because that is the way all pages are named after deploying the project with the Wizard for PhoneGap.

Pass Place Data Between Pages

Changing to the second page will not be enough, though. We need a way to transfer the information about the tapped place from the first page to the second, so we can properly populate the later with the available data for the place. We will do so by storing place identifier in a JavaScript global variable, and reading the database on the second page. The whole process will be the following:

  1. When adding li elements to the listof places, we will also attach the place identifier to them, in a way that it is not visible.
  2. Then, on the tap event, we will get the identifier from the li element and store it in a JavaScript global variable.
  3. We will then add a listener to an event that triggers right before page changes, and when that change is from the main page to the second page, we will use the event to: (1) get the place identifier from the global variable, (2) get place data from the database using the identifier, and (3) populate the second page with that data.

Attach Place Identifiers to Each List Item

In order to attach the place identifier to the list items, we will have to modify our previous code where we print those list items, both when loading the database and when inserting new items on it. Where we were just printing a place name surrounded by list item tags, we will also set the id property of the list item to the identifier of the place.

When loading the database, right at the beginning of the application, we were using the code below:

function success(database, results) {
  for (var i=0; i<results.rows.length; i++){
		  list.prepend('<li>' + results.rows.item(i).name + '</li>');
  }
		list.listview('refresh'); // Refresh the list (updates the style).
}

We will get the place identifier from the same JavaScript object from where we got the place name, it is just a matter of replacing name with id. So the resulting code, after the new modifications, should look like this:

function success(database, results) {
  for (var i=0; i<results.rows.length; i++){
    list.prepend('<li id="' + results.rows.item(i).id + '">' + results.rows.item(i).name + '</li>');
  }
  list.listview('refresh'); // Refresh the list (updates the style).
}

As for the function to print a list item right after inserting it on the database, this is the current code:

function success() {
  list.prepend('<li>' + place + '</li>'); // Add item at the beginning of the list.
		list.listview('refresh');               // Refresh the list (updates the style).
		edit.val('').focus();                   // Empty the MEdit and focus it.
}

This time, we have no obvious way to retrieve the identifier of the place we have just entered. When we were writting this function, we only needed the name of the place, which we knew because we had just inserted it into the database.

If we have a look at PhoneGap Storage documentation, we will see that there is no possible way we can retrieve the identifier of the item we inserted from a success callback method on the transaction() method. But it turns out that if we call our success() function from the executeSql() method instead, it will receive two parameters: the Database object and an SQLResultSet. The later has the following property:

  • insertId: the row ID of the row that the SQLResultSet object’s SQL statement inserted into the database.

This is exactly what we are looking for.

So lets look again at our current code from a wider persepctive, since we will be changing three of its functions:

// Try to insert given place into the database.
function insert() {
  Places.transaction(performQuery, error, success);
}

// Perform SQL Query to insert the place.
function performQuery(database) {
  database.executeSql('INSERT INTO places (name) VALUES (?)', [place]);
}

// Function to run if the new place was added to the database.
function success() {
  list.prepend('<li>' + place + '</li>'); // Add item at the beginning of the list.
		list.listview('refresh');               // Refresh the list (updates the style).
		edit.val('').focus();                   // Empty the MEdit and focus it.
}

Now, on the first function we are going to remove the reference to our success() function, since we are not going to call it for the overall transaction success. We can keep the error callback though:

// Try to insert given place into the database.
function insert() {
  Places.transaction(performQuery, error); // Removed the third parameter (success).
}

On the second function, we will add our success() function callback function to the executeSql method:

// Perform SQL Query to insert the place.
function performQuery(database) {
  database.executeSql('INSERT INTO places (name) VALUES (?)', [place], success);
}

Finally, we will modify our success() function so it prints the place identifier on the id property of the list item:

function success(database, results) {
  list.prepend('<li id="' + results.insertId + '">' + place + '</li>'); // Add item at the beginning of the list.
		list.listview('refresh');               // Refresh the list (updates the style).
		edit.val('').focus();                   // Empty the MEdit and focus it.
}

Store the Identifier of the Tapped Place on a Global Variable

Now that we ensured every item on the list will contain its place identifier, we will modify their tap event, so when user taps a place name on the list, the identifier of that place is stored on a variable that will survive the page change.

At the beginning of this part of the tutorial, we wrote the code to change the page when a place on the list was tapped:

jQuery('li').live('tap', function() {
  jQuery.mobile.changePage('place-viewer.html');
});

We are associating the tap event to the list items (li), hence we will be able to access tapped li element from inside the event function with jQuery(this). So we just need to get its id attribute and store it in a global variable (creating a new property on the window object) right before changing to the second page of our application:

jQuery('li').live('tap', function() {
  window.placeId = jQuery(this).attr('id');
  jQuery.mobile.changePage('place-viewer.html');
});

Populate the Second Page with Place Data

We have now made sure we can get tapped place identifier from anywhere right before changing page. Now we must populate the second page with the data of the place right before the page gets loaded. We will do this with an event, pagebeforeshow, which triggers before a page is displayed when calling changePage() method.

So lets start by adding a listener to this event right after we added the one for the tap event, at the end of the PageStateJSDeviceReady() function:

jQuery('div').live('pagebeforeshow', function(event, ui) {
  // We will write our code for this event here.
});

We attached the event to div elements because there will be always at least one when changing between pages.

Since only the JavaScript code from the main page will be parsed, this event will be triggered everytime we change page. That means our function will be called both when user opens the second page and when he or she gets back to the main page.

We are going to populate the second page only when entering it, and not when entering the main page, so we must check which page we are at when the event gets triggered. To do so, we will check for the existance of a component from the second page. If it exists, we have just changed to the second page, else we changed to the main one.

var lPlaceName = jQuery('#lPlaceName',jQuery.mobile.activePage);
if(lPlaceName.length) // Check if the control exists.
{
  // Here will go the code to be run before displaying the second page.
}

On the first line, we store the lPlaceName Label component on a variable for easy access to it.

You might notice we are passing jQuery() function a second parameter. As I said before, jQuery Mobile loads all the pages on the current DOM. That means that, once the second page gets loaded for the first time, every time we look for the lPlaceName component on the DOM, it will be there, even if we are back to the main page. jQuery.mobile.activePage contains only the DOM of the currently active page, and by passing it to jQuery() we ensure the lPlaceName component is only looked for on that DOM.

Now we can write the code we want to run on the second page right before it gets displayed. We will get the data from the places database for the place with the identifier we stored on the window object, and we will use it to populate the page.

We will start by retrieving the place identifier:

// Place data.
var placeId = window.placeId;

Now we will define a function that runs a transaction on the database:

// Function to load the place data from the database.
function loadData() {
  Places.transaction(performQuery, error);
}

As you see we are only using a error callback function, and no success function. As we did to retrieve the place names from the database on the main page, we must set a success callback function directly on the executeSql() method. So we will define performQuery() function like this:

// Perform SQL Query to get place name.
function performQuery(database) {
  database.executeSql('SELECT name FROM places WHERE id=?', [placeId], success);
}

If you wonder why we are in such a trouble to get the name of the selected place, why we did not store it directly on the window object instead, avoiding this SQL transaction now, remember that we are planning to add more information to places, namely geolocation, that will not be loaded on the main page. We are preparing for the future.

Our success() function will look something like this:

function success(database, results) {
  if(results.rows.length) {
    lPlaceName.text(results.rows.item(0).name);
  } else {
    var reason;
    reason.message = 'no place found with id ' + placeId + '.'
    error(reason);
  }
}

We make sure the query returned results, and in that case we use the name retrieved on the first row of results (there should only be a row) to populate the lPlaceName label display text. If no result is retrieved, we call the error() function with an error message included. Now, lets define our error() function:

// Function to run if there is any issue with the database query.
function error(reason) {

  // Construct message.
  var message = 'Could not load place: ' + reason.message + '.';

  // Print notification.
  navigator.notification.alert(
    message,                      // Message.
    navigator.app.backHistory(),  // Callback function.
    'Loading Place Failed',       // Title.
    'OK'                          // Button labels.
  );

}

It simply displays a dialog with the error message, and gets back to the main page of our application.

navigator.app.backHistory() is an undocumented PhoneGap method to go to a previous page of your application. You should also know of the navigator.app.exitApp() method, which lets you exit your application.

Finally, we proceed to call our initial function:

loadData();

Setup the Back Button

The label is now properly setup before the page is displayed. It is time to continue with the other two controls. First one is the Back button.

Setting the Back button up is quite easy. Use the following code at the end of the PageStateJSDeviceReady() function:

jQuery('#bBack').live('tap', function(event) {
  navigator.app.backHistory()
});

That’s it.

Setup the Delete Button

For the Delete button, we will need to remove the place from the database, go back to the main page and remove the item from the list there before the page is displayed.

Lets start by adding another tap event listener at the end of thePageStateJSDeviceReady() function:

jQuery('#bDelete').live('tap', function(event) {
  // Our code for the event will go here.
}

We will get the place identifier from the global variable as we did to populate the page:

// Get place identifier.
var placeId = window.placeId;

Next, we will define a function to run the transaction, and another to perform the actual query:

// Function to load the place data from the database.
function deletePlace() {
  Places.transaction(performQuery, error);
}

// Perform SQL Query to get place name.
function performQuery(database) {
  database.executeSql('DELETE FROM places WHERE id=?', [placeId], success);
}

Now, if the transaction goes as expected, we will store a global variable to indicate a place deletion took place, and we will get back to the main page, where we will later read that variable.

// Function to run if query goes as expected.
function success() {
  window.itemDeletion = true;
  navigator.app.backHistory()
}

Finally we will define the error() function (just notify the user) and call the first function:

// Function to run if there is any issue with the database query.
function error(reason) {

  // Construct message.
  var message = 'Could not delete place: ' + reason.message + '.';

  // Print notification.
  navigator.notification.alert(
    message,                // Message.
    function(){},           // Callback function.
    'Deleting Place Failed', // Title.
    'OK'                    // Button labels.
  );

}

deletePlace();

Update Main Page List after Place Deletion

When the user gets back to the main page, the place he or she just deleted should not be listed. So lets go to the beforepageshow event we previously defined, and lets append an else statement to the if statement where we check if active page is the second page.

if(lPlaceName.length) // Check if the control exists.
{
  // …
} else {   // Main Page
  // Here we will write code now.
}

So, before the main page is displayed, we will check if an item was deleted, and in that case, remove it from the list:

if (window.itemDeletion === true) {
  var placeId = window.placeId;                // Get the identifier of the place.
  jQuery('li[id="' + placeId + '"]').remove(); // Remove the place from the list.
  window.itemDeletion = false;                 // Unset the flag.
}

End of Part Three

We accomplished our goals for this part of the tutorial. We can now open places on a separate page, and delete them from there. We learned how to deal with communication between different pages on a mobile application, and we practiced more with PhoneGap Storage API.

On the upcoming chapter of this tutorial, we are going to include the only remaining feature of our application: geolocation. We will store geolocation data when saving a place to the database, and will load it on a map displayed on the second page.

Posted by achaves on February 9th, 2012 under Mobile, Tutorials |



One Response to “Where It Was, Part Three”

  1. Adrian Chaves » Where It Was, Part Four Says:

    [...] This article is a follow up of Where It Was, Part Three. [...]

Leave a Comment

Server Response from: BLOGS1