Building a jQuery Mobile Application, Part 3

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

The Entire Series

Want To Learn More?

My How to Build a jQuery Mobile Application EBook takes the Notes App to the next level by adding notes synchronization with a server. Check it out!

About Jorge

Jorge is the author of Building a Sencha Touch Application, How to Build a jQuery Mobile Application, and the Ext JS 3.0 Cookbook. He runs a software development and developer education shop that focuses on mobile, web and desktop technologies.
If you'd like to work with Jorge, contact him at ramonj[AT]miamicoder.com.

Comments

  1. This is a tremendously helpful tutorial and I appreciate all the effort you put into this. I do have one issue that I’ve not been able to resolve. One of the tests are not passing. The “Saves a note to local storage” fails. I believe the error is on the line “expect(expectedNote instanceof Notes.NoteModel).toBeTruthy();” While noteModel is an instance of Notes.NoteModel prior to saving it, when it returns in expectedNote it is not. Any ideas why this test fails for me?

    expectedNote instanceof Notes.NoteModel
    false
    noteModel instanceof Notes.NoteModel
    true

    The same issue is in part 4 (using your downloaded code) as well as “Removes a note from local storage”

    I’m using v 1.0.1 of jQuery Mobile and the latest Jasmine versions, but doubt this is the source of the issue. Any ideas would be appreciated.

    • Scott, you did not mention the jstorage library, but you’re using it, correct?

      • Yes, and the app appears to work correctly, only getting this test failure. I’m able to save, update and delete records, it’s just the test reporting failure

    • Hi Scott,
      I have the same problem i.e. this test code doesn’t work:
      expect(expectedNote instanceof Notes.NoteModel).toBeTruthy();

      Apparently when getting a NoteModel from the list, it’s not a NoteModel but Object:
      var expectedNote = notesList[0]; //expectedNote is Object

      I’ve modified slightly the test code to get the test passed:
      expect(expectedNote).toBeTruthy(); //test if not null
      expect(expectedNote.title === “test_title”).toBeTruthy(); //test if title correct

      @Jorge: many thanks to you for this very helpful tutorial. Really good work !!!

      Artur

  2. Scott, I had the same problem like you but it’s passing all tests now, thanks to Arthur B. nice tip! :)

    Jorge, thanks for your great tutorial, it’s a very useful learning resource! ;-)

  3. Thanks very much Artur, I appreciate understanding how to get the test to pass! I can now try to use this method.

    Thanks again Jorge for sharing your work, you can see how many of us have benefited from your efforts.

  4. Greet tutorial, thanks jorge. I’ll use it in my class :)

    IMHO, the following code can’t be changed, making it more clean and clear.

    1. The enter guard of testing the hash of the fromPage URL on the renderSelectedNote method seems redundant, since it’has been already tested before dispatching from onPageChanged handler.

    2. A findNoteById method should be provided in the DataContext, to instead of current notesList iteration code in the renderSelectedNote method.

  5. Thanks for great tutorial Jorge.

    I have a question though, is it really necessary to use both events “pagebeforechange” and “pagechange”. Couldn’t it be possible to handle every thing on “pagebeforechange”.

    The reason I’m asking that is in this implementation one can still see for a brief moment (depending on the speed of mobile device) the title and narrative of the note that was looked at in the past when one changes to editor page of another note. The title and narrative change as one is already looking at the editor page. I find this not to nice.
    Cheers, Damjan

  6. Great tutorial!
    I have upgraded jquery to 1.8.2 and jquery-mobile to 1.2.0(actually stable versions), and ran into the jqery error by trying to call “note edit”-page:
    “Error: Syntax error, unrecognized expression: #note-editor-page?noteId=…”
    It looks like this version jquery(-mobile) cannot process the “?…”-query part of “data.toPage” url string.

    The solution:
    cut of this part in “onPageBeforeChange” handler after parsing “data.toPage”:
    ——————————-
    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(“?”, “”);
    data.toPage = data.toPage.replace( /\?.*$/g, ” ); // cut of “?..”-tail
    }
    }
    };
    ——————————-
    May be it will help somebody to save debugging time ;)

  7. Hi Jorge
    Excellent set of tutorials inspired me to buy the eBook. Thanks for that jump-start.

    Synchronisation with the Server is now my big issue. The book’s source code gave me some excellent pointers but stopped short of guidance on how to test and implement a php-based synchHandler.

    Any further thoughts on going about that?

Speak Your Mind

*