jQuery Mobile + CouchDB: Part 2 – Document View

2010 December 13th by todd anderson

In the previous post for this series, I covered getting up and running with jQuery Mobile as a client-side application for a CouchDB database. Along with examples for the DHTML documents, I also covered/talked about some great tools that help along with the workflow of creating a client-side application and working with a CouchDB instance. I am going to continue using those tools – mainly CouchApp – and cover the differences between local and external jQuery Mobile pages and how to show a single document from the albums list. If you have not already, I recommend checking out the first part in this series: jQuery Mobile + CouchDB: Part 1 – Getting Started. Some tools and directory structures are discussed in that post that will not be explained in this post.

Introduction

Looking at the current state of the client-side application created for communicating with a CouchDB instance, the landing page loads all the documents from a target database and displays them in a jQuery Mobile list.

The database for the application holds album documents and a simple view was created that would return every document in the database. Currently the URL for viewing that list of documents from the albums view is http://127.0.0.1:5984/albums/_design/albums/index.html.

Now i can peruse the list just fine, but what if the list items do not show all the fields pertaining to a document? (* they do now, but that is the beauty of CouchDB… the album document schema can change without having me curl up and start crying under my desk!) Good question, me. The application should direct the user to a detail page which shows a single document upon selection from an item in the list. Nice answer, you.

In this post, I will be covering how to create and navigate to a detail page for a single document from the list for our jQuery Mobile + CouchDB application. I’ll hope to address the benefits of local and external pages in the jQuery Mobile framework and how to go about displaying pages using the show functions available in the design for a CouchDB database.

Decisions, Decisions

A jQuery Mobile application is comprised of pages, loosely discussed in the previous post. The framework allows for a developer to declare the content of each page inline in the same document (local, internally linked) or point to another HTML document (external) by providing a link as the content of a page. For some quick examples:

Local:

index.html

...

<body>

    <div id="home" data-role="page">

        <div data-role="header">Home</div>

        <div data-role="content">

            <p>Hello World!</p>

            <p><a href="#signin">Sign in?</a>

        </div>

        <div data-role="footer">welcome</div>

    </div>

    <div id="signin" data-role="page">

        <div data-role="header">Sign up</div>

        <div data-role="content">

            <p><a href="#home">Nevermind</a></p>

        </div>

        <div data-role="footer">come in</div>

    </div>

</body>

...

In this example, two pages are declared (home and signin), and each have a link in their content that points to the other. By setting the href to the value of the id attribute of a div element with a data-role of page, preceded by a hash (#), the framework knows to display the content of that page upon request – updating the location and history as well.

External:

index.html

...

<body>

    <div id="home" data-role="page">

        <div data-role="header">Home</div>

        <div data-role="content">

            <p>Hello World!</p>

            <p><a href="pages/signin.html">Sign in?</a>

        </div>

        <div data-role="footer">welcome</div>

    </div>

</body>

...

/pages/signin.html

<body>

    <div id="signin" data-role="page">

        <div data-role="header">Sign up</div>

        <div data-role="content">

            <p><a href="../index.html">Nevermind</a></p>

        </div>

        <div data-role="footer">come in</div>

    </div>

</body>

[* NOTE :: If you try to develop an application using externally linked pages on your local disk, you will get an error that looks somewhat like the following:

XMLHttpRequest cannot load file:///Users/{user}/{location}/index.html. Origin null is not allowed by Access-Control-Allow-Origin.

The reason is that file:// requests produce a null Origin. Someone more familiar with jquery Mobile could hopefully give a fuller explanation, but a null Origin throws an error on transitioning pages. When developing i deploy the client to my localhost for testing.*]

In this example, the landing page has a link that points to an external HTML document (signin.html). With a div element attributed as a page in the external HTML document, the location is updated to /index.html#pages/signup.html.

Both approaches have their advantages and disadvantages. The biggest advantage for internal pages is transition speed, while the biggest disadvantage for internal pages is readability; and those are switched for external pages. At least those are what i perceive as being the selling points for internal vs external, aside from the URLs…

I would choose external most of the time, mainly because it adds the ability to view a page without being in the flow of a whole page set in a single document. Sure, the URL is way uglier (ie index.html#pages/signup.html as opposed to index.html#signup) but hopefully i could then bug someone smarter than me to pretty up the URLs with some rewrites.

So, in conclusion, I lean toward external pages… but i will offer up both solutions so you can see how each would work.

Local Album Page View

First things first, fire up your favorite text editor and open the _/attachments/index.html page from your CouchApp directory for the albums application. We are going to add another page div to the document and display the album detail locally:

<!DOCTYPE html>

<html>

  <head>

    <title>My Albums</title>

    <link rel="stylesheet" href="style/main.css" type="text/css">

    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>

  </head>

  <body>

    <div data-role="page">

        <div data-role="header"><h2>Albums</h2></div>

        <div data-role="content">

            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />

        </div>

        <div data-role="footer">Footer</div>

    </div>

    <div data-role="page" id="albumview">

        <div data-role="header" id="albumheader">

            <h2 class="albumtitle"></h2>

        </div>

        <div data-role="content" id="albumcontent"></div>

        <div data-role="footer" />

    </div>

  </body>

  <script src="vendor/couchapp/loader.js"></script>

  <script type="text/javascript" charset="utf-8">

      // forget about this for now.

  </script>

</html>

In this example, we have added another page and given it the id attribute value of “albumview“. When an item from the albums list is clicked, we’ll notify the framework to navigate to that page and display the corresponding content. The JavaScript was removed for this example just to clear away the clutter from what the intention was in making a new page. Just so we know we are working with the same content, here is the full updated document with inline script:

<!DOCTYPE html>

<html>

  <head>

    <title>My Albums</title>

    <link rel="stylesheet" href="style/main.css" type="text/css">

    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>

  </head>

  <body>

    <div data-role="page">

        <div data-role="header"><h2>Albums</h2></div>

        <div data-role="content">

            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />

        </div>

        <div data-role="footer">Footer</div>

    </div>

    <div data-role="page" id="albumview">

        <div data-role="header" id="albumheader">

            <h2 class="albumtitle"></h2>

        </div>

        <div data-role="content" id="albumcontent"></div>

        <div data-role="footer" />

    </div>

  </body>

  <script src="vendor/couchapp/loader.js"></script>

  <script type="text/javascript" charset="utf-8">



      $db = $.couch.db("albums");



      function handleDocumentReady()

      {

          refreshAlbums();

      }



      function refreshAlbums()

      {

          $("#albums").empty();

          $db.view("albums/albums",

            { success: function( data ) {

                    var i;

                    var album;

                    var artist;

                    var title;

                    var description;

                    var listItem;

                    for( i in data.rows )

                    {

                        album = data.rows[i].value;

                        artist = album.artist;

                        title = album.title;

                        description = album.description;

                        listItem = "<li>" +

                                    "<h2 class=\"artist\">gt;" + artist + "<\/h2>" +

                                    "<p class=\"title\">gt;" + title + "<\/p>" +

                                    "<p class=\"description\">gt;" + description + "<\/p>";

                        $("#albums").append( listItem );

                    }

                    $("#albums").listview( "refresh" );

                }

            });

      }



      $(document).ready( handleDocumentReady );



  </script>

</html>

As it stands, the HTML that is constructed and appended as a list item to the albums list does not have any clickable navigation. As shown in previous examples, to add navigation to internal pages, we’ll add a link with an href to a page preceded by a hash (#) to each listItem HTML snippet.

ListItem HTML

Now we could just update the HTML string as such:

listItem = "<li>" +

                 "<a href=\"#albumview\"><h2 class=\"artist\">gt;" + artist + "<\/h2><\/a>" +

                 "<p class=\"title\">gt;" + title + "<\/p>" +

                 "<p class=\"description\">gt;" + description + "<\/p>";

With the link wrapping the artist header, upon user-click the document would navigate to the albumview page. The problem with this solution is that the albumview is not populated with any content. We don’t know which list item was clicked or any other relevant information related to the target document. To access that, we’ll switch up how the HTML snippet is created and assign a click handler to each link in order to update a variable accessible between pages.

Make the following modifications the the HTML snippet within the for… loop of refreshAlbums():

for( i in data.rows )

{

    album = data.rows[i].value;

    artist = album.artist;

    title = album.title;

    description = album.description;

    listItem = $("<li/>", {

                        class: "album"

                    });

    header = "<h2 class=\"artist\">" + artist + "<\/h2>";

    albumLink = $("<a/>", {

                            href: "#albumview",

                            "data-identity": album._id,

                            click: function() {

                                albumId = $(this).data("identity");

                            }

                    });

    albumLink.append( header );

    listItem.append( albumLink );

    listItem.append( "<p class=\"title\">" + title + "<\/p>" );

    listItem.append( "<p class=\"description\">" + description + "<\/p>" );

    $("#albums").append( listItem );

}

In this snippet, we have resolved listItem to an HTML entity we can append to and construct a click-able albumLink using object notation. a data-identity property is set in the link to access and assign an albumId which will later be used to request the document once we have landed on the albumview page. The content elements are largely the same as they were previously for the listItem, but we have wrapped the header in a link element.

We will need to bind an event prior to showing the albumview page in order to request the document and display it once we land on the albumview page. The following snippet is the full document with those changes highlighted:

<!DOCTYPE html>

<html>

  <head>

    <title>My Albums</title>

    <link rel="stylesheet" href="style/main.css" type="text/css">

    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>

  </head>

  <body>

    <div data-role="page">

        <div data-role="header"><h2>Albums</h2></div>

        <div data-role="content">

            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />

        </div>

        <div data-role="footer">Footer</div>

    </div>

    <div data-role="page" id="albumview">

        <div data-role="header" id="albumheader">

            <h2 class="albumtitle"></h2>

        </div>

        <div data-role="content" id="albumcontent"></div>

        <div data-role="footer" />

    </div>

  </body>

  <script src="vendor/couchapp/loader.js"></script>

  <script type="text/javascript" charset="utf-8">



      var albumId;

      $db = $.couch.db("albums");



      function handleDocumentReady()

      {

          $("#albumview").bind( "pagebeforeshow", openAlbum );

          refreshAlbums();

      }



      function refreshAlbums()

      {

          $("#albums").empty();

          $db.view("albums/albums",

            { success: function( data ) {

                    var i;

                    var album;

                    var artist;

                    var title;

                    var description;

                    var listItem;

                    for( i in data.rows )

                    {

                        album = data.rows[i].value;

                        artist = album.artist;

                        title = album.title;

                        description = album.description;

                        listItem = $("<li/>", {

                                        class: "album"

                                        });

                        header = "<h2 class=\"artist\">" + artist + "<\/h2>";

                        albumLink = $("<a/>", {

                                        href: "#albumview",

                                        "data-identity": album._id,

                                        click: function() {

                                                albumId = $(this).data("identity");

                                            }

                                        });

                        albumLink.append( header );

                        listItem.append( albumLink );

                        listItem.append( "<p class=\"title\">" + title + "<\/p>" );

                        listItem.append( "<p class=\"description\">" + description + "<\/p>" );

                        $("#albums").append( listItem );

                    }

                    $("#albums").listview( "refresh" );

                }

            });

      }



      function openAlbum()

      {

          $("#albumcontent").children().remove();

            $db.openDoc( albumId,

                  { success: function( data ) {

                          var artist = data.artist;

                          var title = data.title;

                          var description = data.description;

                          var html = "<h2 class=\"artist\">" + artist + "<\/h2>" +

                                        "<p class=\"title\">" + title + "<\/p>" +

                                        "<p class=\"description\">" + description + "<\/p>";

                          $("#albumcontent").append( html );

                          $("#albumheader .albumtitle").text( title );

                  }

              });

      }



      $(document).ready( handleDocumentReady );



  </script>

</html>

In this snippet we have bound the “pagebefoeshow” event to the openAlbum() method handler within the handleDocumentReader() method:

$("#albumview").bind( "pagebeforeshow", openAlbum );

This will invoke openAlbum() prior to transitioning to the albumview page, allowing us to request the document based on the albumId and add elements to the content of the albumview page:

function openAlbum()

{

    $("#albumcontent").children().remove();

     $db.openDoc( albumId,

            { success: function( data ) {

                    var artist = data.artist;

                    var title = data.title;

                    var description = data.description;

                    var html = "<h2 class=\"artist\">" + artist + "<\/h2>" +

                                  "<p class=\"title\">" + title + "<\/p>" +

                                  "<p class=\"description\">" + description + "<\/p>";

                    $("#albumcontent").append( html );

                    $("#albumheader .albumtitle").text( title );

            }

        });

}

… nothing to unfamiliar… and nothing to pretty :) .

Deployment

We can now push our changes to the CouchDB database using the couchapp utliity. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is my CouchApp application directory). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If that was successful and you now visit http://127.0.0.1:5984/albums/_design/albums/index.html, you will be presented with the list of albums. Click on a list item, and you should be navigated to the albumview page which will display the detail of the document selected from the list. It may look something like the following:

I intentionally left the browser wide (and not the simulated size of a mobile device) so you can how see the hash is added to the URL once on the albumview page: http://127.0.0.1:5984/albums/_design/albums/index.html#albumview.

This is all well and good, but it leads to a problem (in my opinion) as to directly sending a user to the albumview with a populated document. As it stands, an album detail can only be seen once a user has clicked on an item from the list. If you just pasted the http://127.0.0.1:5984/albums/_design/albums/index.html#albumview URL in a new browser window, the content would be empty. Not good User Experience.

Fortunately, there is an alternative. We can add a show function to our application in CouchDB to navigate to an external page that has a (somewhat) uglier URI but allows for you to land on a detail page without going through the landing page of the application. yay!

External Album Page View

In the browser, up to this point through the series, we have seen pages that return the JSON object representing rows of documents (ie. http://127.0.0.1:5984/albums/_all_docs) and used design views to create a default landing page to interact with the albums database (http://127.0.0.1:5984/albums/_design/albums/index.html). But there is more to CouchDB. Using shows and lists, you can serve up a document in any type of representation, including HTML. They are restricted to only GET requests, so you can’t save a document in a different format, but you can certainly return a document in another format.

If you look within the /albums directory we created using CouchApp, you may notice a couple folders we have largely been ignoring while we toiled away in _/attachments and /vendorshows and lists. We are not going to look into lists at the moment, but we will create a show function in our application design to return the HTML representation of a document marked-up as a jquery Mobile page. With this scenario, we can treat the show function as an external page view and have a ugl(ier)y URL, but one we can send to anyone and not have to go through list item selection from the main page.

Show Function

As we are moving away from internal pages to external pages for our application, we are going to have each page returned in a show function of our design. The first order of business to return a album detail page upon selection from the list, is to move the HTML markup for albumview into a show design document in the /shows directory of our albums CouchApp application.

Open up your favorite text editor and create a new JavaScript document. Enter in the following function and save it as album.js in the /shows folder:

function(doc, req) {

  var html = "<div data-role=\"page\" id=\"albumview\">" +

                       "<div data-role=\"header\" id=\"albumheader\">" +

                           "<h2 class=\"albumtitle\">" + doc.title + "<\/h2>" +

                       "<\/div>" +

                       "<div data-role=\"content\" id=\"albumcontent\">" +

                           "<h2 class=\"artist\">" + doc.artist + "<\/h2>" +

                           "<p class=\"title\">" + doc.title + "<\/p>" +

                           "<p class=\"description\">" + doc.description + "<\/p>" +

                       "<\/div>" +

                       "<div data-role=\"footer\" \/>" +

                   "<\/div>";

  return html;

}

Each show document must declare the function(doc, reg) method. This is the standard function of a show document which is invoked on a request through _http://127.0.0.1:5984/albums/_design/albums/_show/album/${_id}_ and handed the full corresponding document from the database and the HTTP request object.

If we look at the index.html page we worked on in the previous section (Local Album Page View), we’ll see that what is returned from this show function is really just a mixture of the mark-up for the albumview page and the openAlbum() method. You may notice that we are not sending back a full HTML document from this show function. That is alright for now. This mark-up will be treated as a page and housed and styled by our index.html page. We can probably do some fancy string assembly based on the request path, but for now we are just gonna leave it as an insertion into the jquery Mobile application.

Modifying index.html

Now that we have a show function serving up our abumview pages, we need to clean up our index.html document and change our albumview link from internal to external. Open up the _/attachments/index.html document in your favorite editor and make the following modifications:

<!DOCTYPE html>

<html>

  <head>

    <title>My Albums</title>

    <link rel="stylesheet" href="style/main.css" type="text/css">

    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>

  </head>

  <body>

    <div data-role="page">

          <div data-role="header"><h2>Albums</h2></div>

          <div data-role="content">

              <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />

          </div>

          <div data-role="footer">Footer</div>

      </div>

  </body>

  <script src="vendor/couchapp/loader.js"></script>

  <script type="text/javascript" charset="utf-8">



      $db = $.couch.db("albums");



      function handleDocumentReady()

      {

          refreshAlbums();

      }



      function refreshAlbums()

      {

          $("#albums").empty();

          $db.view("albums/albums",

            { success: function( data ) {

                    var i;

                    var album;

                    var artist;

                    var title;

                    var description;

                    var listItem;

                    var externalPage;

                    for( i in data.rows )

                    {

                        album = data.rows[i].value;

                        artist = album.artist;

                        title = album.title;

                        description = album.description;

                        externalPage = "_show/album/" + album._id;

                        listItem = "<li class=\"album\">" +

                                            "<a href=\"" + externalPage + "\">" +

                                                "<h2 class=\"artist\">" + artist + "<\/h2>" +

                                            "<\/a>" +

                                            "<p class=\"title\">" + title + "<\/p>" +

                                            "<p class=\"description\">" + description + "<\/p>";

                                        "<\/li>";

                        $("#albums").append( listItem );

                    }

                    $("#albums").listview( "refresh" );

                }

            });

      }



      $(document).ready( handleDocumentReady );



  </script>

</html>

There’s no highlighting for what we removed in that snippet, but we took out the albumview page declaration from the body and removed the openAlbum() method (as well as its binding on “pagebeforeshow“). The listItem is now back to an HTML string that is appending to the albums listview. Instead of adding a click handler to a link, the href is attributed as an external jQuery Mobile page which is is a query to show the associated document with the _id. Simple, clean.

Deployment

We modified our application to utilize a show function to serve the albumview page up within a jquery Mobile application. With these changes saved, we can now push to the CouchDB database using the couchapp utility. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is the CouchApp application directory for these examples). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If all was successful and you now go to http://127.0.0.1:5984/albums/_design/albums/index.html, we’ll still have our old familiar list. Click on an item, and we should be navigated to a detail (or albumview) page somewhat like the following:

I left the browser wide again so you can see the new URL for viewing a single album document: http://127.0.0.1:5984/albums/_design/albums/index.html#_show/album/${doc._id}. Pretty neat. Now you can even copy that #show page URL and paste it in another window and you should be taken directly to that detail page. You may notice that we still get the back button in the upper left of the header. That is because of the hash (#) in the URL which the jquery Mobile framework interprets as being a page linked from another. So a false history. If you clicked Back from the page, it would not go to the index.html page. A minor flaw in our design that we are going to let go for now :) Hey, we just got all this neat stuff working… we’ll work out the kinks later. There’s more fun stuff to work on.

Conclusion

Well, i said the subsequent posts after the initial Getting Started would be shorter… i lied :) In any event, we created a detail page for a single document and offloaded the page loading to an external jquery Mobile page utilizing the show function of the albums design. A nice solution with an uglyURL (we can address that later) that can be passed to other people so they can land on that single page without going through the whole application from the beginning. Since I prefer the external page solution, any continuing posts on this example will utilize the show function to serve, at least, the album detail page.

Now what if we wanted to edit the fields of that document… I am on the edge of my seat as i write this. Literally. Someone stacked a bunch of wrapping stuff on this chair and left me little room. I could have moved it, but i was just too excited to talk about forms…

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.