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

Building a jQuery Mobile Application, Part 2

DZone's Guide to

Building a jQuery Mobile Application, Part 2

· Mobile Zone
Free Resource

Discover how to focus on operators for Reactive Programming and how they are essential to react to data in your application.  Brought to you in partnership with Wakanda

Ready for more jQuery Mobile and Jasmine? This is the second part of my tutorial on how to create a mobile web app using jQuery Mobile. In this tutorial we’re building a mobile web application that offers its users the following features:

  • Ability to create, edit and delete notes.
  • Ability to store notes on the device running the application, across browser sessions.
  • Ability to view the entire collection of notes stored on the device.

In the first chapter of this series we started building the business logic layer of the app. We are using a simple workflow that consists of the following steps:

  • Use the Jasmine framework to define the business logic behavior.
  • Implement the behavior in the application.
  • Confirm that the implemented behavior adheres to its definition.

We’re now going to continue working on the business logic features that will allow us to retrieve the list of notes cached on the device. We’re also going to use the jQuery Mobile framework to present the cached notes to the user.

By the end of this article, we will have completed the first version of the Notes List screen. This screen will render the list of cached notes as depicted below.

Consolidating Test Suites and Creating a Data Context Module

Let’s get started with a bit of refactoring. As you might recall from the first part of this tutorial, we placed the tests into two Jasmine suites. The first suite looks like this:

describe("Public interface exists", function () {

    it("Should have public interface to return notes list", function () {
        expect(Notes.app.getNotesList).toBeDefined();
    });

});

And the second suite looks like this:

describe("Public interface implementation", function () {

    it("Should return notes list", function () {

        var notesList = Notes.app.getNotesList();

        expect(notesList instanceof Array).toBeTruthy();
    });
});

As I was writing the outline for this article, I realized that using two test suites this early in the project, when we have only a few specs, might be overkill. We can simplify things and use only one test suite for now. We can always add more suites if there’s a real need later.

Let’s replace the suites above with the following:

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

    it(“Exists in the app”, function () {
        expect(Notes.dataContext).toBeDefined();
    });

    it(“Returns notes Array”, function () {

        var notesList = Notes.dataContext.getNotesList();

        expect(notesList instanceof Array).toBeTruthy();
    });

});

As the first spec indicates, we’re going to rename the app instance variable to dataContext. After all, what this instance will contain is all the functions that have to do with data access – in our case, access to the notes stored on the device. In the first part of the series, the app variable looked like this:

Notes.app = (function () {

    var notesList = [];

    function getNotesList() {
        return notesList;
    }

    return {

        getNotesList: getNotesList

    }

})();

After the renaming, we end up with dataContext, like so:

Notes.dataContext = (function () {

    var notesList = [];

    function getNotesList() {
        return notesList;
    }

    return {

        getNotesList: getNotesList

    }

})();

Along with this change, we’re going to rename the App.js file to DataContext.js. After renaming, our files should now look as depicted below.

Of course, we need to run our tests to make sure the refactoring did not change the expected behavior. In the specrunner.html, we need to replace the reference to App.js with a reference to DataContext.js:

<!-- App -->
<script type="text/javascript" src="app/DataContext.js"></script>

Now we can run our Jasmine suite. Let’s open the specrunner.html file in our favorite browser and check the output of the test. If we didn’t make any mistakes during our refactoring, our tests should be green: Our refactoring is done. What we did, in short, was rename the application module we created in the first part of the tutorial – it’s now called DataContext – and adjust our tests accordingly.

Creating a Blank Note

Besides retrieving the cached notes, an important function we need our data context to perform is creating a blank note. This feature is needed when users tap the New button in the UI. Tapping the New button will pass a new, blank note, to the Note Editor view:

We can describe this “create blank note” behavior in our test suite as follows:

it("Returns a blank note", function () {

    var blankNote = Notes.dataContext.createBlankNote();
    expect(blankNote.title.length === 0).toBeTruthy();
    expect(blankNote.narrative.length === 0).toBeTruthy();
});

If we run this test, it will not pass. Why? Well, we need to add the createBlankNote() function to the Data Context module. This is the code we need:

Notes.dataContext = (function () {

    var notesList = [];

    function createBlankNote() {

        var dateCreated = new Date();
        var id = new String(dateCreated.getTime()) + new String(getRandomInt(0, 100));
        var noteModel = new Notes.NoteModel({
            id: id,
            dateCreated: dateCreated,
            title: "",
            narrative: ""
        });

        return noteModel;
    }

    function getNotesList() {
        return notesList;
    }

    return {
        createBlankNote: createBlankNote,
        getNotesList: getNotesList
    };

})();

If you look at createBlankNote(), you will notice that it calls a getRandomInt() helper function to create the id of the new note. As we haven’t defined getRandomInt() yet, if we run the Jasmine spec, the results should show that the helper function is missing:

it-fails-3

Let’s add getRandomInt() like so:

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

Re-running the test again should fail with a different error. The reason for the new error is that we’re trying to create an instance of the NoteModel class, which we haven’t defined yet.

Let’s create the NoteModel.js file in the application’s folder, and add the following definition for the NoteModel class:

Notes.NoteModel = function (id, dateCreated, title, narrative) {
    this.id = id;
    this.dateCreated = dateCreated;
    this.title = title;
    this.narrative = narrative;
}

This construct should look familiar to you. The NoteModel is a class that represents a note. Every time we need to move a note’s data around, we will use an instance of this class. And what we will cache on the device is a serialized array of NoteModel instances.

We also need to include a reference to the NoteModel.js file in specrunner.html:

<!-- App --><script type="text/javascript" src="app/DataContext.js"></script>
<script type="text/javascript" src="app/NoteModel.js"></script>

After adding the NoteModel, the tests should pass: Are you with me so far? This is getting interesting. Next we will begin working on retrieving cached notes from HTML5’s local storage.

Retrieving Cached Notes from Local Storage

We will use the localStorage API to store notes across browser sessions. Local storage is based on keys and values, where the keys and values are strings. As we will cache an array of NoteModel instances, we will need a serialization mechanism to convert the array of notes to a string that will be saved in local storage. Similarly, we will need a mechanism to convert the serialized notes into an array of NoteModel instances when retrieving the cached notes.

We could write the serialization/deserialization code we need. However, to simplify this tutorial, we will use an abstraction layer on top of the localStorage API. This layer will take care of the serialization and deserialization services, and it will be provided by the jStorage plugin.

The jStorage plugin allows us to cache numbers, strings, and objects in local storage. We are going to place the jStorage plugin in its own folder like so:

In the data context module, we will define a private function which retrieves the cached notes from local storage like so:

function loadNotesFromLocalStorage() {

    var storedNotes = $.jStorage.get(notesListStorageKey);

    if (storedNotes !== null) {
        notesList = storedNotes;
    }

}

And let’s also define the notesListStorageKey variable, right after the notesList definition:

var notesList = [];
var notesListStorageKey = "Notes.NotesList";

The loadNotesFromLocalStorage() function simply uses the jStorage plugin to retrieve the list of cached notes from local storage. The plugin takes care of converting the list of notes from a String instance to an Array instance, exactly what we need.

When the application is run, the cached notes will be presented to the user. This means that the loadNotesFromLocalStorage() is one of the first functions that will be called in the app. To call this function when the application starts, as well as execute any other initialization code needed by the app, we will use a helper method, which we will name init().

Let’s work on the init() function by first creating a spec for it in the AppSpec.js file:

it("Has init function", function () {
    expect(Notes.dataContext.init).toBeDefined();
});

Then, we need to add the init() public function to the DataContext.js file:

function init() {
    loadNotesFromLocalStorage();
}
.
. // Other functions…
.
return {
    init: init,
    createBlankNote: createBlankNote,
    getNotesList: getNotesList
};

The test for init() should be green at this point:

Testing With Data by Retrieving a List of Dummy Notes

At this point we’ve added and tested the features we needed for this part of the tutorial. However, it would be nice if we could also test by saving a few dummy notes to local storage, and having the Data Context module retrieve them for us.

This is not difficult to do. Let’s first create a helper module, which we will place in a new TestHelper.js file, in the spec folder. This module will allow us to save a few dummy notes into local storage:

Notes.testHelper = (function () {

    function createDummyNotes() {

        var notesListStorageKey = "Notes.NotesList";
        var notesCount = 10;
        var notes = [];

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

            var note = Notes.dataContext.createBlankNote();
            note.title = "Title " + i;
            note.narrative = "Narrative " + i;
            notes.push(note);
        }

        $.jStorage.set(notesListStorageKey, notes);
    };

    return {
        createDummyNotes: createDummyNotes
    }

})();

Notice that testHelper uses the same notesListStorageKey value used in the data context module, along with the data context’s createBlankNote() function, to create an array of dummy notes. These notes are saved to local storage through the jStorage plugin.

We also need a reference to TestHelper.js in our specrunner.html file:

<!-- App --><script type="text/javascript" src="app/DataContext.js"></script>
<script type="text/javascript" src="app/NoteModel.js"></script>
<!-- Test Helper --><script type="text/javascript" src="spec/TestHelper.js"></script>
<!-- Spec -->
<script type="text/javascript" src="spec/AppSpec.js"></script>

Back in the AppSpec.js file, we can define a spec confirming that we can load the dummy notes:

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

    Notes.testHelper.createDummyNotes();
    // Load dummy notes from ls.
    Notes.dataContext.init();

    var notesList = Notes.dataContext.getNotesList();

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

This spec first uses the testHelper’s createDummyNotes() function to place a few notes in local storage. Then, it invokes the data context’s init() function, which in turn calls the loadNotesFromLocalStorage() private function.

After the call to init(), the spec retrieves the notes list via a call to the data context’s getNotesList() function. The expectation is simply that the notes list is not empty.

Notice that we could be more thorough and compare the retrieved notes to the ones we saved, making sure that the data did not change. I will leave this exercise as homework for you.

What do you say we dive into the presentation layer?

Rendering Cached Notes

Now we’re ready to render the cached notes. If you’ve never read the jQuery Mobile overview and quick start guide, this is a good time to do it. Don’t worry, I’ll wait for you to come back. :-)

The first thing we need to do is use jQuery Mobile’s page template to create our app’s html page. Let’s create the index.html file in our application’s folder, as depicted below:

Then, add the following markup to the index.html file:

<!--<html>
<head>-->
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="../../lib/jqm/jquery.mobile-1.0.min.css" rel="stylesheet" type="text/css" />
    <link href="css/app.css" rel="stylesheet" type="text/css" />
    <script src="../../lib/jqm/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="../../lib/jstorage/jstorage.min.js" type="text/javascript"></script>
    <script src="app/DataContext.js" type="text/javascript"></script>
    <script src="app/Controller.js" type="text/javascript"></script>
    <script src="app/NoteModel.js" type="text/javascript"></script>
    <script src="spec/TestHelper.js" type="text/javascript"></script>

    <!—--- Add mobileinit event handler here ----->

    <script src="../../Lib/jqm/jquery.mobile-1.0.min.js" type="text/javascript"></script>
<!--</head>
<body>-->
    <div data-role="page" id="notes-list-page" data-title="My Notes">
        <div data-role="header" data-position="fixed">
            <h1>
                My Notes</h1>
            <a href="#note-editor-page" class="ui-btn-right" data-theme="b" data-icon="plus">New</a>
        </div>
        <div data-role="content" id="notes-list-content">
        </div>
    </div>
<!--</body>
</html>-->

As you can see, the head section of the file has references to the jQuery and jQuery Mobile libraries, the jStorage library, and the application’s files – DataContext.js, Controller.js and NoteModel.js. So far we have created DataContext.js and NoteModel.js. If you’re wondering about Controller.js, we will create it next.

Besides the application’s JavaScript files, we have included a reference to the TestHelper.js file. This file will help us create the dummy notes that the application will render.

Notice also, in the head section, a placeholder for the mobileinit event handler, which we will add later. The mobileinit event is fired on the document object when jQuery Mobile starts to execute. We can bind to this event when we want to run initialization code in our application, or when we need to apply overrides to jQuery Mobile’s defaults.

Let’s focus on the body section of the file now. Here we’ve created a single jQuery Mobile page with a header and a content section. This will be the Notes List page, the main page of the application. The header of this page already contains the New button, which will allow our users to create a new note. We will work on this button in the next chapter of this series.

We will use the content section of the Notes List page, adorned with the data-role=”content” attribute, to render the cached notes. This section is empty now, but in a few minutes we will write the code that inserts the list of cached notes into it.

To finish with the index.html file, let’s create the mobileinit event handler like so:

<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="../../lib/jqm/jquery.mobile-1.0.min.css" rel="stylesheet" type="text/css" />
<link href="css/app.css" rel="stylesheet" type="text/css" />
<script src="../../lib/jqm/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="../../lib/jstorage/jstorage.min.js" type="text/javascript"></script>
<script src="app/DataContext.js" type="text/javascript"></script>
<script src="app/Controller.js" type="text/javascript"></script>
<script src="app/NoteModel.js" type="text/javascript"></script>
<script src="spec/TestHelper.js" type="text/javascript"></script>
<script type="text/javascript">

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

Notes.testHelper.createDummyNotes();

Notes.controller.init();

});

</script>
<script src="../../Lib/jqm/jquery.mobile-1.0.min.js" type="text/javascript"></script>

Notice that the event handler is defined right before the reference to the jQuery Mobile library. As the mobileinit event’s documentation explains, this event is triggered immediately upon execution, which means that any handlers for it should be bound before jQuery Mobile is loaded.

The handler executes a couple of function calls. One is the call to the createDummyNotes() function in the Test Helper module, with which we’re already familiar. The second call is to the init() function of a controller variable that we have not created yet.

Having observed the modular approach we’re following, as well as the naming conventions for the modules, you can probably see where we’re going with this. In effect, we’ve been building our app in a Model-View-Controller fashion, where the NoteModel and Data Context modules function as the model, and the jQuery Mobile pages function as the views. We will build the controller next.

Creating the Controller

Our controller will respond to user input, and will change the models and views accordingly. The application’s controller goes in a new file that we will name Controller.js. Let’s define an empty controller module like so:

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

})(jQuery, Notes.dataContext);

Observe how we’re passing to the controller a reference to the jQuery object, along with a reference to the app’s data context. This will give the controller the ability to respond to user input, and change the model and views as needed.

For now, all we need our controller to do is render the list of cached notes. We already established that the controller will have an initialization function, init(), which will be triggered when the document’s mobileinit event is fired. Let’s create the init() function in the controller module like so:

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

    function init() {

        dataContext.init();
        var d = $(document);
        d.bind("pagechange", onPageChange);

    }

    return {

        init: init
    }

})(jQuery, Notes.dataContext);

The init() function in the controller first triggers the data context’s init() function. This is understandable, as we need the data context fully initialized before we can use it.

You would expect that the next step in the controller’s init() function would be to render the cached notes. That is not wrong, but it would only work for rendering the cached notes upon application initialization. Instead, we’re doing something more useful. We’re binding to jQuery Mobile’s pagechange event. We will use this event to render the cached notes not only when the application starts, but also when the user transitions from the Note Editor page, which we haven’t created yet, to the Notes List page.

Let’s add the onPageChange() handler function to the controller:

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

    var notesListSelector = "#notes-list-content";
var noNotesCachedMsg = "</pre>
<div>No notes cached</div>
<pre>";
    var notesListPageId = "notes-list-page";
    var currentNote = null;

    function init() {

        dataContext.init();
        var d = $(document);
        d.bind("pagechange", onPageChange);

    }

    function onPageChange(event, data) {

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

        switch (toPageId) {
            case notesListPageId:

                renderNotesList();
                break;
        }
    }

    return {

        init: init
    }

})(jQuery, Notes.dataContext);

This handler is relatively simple. It inspects the id attribute of the page we’re transitioning to, available through the data.toPage property, and takes action based on the value of this property. If the id matches that of the Notes List page, the handler invokes the private function renderNotesList(), which we will define next.

The renderNotesList() function will ask the app’s data context for the list of cached notes, and it will render it within the Notes List page. Let’s create this function like so:

function renderNotesList() {

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

    view.empty();

    if (notesList.length === 0) {

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

        var notesCount = notesList.length;
        var note;
        var ul = $("

").appendTo(view);
        for (var i = 0; i < notesCount; i++) {
            note = notesList[i];
$("</pre>
<ul>
<li>"
 + "<a href="\"index.html#note-editor-page?noteId="" data-url="\"index.html#note-editor-page?noteId="">"
+ "</a>
<div>" + note.title + "</div>
<a href="\"index.html#note-editor-page?noteId="" data-url="\"index.html#note-editor-page?noteId="">
"
 + "
</a>
<div class="\"list-item-narrative\"">" + note.narrative + "</div>
<a id="" href="\"index.html#note-editor-page?noteId="" data-url="\"index.html#note-editor-page?noteId="">
"
 + "</a>"
 + "</li>
</ul>
<pre>").appendTo(ul);
        }

        ul.listview();
    }
}

After obtaining the cached notes from the data context, the renderNotesList() function empties the content section of the Notes List page. It then proceeds to either add a “No notes cached” message to the page if the cached notes array is empty, or create an html list with a list item for each cached note.

Observe that after adding the html list to the page, we need to call the jQuery Mobile listview() method on the list. This will apply jQuery Mobile’s list enhancements to the list.

Notice as well that each list item uses the list-item-narrative css class. We will define this class in the app.cc file, which we will place in the css folder:

This is the class definition:

.list-item-narrative
{
color: #666666;
font-weight: normal;
}

With the index.html file and the controller module in place, we are ready to check how we did. Let’s open the index.html file in our favorite browser and see what happens. If everything went well, we should see something like this:

What do you think?

Next Steps

In the next chapter of this series we will work on the features that will allow our users to create, edit and delete notes.

Downloads

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

 

 

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

Learn how divergent branches can appear in your repository and how to better understand why they are called “branches".  Brought to you in partnership with Wakanda

Topics:
css ,mobile ,ajax & scripting

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

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

{{ parent.tldr }}

{{ parent.urlSource.name }}