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

Building a jQuery Mobile Application, Part 4

DZone's Guide to

Building a jQuery Mobile Application, Part 4

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

In this fourth part of my series on how to build a jQuery Mobile application, we are going to complete the following tasks:

  • Implement the behavior of the app when a user tries to save an invalid note.
  • Implement the ability to delete notes.
  • Create and apply a custom theme swatch, to change the look of one of the application’s dialogs.


Getting Ready to Validate a Data Model

In the previous chapter of the series, we decided that we are going to consider that a note is valid when it has a title, and we are not going to force our users to enter the note’s narrative before they can save the note. We defined this behavior through the isValid() function of the NoteModel module:

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

We also left an empty branch in the onSaveNoteButtonTapped() function of the Controller module, where we need to add the code that will inform our user that her note is invalid. Here is the function as we originally created:

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.
    }
};

Before we complete this function, we need to define what UI elements we will use to inform the user that her note is invalid. We can accomplish this with a jQuery Mobile dialog. In our case, we want to create a very simple dialog, made of a header and a short message to explain our users what is happening:

How to Create a Dialog With jQuery Mobile

One of the ways you instruct the framework to display a page as a dialog is using the data-role=”dialog” attribute. We will follow this approach, defining an Invalid Note dialog in the index.html file as follows:

<!--Invalid Note dialog-->
<div id="invalid-note-dialog" data-role="dialog" data-title="Invalid Note" data-theme="e">
<div data-role="header" data-theme="e">
<h1>Wait!</h1>
</div>
<div data-role="content">Enter a title for this note.</div>
</div>

We will insert this markup right after the Note Editor page in the index.html file. A detail I don’t want you to miss is how we use the data-theme=”e” attribute to change the appearance of the dialog. Applying the E swatch to the dialog helps give it more of a warning look.
With the dialog in place, we need to go in the controller module and add an identifier for it:

var invalidNoteDlgSel = "#invalid-note-dialog";

This identifier will allow us to activate the dialog in the onSaveNoteButtonTapped() method of the controller 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 {
        $.mobile.changePage(invalidNoteDlgSel, defaultDlgTrsn);
    }
};

Note that we are making the dialog the active page using the $.mobile.changePage() method, which you can use to trigger page changes programmatically. This function takes a reference to the page in question, as well as the transition you want to use when bringing the page into view.

Instead of passing an inline-defined transition, we are going to define a default transition, which we will use for all the dialogs in the application. We will use the following on-liner to create defaultDlgTrsn right at the beginning of the controller module:

var defaultDlgTrsn = { transition: "slideup" };

All right, time to check how the dialog looks. Start your favorite WebKit browser or emulator, and try to save a note with a blank title. You should see the Invalid Note dialog become active:

The Right Place For The mobileinit Event Handler

Our refactoring step for this chapter will consist of moving the mobileinit event handler out of the index.html file. We don’t want to contaminate our html files with JavaScript, so we’re going to move the handler to the Controller.js file. We will place it right after the controller module’s definition:

Notes.controller = (function ($, dataContext, document) {

    // Controller’s implementation omitted for brevity.

} (jQuery, Notes.dataContext, document));

$(document).bind("mobileinit", function () {
    Notes.controller.init();
});

The index.html file is now virtually free of JavaScript code, which is desirable in order to keep the maintenance costs of the application low.

Creating a Confirmation Dialog

The second feature we need to address is deleting a note. Users of the application will initiate this workflow by tapping the Delete button on the Edit Note page:

When the user taps the button, we are going to render a small dialog, asking her for confirmation:

If the user taps the No button on the confirmation dialog, we will just return to the Note Editor page. However, if she taps the Yes button, we will proceed to delete the note, and then return to the Notes List page.

Let’s first create the dialog, and then connect it to the Delete button on the Note Editor page.

We will create the Confirm Delete Note dialog in the index.html file, using the following markup:

<!-- Confirm Delete Note dialog--></pre>
<div id="confirm-delete-note-dialog" data-role="dialog" data-title="Delete Note">
<div data-role="header">
<h1>Delete Note?</h1>
</div>
<div data-role="content">
<div id="delete-note-content-placeholder"></div>
 <a id="cancel-delete-note-button" data-role="button" data-theme="b" data-rel="back">No</a>
 <a id="ok-to-delete-note-button" data-role="button" data-theme="f">Yes</a></div>
</div>

Notice how we use the data-role=”dialog” attribute to have the framework render this page as a dialog. We will use the delete-note-content-placeholder div to render the selected note. The interesting thing about the buttons, which are links decorated with the data-role=”button” attribute, is how we’re using theme swatches to define their colors.

We’re using the B swatch, a built-in swatch of the default theme, for the No button. The Yes button uses the F swatch to give the button the red color. We will create the F swatch in a few minutes.

With the dialog created, we need to focus on how we will render it from within the controller module. Evidently, we need to create an identifier for the dialog:

var confirmDeleteNoteDlgSel = "#confirm-delete-note-dialog";

We also need identifiers for the Delete button in the Note Editor page, the Yes button in the Confirm Delete Note dialog, and the div element that will serve as placeholder in the dialog. Let’s add them to the controller module like so:

var deleteNoteButtonSel = "#delete-note-button",
deleteNoteContentPlaceholderSel = "#delete-note-content-placeholder",
okToDeleteNoteButtonSel = "#ok-to-delete-note-button";

As the dialog will be activated upon the user tapping the Delete button in the Note Editor page, we will define a tap handler for this button in the init function of the controller. Something like this will do:

var init = function () {

    // Rest of the function omitted for brevity.

    d.delegate(deleteNoteButtonSel, "tap", onDeleteNoteButtonTapped);
};

Here we are saying that the tap event on the button will invoke the onDeleteNoteButtonTapped() function, which we will now add to the controller module:

var onDeleteNoteButtonTapped = function () {

    if (currentNote) {
        // Render selected note in confirmation dlg.
        // Deletion will be handled elsewhere, after user confirms it's ok to delete.

        var noteContentPlaceholder = $(deleteNoteContentPlaceholderSel);

        noteContentPlaceholder.empty();
        $("</pre>
<h3>" + currentNote.title + "</h3>
<pre>
" + currentNote.narrative + "

").appendTo(noteContentPlaceholder);

        $.mobile.changePage(confirmDeleteNoteDlgSel, defaultDlgTrsn);
    }
};

In onDeleteNotebuttonTapped(), we first render the current note’s title and narrative in the content area of the dialog. We can accomplish this by simply appending html nodes to the placeholder we defined within the dialog. Then, we make the dialog the active page, using the $mobile.changePage() function. Note how once again we use the defaultDlgTrsn, previously defined when we were creating the Invalid Note dialog.

Now we are at a point where we are waiting for the user’s answer to our question – delete the note, yes or no? The answer will tell us whether to delete the note or cancel the workflow.

Deleting a Note

Cancellation will occur upon the user tapping the No button. We don’t have to write code for this scenario, as we are using the data-rel=”back” attribute for the No button:

<a id="cancel-delete-note-button" data-role="button" data-theme="b" data-rel="back">No</a>

This role will cause the button’s tap event to initiate a transition to the previous page, the Note Editor page, which is exactly what we want.

The Yes button is a little different. When the user taps this button, we need to delete the note and initiate a transition to the Notes List page. The Notes List page should then render the updated notes list.

We will define the tap handler for the Yes button in the controller module’s init function, binding to the button’s tap event:

var init = function () {

    // Rest of the function omitted for brevity.

    d.delegate(okToDeleteNoteButtonSel, "tap", onOKToDeleteNoteButtonTapped);
};

Then, we will define onOKToDeleteNoteButtonTapped() like so:

var onOKToDeleteNoteButtonTapped = function () {

    dataContext.deleteNote(currentNote);
    returnToNotesListPage();
};

Easy, right? We first call the data context module’s deleteNote(), and then transition to the Notes List page by calling returnToNotesListPage(), a function that we created when we were working on the steps required to save a note.

The problem is that we have not defined a deleteNote() function in the data context module. Before doing so, let’s define a test for this function in the AppSpec.js file:

it("Removes a note from local storage", function () {

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

        // Start with an empty notes list.
        var notesList = [];
        // Add note to local storage.
        notesList.push(noteModel);
        $.jStorage.set(notesListStorageKey, notesList);
        notesList = $.jStorage.get(notesListStorageKey);
        expect(notesList.length).toEqual(1);

        // Proceed to delete.
        Notes.dataContext.init(notesListStorageKey);
        Notes.dataContext.deleteNote(noteModel);

        // Should retrieve empty array
        notesList = $.jStorage.get(notesListStorageKey);
        expect(notesList.length).toEqual(0);

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

    });

In the test, we first create a note and save it directly into local storage. Then, we use the function being tested, deleteNote(), to remove the note. Of course, the expectation is that we can delete a note using this function.

The test should fail, as deleteNote() still does not exist:

With the test in place, let’s head over to the DataContext.js file, and define deleteNote() like so:

var deleteNote = function (noteModel) {

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

    saveNotesToLocalStorage();
};

Again, a very simple function that loops through the array of existing notes, trying to find one with the id of the note we want to delete. If found, the note is removed from the array. The updated array is then serialized to local storage through a call to saveNotesToLocalStorage().

We also need to add deleteNote() to the public interface of the module so we can invoke it from the controller module:

var pub = {
    init: init,
    createBlankNote: createBlankNote,
    getNotesList: getNotesList,
    saveNote: saveNote,
    deleteNote: deleteNote
};

Time to re-run the test, which should pass if we didn’t make any mistakes:

This completes the code that we needed for the Delete Note feature. The UI elements are in place, and the Controller and Data Context modules are ready to handle this workflow. Now you can fire up your favorite device emulator or WebKit browser, and verify that you can delete notes.

Using a Custom Theme Swatch in jQuery Mobile

Before we end this chapter, let us take care of a cosmetic issue. How about changing the color of the Yes button in the dialog?

One approach to accomplish this consists of using a custom jQuery Mobile theme swatch. We will take advantage of the ThemeRoller for jQuery Mobile to change the swatch of the Yes button.

As you already saw, we assigned the data-theme=”f” attribute to the button when we created the dialog:

<a id="ok-to-delete-note-button" data-role="button" data-theme="f">Yes</a>

With this in mind, we will head over to the ThemeRoller site and define an F swatch. The swatch has several properties, but we’re only interested in those that apply to buttons. Let’s use the following properties for the different buttons states:

After entering these values, we can use the Download Theme link to download the theme file.

The file contains styles for all elements enhanced by the jQuery Mobile framework. We will copy the styles that apply to button elements, or elements decorated with the data-role=”button” attribute, which we will add to our app.css file like so:

.ui-btn-up-f {
border: 1px solid #c1272d /*{f-bup-border}*/;
background: #c1272d /*{f-bup-background-color}*/;
font-weight: bold;
color: #ffffff /*{f-bup-color}*/;
text-shadow:  0  /*{f-bup-shadow-x}*/  1px  /*{f-bup-shadow-y}*/  1px  /*{f-bup-shadow-radius}*/ #444444 /*{f-bup-shadow-color}*/;
background-image: -webkit-gradient(linear, left top, left bottom, from( #D42A31 /*{f-bup-background-start}*/), to( #AD2328 /*{f-bup-background-end}*/)); /* Saf4+, Chrome */
background-image: -webkit-linear-gradient(top, #D42A31 /*{f-bup-background-start}*/, #AD2328 /*{f-bup-background-end}*/); /* Chrome 10+, Saf5.1+ */
background-image:    -moz-linear-gradient(top, #D42A31 /*{f-bup-background-start}*/, #AD2328 /*{f-bup-background-end}*/); /* FF3.6 */
background-image:     -ms-linear-gradient(top, #D42A31 /*{f-bup-background-start}*/, #AD2328 /*{f-bup-background-end}*/); /* IE10 */
background-image:      -o-linear-gradient(top, #D42A31 /*{f-bup-background-start}*/, #AD2328 /*{f-bup-background-end}*/); /* Opera 11.10+ */
background-image:         linear-gradient(top, #D42A31 /*{f-bup-background-start}*/, #AD2328 /*{f-bup-background-end}*/);
}
.ui-btn-up-f a.ui-link-inherit {
color: #ffffff /*{f-bup-color}*/;
}

.ui-btn-hover-f {
border: 1px solid #DD2C33 /*{f-bhover-border}*/;
background: #DD2C33 /*{f-bhover-background-color}*/;
font-weight: bold;
color: #ffffff /*{f-bhover-color}*/;
text-shadow:  0  /*{f-bhover-shadow-x}*/  1px  /*{f-bhover-shadow-y}*/  1px  /*{f-bhover-shadow-radius}*/ #444444 /*{f-bhover-shadow-color}*/;
background-image: -webkit-gradient(linear, left top, left bottom, from( #F33038 /*{f-bhover-background-start}*/), to( #C6272D /*{f-bhover-background-end}*/)); /* Saf4+, Chrome */
background-image: -webkit-linear-gradient(top, #F33038 /*{f-bhover-background-start}*/, #C6272D /*{f-bhover-background-end}*/); /* Chrome 10+, Saf5.1+ */
background-image:    -moz-linear-gradient(top, #F33038 /*{f-bhover-background-start}*/, #C6272D /*{f-bhover-background-end}*/); /* FF3.6 */
background-image:     -ms-linear-gradient(top, #F33038 /*{f-bhover-background-start}*/, #C6272D /*{f-bhover-background-end}*/); /* IE10 */
background-image:      -o-linear-gradient(top, #F33038 /*{f-bhover-background-start}*/, #C6272D /*{f-bhover-background-end}*/); /* Opera 11.10+ */
background-image:         linear-gradient(top, #F33038 /*{f-bhover-background-start}*/, #C6272D /*{f-bhover-background-end}*/);
}
.ui-btn-hover-f a.ui-link-inherit {
color: #ffffff /*{f-bhover-color}*/;
}
.ui-btn-down-f {
border: 1px solid #DD2C33 /*{f-bdown-border}*/;
background: #DD2C33 /*{f-bdown-background-color}*/;
font-weight: bold;
color: #ffffff /*{f-bdown-color}*/;
text-shadow:  0  /*{f-bdown-shadow-x}*/  1px  /*{f-bdown-shadow-y}*/  1px  /*{f-bdown-shadow-radius}*/ #444444 /*{f-bdown-shadow-color}*/;
background-image: -webkit-gradient(linear, left top, left bottom, from( #C6272D /*{f-bdown-background-start}*/), to( #F33038 /*{f-bdown-background-end}*/)); /* Saf4+, Chrome */
background-image: -webkit-linear-gradient(top, #C6272D /*{f-bdown-background-start}*/, #F33038 /*{f-bdown-background-end}*/); /* Chrome 10+, Saf5.1+ */
background-image:    -moz-linear-gradient(top, #C6272D /*{f-bdown-background-start}*/, #F33038 /*{f-bdown-background-end}*/); /* FF3.6 */
background-image:     -ms-linear-gradient(top, #C6272D /*{f-bdown-background-start}*/, #F33038 /*{f-bdown-background-end}*/); /* IE10 */
background-image:      -o-linear-gradient(top, #C6272D /*{f-bdown-background-start}*/, #F33038 /*{f-bdown-background-end}*/); /* Opera 11.10+ */
background-image:         linear-gradient(top, #C6272D /*{f-bdown-background-start}*/, #F33038 /*{f-bdown-background-end}*/);
}
.ui-btn-down-f a.ui-link-inherit {
color: #ffffff /*{f-bdown-color}*/;
}

After adding these classes to the app.css file, we can check the dialog’s look. The Yes button should now render with the new F swatch’s properties:

Summary

At this point, we have built a fully functional jQuery Mobile application that people can use to create, edit and delete notes.

In this series we started with a short introduction to the jQuery Mobile framework, learned how to create a modular application that saves information to the user’s device, created an attractive user interface, and designed behavior tests for the business logic.

I hope you use the experience gained in this and previous chapters to create great applications. Don’t forget to let me know how it goes! :-)

Downloads

Download the source code for this article: Building a jQuery Mobile App Part 4 Sr.c.zip

 

From http://jorgeramon.me/2012/building-a-jquery-mobile-application-part-4/

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
mobile ,jquery

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}