Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Building a jQuery Mobile Application, Part 3

DZone's Guide to

Building a jQuery Mobile Application, Part 3

· Mobile Zone ·
Free Resource

In this third part of my tutorial on how to create a mobile web app using  jQuery Mobile we are going to focus on the following areas:

  • How to pass information between the Notes List page and the Note Editor page using URLs and query strings
  • How to load a note in the Note Editor page
  • How to save a new or edited note

Let’s begin with a few changes that will make the application easier to maintain and test.

Refactoring

Our data context module directly uses a reference to the jQuery library when calling the jStorage plugin. We will change our code so we pass the jQuery reference as a parameter when we invoke the dataContext module,  like so:

Notes.dataContext = (function ($) {

    // Module’s implementation omitted…

} (jQuery));

In addition, we will remove the hard-coded reference to the storage key we use to cache notes in local storage:

var notesListStorageKey = "Notes.NotesList"; // No need to hard-code this value.

We will now pass this value to the dataContext module through the init function:

var init = function (storageKey) {
    notesListStorageKey = storageKey;
    loadNotesFromLocalStorage();
};

Among other things, this change will allow us to test the dataContext module with a different key than the one we use in the application itself. Let’s modify our AppSpec.js file to support this change:

describe("Data Context tests", function () {

    var notesListStorageKey = "Notes.NotesListTest";

    // Other tests omitted…

    it("Returns dummy notes saved in local storage", function () {

        Notes.testHelper.createDummyNotes();
        // Load dummy notes from local storage.
        Notes.dataContext.init(notesListStorageKey);

        var notesList = Notes.dataContext.getNotesList();

        expect(notesList.length > 0).toBeTruthy();
    });

});

Grouping Notes by Date

The next change will make it easier for our users to find a particular note in the Notes List page. We are going to group the notes by date:

We can make this helpful modification in the renderNotes function of the controller module:

var renderNotesList = function () {

    var notesList = dataContext.getNotesList();
    var view = $(notesListSelector);

    view.empty();

    if (notesList.length === 0) {

        $(noNotesCachedMsg).appendTo(view);
    } else {

        var liArray = [],
            notesCount = notesList.length,
            note,
            dateGroup,
            noteDate,
            i;

        var ul = $("<ul id=\"notes-list\" data-role=\"listview\"></ul>").appendTo(view);

        for (i = 0; i < notesCount; i += 1) {

            note = notesList[i];

            noteDate = (new Date(note.dateCreated)).toDateString();

            if (dateGroup !== noteDate) {
                liArray.push("<li data-role=\"list-divider\">" + noteDate + "</li>");
                dateGroup = noteDate;
            }

            liArray.push("<li>"
                + "<a data-url=\"index.html#note-editor-page?noteId=" + note.id + "\" href=\"index.html#note-editor-page?noteId=" + note.id + "\">"
                + "<div  class=\"list-item-title\">" + note.title + "</div>"
                + "<div class=\"list-item-narrative\">" + note.narrative + "</div>"
                + "</a>"
                + "</li>");

        }

        var listItems = liArray.join("");
        $(listItems).appendTo(ul);

        ul.listview();
    }
};

In the loop that creates the list items representing the notes, we keep track of the date the notes were taken, inserting a new list item with the attribute data-role=”list-divider” whenever the date value changes. This gives us a nice list divider: In addition, notice how the list items contain a link whose query string has a reference to the id of the note. We will use this information when we load a selected note into the Note Editor page. Let’s start working on this page now.

Creating the Note Editor Page

The Note Editor page consists of a div element with the data-role=”page” attribute. We will add this div to the index.html file like so:

<div data-role="page" id="note-editor-page" data-title="Edit Note">
    <div data-role="header" data-position="fixed">
        <a href="#notes-list-page" data-icon="back" data-rel="back">Cancel</a>
        <h1>
            Edit Note</h1>
        <a id="save-note-button" href="" data-theme="b" data-icon="check">Save</a>
    </div>
    <div data-role="content">
        <form action="" method="post" id="note-editor-form">
        <label for="note-title-editor">
            Title:</label>
        <input type="text" name="note-title-editor" id="note-title-editor" value="" />
        <label for="note-narrative-editor">
            Narrative:</label>
        <textarea name="note-narrative-editor" id="note-narrative-editor"></textarea>
        </form>
    </div>
    <div data-role="footer" data-position="fixed" class="ui-bar">
        <a id="delete-note-button" data-icon="delete" data-transition="slideup" data-rel="dialog">Delete</a>
    </div>
</div>

This page has a header area with Save and Cancel buttons, a form with the elements that allow us to edit a note, and a footer that hosts the Delete button. As you can see, jQuery Mobile has taken care of nicely styling and laying out the form elements.

We will give the Save and Delete button special treatment, binding to their tap events to trigger the routines that save or delete the note loaded in the Note Editor page.

The role of the Cancel button is to take us back to the Notes List page. This is why we use a link to the #notes-list-page bookmark.

Loading a Note in the Editor

Let’s take a moment to think about what needs to happen to load a note into the editor. For existing notes, when the user taps the li element representing a note in the Notes List page, we will perform the following steps:

  • Look up the note in the notesList array, based on the note’s id contained in the li element’s link.
  • Set the values of the title and narrative form elements in the Note Editor page to those of the selected note’s title and narrative.
  • Make the Note Editor page active.

For new notes, when the user taps the New button in the Notes List page, we will perform these steps:

  • Create a new, blank note.
  • Set the values of the title and narrative form elements in the Note Editor page to those of the new note’s title and narrative.
  • Make the Note Editor page active.

These routines are similar. The only difference is that in the first case we have to look up and load an existing note, while in the second case we need to create and load a new note.

Let’s begin with the first set of steps. The first thing we need in our controller is a reference to the Note Editor. We will define a selector for it in the declarations section of the controller module:

var noteEditorPageId = "note-editor-page";

Next, we need to return to the onPageChange() function and handle the case when we are switching to the editor page:

var onPageChange = function (event, data) {

    var toPageId = data.toPage.attr("id");
    var fromPageId = null;

    if (data.options.fromPage) {
        fromPageId = data.options.fromPage.attr("id");
    }

    switch (toPageId) {

        case notesListPageId:
            resetCurrentNote();
            renderNotesList();
            break;

        case noteEditorPageId:

            if (fromPageId === notesListPageId) {
                renderSelectedNote(data);
            }
            break;
    }
};

The handler is a bit more complex now. We added the fromPageId variable, which will store the id of the page we’re navingating from. We will call this page Source page from now on. We will call the page we’re navigating to Target page.

The value of fromPageId will help us determine if we need to load a note into the editor:

if (fromPageId === notesListPageId) {
    renderSelectedNote(data);
}

If the source page is the notes list, we load the note by calling the renderSelectedNote() function, which we will implement in a minute. Note that we added the check for the fromPage parameter because the pagechange event is also triggered after the application launches, when there isn’t a source page yet. It doesn’t make sense to acquire the id of the source page when the source page itself doesn’t exist:

if (data.options.fromPage) {
    fromPageId = data.options.fromPage.attr("id");
}

Before adding the code for renderSelectedNote(), let’s jump to the top of the controller module and create identifiers for the title and narrative form elements:

var noteTitleEditorSel = "[name=note-title-editor]";
var noteNarrativeEditorSel = "[name=note-narrative-editor]";

And here’s the implementation of renderSelectedNote():

var renderSelectedNote = function (data) {

    var u = $.mobile.path.parseUrl(data.options.fromPage.context.URL);
    var re = "^#" + noteEditorPageId;

    if (u.hash.search(re) !== -1) {

        var queryStringObj = queryStringToObject(data.options.queryString);

        var titleEditor = $(noteTitleEditorSel);
        var narrativeEditor = $(noteNarrativeEditorSel);

        var noteId = queryStringObj["noteId"];

        if (typeof noteId !== "undefined") {

            // We were passed a note id => We're editing an existing note.
            var notesList = dataContext.getNotesList();
            var notesCount = notesList.length;
            var note;

            for (var i = 0; i < notesCount; i++) {

                note = notesList[i];

                if (noteId === note.id) {

                    titleEditor.val(note.title);
                    narrativeEditor.val(note.narrative);
                    currentNote = note;
                }
            }
        } else {
            // We're creating a note. Reset the fields.
            titleEditor.val("");
            narrativeEditor.val("");
        }

        titleEditor.focus();
    }
};

The renderSelectedNote() function takes advantage of the fact that we’re passing the selected note’s id in the query string of the list item’s link:

liArray.push("<li>"
    + "<a data-url=\"index.html#note-editor-page?noteId=" + note.id + "\" href=\"index.html#note-editor-page?noteId=" + note.id + "\">"
    + "<div  class=\"list-item-title\">" + note.title + "</div>"
    + "<div class=\"list-item-narrative\">" + note.narrative + "</div>"
    + "</a>"
    + "</li>");

Our first goal inside renderSelectedNote() is to inspect the hash of the source page’s URL, to make sure the source page is the Notes List page. We do this using a regular expression search:

var u = $.mobile.path.parseUrl(data.options.fromPage.context.URL);
    var re = "^#" + noteEditorPageId;

    if (u.hash.search(re) !== -1) {
… additional code omitted
    }

 To acquire the URL’s hash we use the $.mobile.path.parseUrl() function, which parses a URL into an object that facilitates accessing the URL’s components.

Once we’re certain the source page is the Notes List, we create an object containing the query string parameters passed from the source page. The queryStringToObject() helper function performs this task:

var queryStringToObject = function (queryString) {

    var queryStringObj = {};
    var e;
    var a = /\+/g;  // Replace + symbol with a space
    var r = /([^&;=]+)=?([^&;]*)/g;
    var d = function (s) { return decodeURIComponent(s.replace(a, " ")); };

    e = r.exec(queryString);
    while (e) {
        queryStringObj[d(e[1])] = d(e[2]);
        e = r.exec(queryString);

    }

    return queryStringObj;
};

The queryStringToObject() function takes the value of the data.options.queryString property as a parameter:

var queryStringObj = queryStringToObject(data.options.queryString);

The problem is that the data.options object does not have a native queryString property. However, we can create it if we find a place, or rather a time before the page transition occurs, where we can acquire the value of the query string.

It turns out that this is possible if we define a handler for jQuery Mobile’s pagebeforechange event. Let’s revisit the init() function, and add the following line:

d.bind("pagebeforechange", onPageBeforeChange);

Now we can define onPageBeforeChange() like so:

var onPageBeforeChange = function (event, data) {

    if (typeof data.toPage === "string") {

        var url = $.mobile.path.parseUrl(data.toPage);

        if ($.mobile.path.isEmbeddedPage(url)) {

            data.options.queryString = $.mobile.path.parseUrl(url.hash.replace(/^#/, "")).search.replace("?", "");
        }
    }
};

Pay attention to the following line:

data.options.queryString = $.mobile.path.parseUrl(url.hash.replace(/^#/, "")).search.replace("?", "");

Here we use $.mobile.path.parseUrl() to acquire the query string and add it to the data.options object. As the onPageBeforeChange handler is invoked before the onPageChange handler, we’ve found an approach to inject the query string defined in the source page into the events chain, and propagate it to the target pages, where it can be used.

Back in renderSelectedNote(), we can finally take care of loading the selected note, or resetting the title and narrative fields, like so:

if (typeof noteId !== "undefined") {

    // We were passed a note id => We're editing an existing note.
    var notesList = dataContext.getNotesList();
    var notesCount = notesList.length;
    var note;

    for (var i = 0; i < notesCount; i++) {

        note = notesList[i];

        if (noteId === note.id) {
            currentNote = note;
            titleEditor.val(currentNote.title);
            narrativeEditor.val(currentNote.narrative);
        }
    }
} else {
    // We're creating a note. Reset the fields.
    titleEditor.val("");
    narrativeEditor.val("");
}

Observe how we’re keeping a reference to the selected note in the currentNote variable. This will later allow us to save or delete the note without having to perform a lookup on the notesList array.

This is what it takes to load a note. Let’s make sure things are working as expected. Fire up your favorite browser and confirm that tapping a note in the Notes List page loads the note into the Note Editor page:

Similarly, tapping the New button should load a new note into the editor.

Saving a Note

As the Save Note workflow is initiated when a user taps the Save button, the controller module needs to define a handler for the button’s tap event. We will use this handler to invoke a saveNote() function that we will create in the dataContext module. Let’s work on the tap handler first.

We need an identifier for the Save button, which we can create at the top of the controller module like so:

var saveNoteButtonSel = "#save-note-button";

Next, we need to bind the tap handler in the controller’s init() function:

var init = function () {

    dataContext.init("Notes.NotesList");

    var d = $(document);
    d.bind("pagebeforechange", onPageBeforeChange);
    d.bind("pagechange", onPageChange);
    d.delegate(saveNoteButtonSel, "tap", onSaveNoteButtonTapped);
};

Now we can define the onSaveButtonTapped function like so:

var onSaveNoteButtonTapped = function () {

    // Validate note.
    var titleEditor = $(noteTitleEditorSel);
    var narrativeEditor = $(noteNarrativeEditorSel);
    var tempNote = dataContext.createBlankNote();

    tempNote.title = titleEditor.val();
    tempNote.narrative = narrativeEditor.val();

    if (tempNote.isValid()) {

        if (null !== currentNote) {

            currentNote.title = tempNote.title;
            currentNote.narrative = tempNote.narrative;
        } else {

            currentNote = tempNote;
        }

        dataContext.saveNote(currentNote);

        returnToNotesListPage();

    } else {
        // TODO: Inform the user the note is invalid.
    }
};

The first interesting thing that happens in this function is the creation of a temporary note, and the call to the NoteModel’s isValid method. This is the method that will allow us to validate a note before making it permanent.

But isValid does not exist yet. We need to create it like so:

Notes.NoteModel.prototype.isValid = function () {
    "use strict";
    if (this.title && this.title.length > 0) {
        return true;
    }
    return false;
};

A simple check on the note’s title is enough to validate the note. Nothing complicated.

Back in onSaveButtonTapped, we find out if we’re editing an existing note by observing the value of currentNote. If currentNote points to an existing note, we transfer the title and narrative of the temporary note to it. If currentNote is not poiting to an existing note, we make it point to the temporary note’s reference.

Then, we save the note by calling the dataContext module’s saveNote function. We haven’t created this function yet. Let’s define a behavior test for it in the AppSpec file:

it("Saves a note to local storage", function () {

    // Make sure LS is empty before the test.
    $.jStorage.deleteKey(notesListStorageKey);
    var notesList = $.jStorage.get(notesListStorageKey);
    expect(notesList).toBeNull();

     // Create a note.
    var dateCreated = new Date();
    var id = dateCreated.getTime().toString();
    var noteModel = new Notes.NoteModel({
        id: id,
        dateCreated: dateCreated,
        title: ""
    });

    Notes.dataContext.init(notesListStorageKey);
    Notes.dataContext.saveNote(noteModel);

    // Should retrieve the saved note.
    notesList = $.jStorage.get(notesListStorageKey);
    var expectedNote = notesList[0];

    expect(expectedNote instanceof Notes.NoteModel).toBeTruthy();

    // Clean up
    $.jStorage.deleteKey(notesListStorageKey);
});

In this spec we first empty the local storage container we will use. Then, we save a dummy note by calling saveNote on the dataContext module. Last, we retrieve the value from local storage and assert that it is in effect an instance of the NoteModel class.

As the saveNote function is missing, this test should fail:

In the dataContext module, let’s create saveNote as follows:

var saveNote = function (noteModel) {

    var found = false;
    var i;

    for (i = 0; i < notesList.length; i += 1) {
        if (notesList[i].id === noteModel.id) {
            notesList[i] = noteModel;
            found = true;
            i = notesList.length;
        }
    }

    if (!found) {
        notesList.splice(0, 0, noteModel);
    }

    saveNotesToLocalStorage();
};

This function is pretty straightforward. We start by iterating over the array of existing notes. If we find the id of the edited note, we “edit” the existing note through an in-place replacement with the passed note. If don’t find the id, we simply place the passed note at the beginning of the array.

Finally, we call the private function saveNotesToLocalStorage, which we also need to add to the dataContext module. This is where we save the modified array to local storage:

var saveNotesToLocalStorage = function () {
    $.jStorage.set(notesListStorageKey, notesList);
};

Let’s run the spec again. This time, it should pass:

Back in the onSaveButtonTapped function, we also wrote a call to the helper function returnToNotesListPage, which we will implement like so:

var returnToNotesListPage = function () {

    $.mobile.changePage("#" + notesListPageId,
        { transition: "slide", reverse: true });
};

This is a convenience function that we will call every time we need to return to the Notes List page.

There is one last step we need to take in order for the Edit Note and Save Note workflows to work correctly. We need to make sure we reset the currentNote reference after a note is saved. We can do this from the onPageChange function, within its switch statement:

var onPageChange = function (event, data) {
var toPageId = data.toPage.attr("id");
var fromPageId = null;
if (data.options.fromPage) {
fromPageId = data.options.fromPage.attr("id");
}
switch (toPageId) {
case notesListPageId:
resetCurrentNote(); // Reset reference to the note being edited.
renderNotesList();
break;
case noteEditorPageId:

if (fromPageId === notesListPageId) {
renderSelectedNote(data);
}
break;
}
};

When we’re navigating back to the Notes List page, we’re now invoking the private function resetCurrentNote, which looks like this:

var resetCurrentNote = function () {
    currentNote = null;
}

What do you think? Are you ready to test on the emulator? First go back to the mobileinit handler and either remove or comment out the call to createDummyNotes. We don’t need it anymore:

$(document).bind("mobileinit", function () {

    //Notes.testHelper.createDummyNotes();

    Notes.controller.init();

});

Now, fire up the emulator. You should be able to create and edit notes.

Nice! We just built the ability to create and edit notes into our application.

I’m sure you noticed that we still have a loose end in the onSaveNoteButtonTapped function, as we have not handled the invalid note scenario. We will leave this step for the next chapter, along with deleting notes.

Stay tuned!

Downloads

Download the source code for this article: Building a jQuery Mobile App Part 3 Src.zip

Previous Articles

 

From http://jorgeramon.me/2011/building-a-jquery-mobile-application-part-3

Topics:
mobile

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}