Sunday, December 16, 2012

Using JQuery and CouchDb to build a simple AJAX web application


I am Sagar S. Vasule initially wrote this post a couple of months ago, while trying out writing a blog on wordpress. That blog didn't go anywhere, but I thought this tutorial on writing an AJAX web application with JQuery and CouchDb might still be of interest to developers playing with CouchDb. Certainly I couldn't find anything similar at the time
I assume that you have CouchDb 0.10 and CouchApp 0.5 installed. Later versions may work, but no guarantees.
First, go to http://127.0.0.1:5984/_utils and create a database called "addressbook". Because we'll be making the functionality to display data before the functionality to create or update it, it's a good idea to use the built-in CouchDb web interface to add a couple of documents. The initial version of our webapp will let us store a mobile number for each person, so add the following 2 documents:
  1. {"type""address""name""Fred""mobile""555-0001"}  
  2. {"type""address""name""Barney""mobile""555-0002"}  
CouchDb uses design documents to expose the database to the world, but editing design documents directly gets a little painful. Couchapp eases this pain, by breaking design documents out into a set of directories and files on the local filesystem representing shows, views, and so on, which can be pushed to the CouchDb server at will. We need to create a couchapp design document, and also a view to retrieve the phone numbers, which we can do using couchapp at the command-line:
couchapp generate app addressbook
couchapp generate view addressbook phonenumbers
The first line creates a directory called addressbook with a skeleton design document set up, and the second creates a view in the design document.
We can push this to the server using:
couchapp push addressbook http://127.0.0.1:5984/addressbook
If you look at the database using the web interface now, you should see the design document "_design/addressbook" in addition to the documents representing the two phone numbers.
The view we will need simply gives each name and mobile number, with name as key, so the view is sorted by name. No reduce function is required, so delete addressbook/views/phonenumbers/reduce.js, and put the following in addressbook/views/phonenumbers/map.js
  1. function(doc) {  
  2.   if (doc.type && doc.type == "address" && doc.name && doc.mobile) {  
  3.     emit(doc.name, doc.mobile);  
  4.   }  
  5. }  
Push the couchapp to the server using
couchapp push addressbook http://127.0.0.1:5984/addressbook
and check that you can see the view at http://127.0.0.1:5984/addressbook/_design/addressbook/_view/phonenumbers
Couchapp has set up an initial index.html for us, which we can edit at addressbook/_attachments/index.html, and view at http://127.0.0.1:5984/addressbook/_design/addressbook/index.html. Make sure that you can view the file ok. You should then alter index.html to contain:

  1. <!DOCTYPE html>  
  2. <html>  
  3.   <head>  
  4.     <title>Simple address book</title>  
  5.     <link rel="stylesheet" href="style/main.css" type="text/css">  
  6. <span style="white-space: pre;"> </span><script src="/_utils/script/json2.js"></script>  
  7. <span style="white-space: pre;"> </span><script src="/_utils/script/jquery.js?1.3.1"></script>  
  8. <span style="white-space: pre;"> </span><script src="/_utils/script/jquery.couch.js?0.9.0"></script>  
  9. <span style="white-space: pre;"> </span><script type="text/javascript">  
  10. <span style="white-space: pre;"> </span></script>  
  11.   </head>  
  12.   <body>  
  13.     <h1>Simple address book</h1>  
  14. <span style="white-space: pre;"> </span><div id="add"><button type="button" id="add">Add</button></div>  
  15.     <div id="addressbook"></div>  
  16.   </body>  
  17. </html>  

The head contains a title, a link to a stylesheet, imports jQuery and jQuery Couch. These are used by the CouchDb web interface, and included with CouchDb by default. It then has a space for our own javascript to go - more on this later. The body contains an add button within a div, and an empty div called addressbook. The app will display address book entries in the addressbook div.
We use javascript to access data in the CouchDb, and to process events from the browser, such as buttons and links being clicked. Conceptually, we can think of CouchDb as the Model, the DOM representing the HTML shown to the user as being the view, and our javascript as being the Controller, albeit with relatively little separation in this example.
Javascript typically uses a continuation-style to process events. For example, we call asynchronous functions, for example to request data from CouchDb, and pass them a function to call when they return, eg to update the view with the data we receive. A simple example:
  1. $.couch.db("addressbook").saveDoc(  
  2.   {type: "address", name: "Wilma", mobile: "555-003"},  
  3.   {success: function() { alert("Saved ok."); }}  
  4. );  
This example saves a new document to the Couch database, and if successful, pops up an alert.
Another example is jQuery, which calls functions when links or buttons are clicked. The following example will trigger an alert whenever an "a" element is clicked (ie a hyperlink):
  1. $("a").click(function() { alert("Click"); })  
jQuery uses selectors to control what elements of an HTML document will trigger functions. In the above example, the selector applied to all "a" elements. We can use "a#blah" to select all "a" elements with an id attribute of "blah", or "a.blah" to select all "a" elements with a class of blah. Selectors form a mini-language and the jQuery documentation gives more details.
The first javascript we add is called when the base document has finished loading. It retrieves the JSON object of the phonenumbers view from CouchDb. When the JSON object is retrieved a continuation iterates over each row in the view, adding a div for each row containing the name, phonenumber and links to edit and delete it:
  1.   $db = $.couch.db("addressbook");  
  2.   
  3.   function refreshAddressbook() {  
  4.    $("div#addressbook").empty();  
  5.    $db.view("addressbook/phonenumbers", {  
  6.     success: function(data) {  
  7.      for (i in data.rows) {  
  8.       id = data.rows[i].id;  
  9.       name = data.rows[i].key;  
  10.       phonenumber = data.rows[i].value;  
  11.       html = '<div class="address">' +  
  12.        '<span class="name">' + name + '</span> ' +  
  13.        '<span class="phonenumber">' + phonenumber + '</span> ' +  
  14.        '<a href="#" class="edit">edit</a> '+  
  15.        '<a href="#" class="delete">delete</a> '+  
  16.        '</div>';  
  17.       $("div#addressbook").append(html);  
  18.      }  
  19.    }});  
  20.   }  
  21.   
  22.   $(document).ready(function() {   
  23.    refreshAddressbook();  
  24.   });  
A few things to note:
  • We have created a variable $db to access the CouchDb database. If we change the name of the database, this means we only have to change one place.
  • The function called on success(ful retrieval of data from CouchDb) takes a parameter, data, which is the data returned fromt the view.
  • We call the function from a continuation passed to $(document).ready(). This ensures that we won't request data until the HTML document is fully loaded.
  • The links have an id, which is the id of the document representing the address book entry.
If we push to the server now, using
couchapp push addressbook http://127.0.0.1:5984/addressbook
and refresh in the browser, we should see the page load and then populate with phone numbers from the database. The add button and the edit and delete links will be inactive, but at least the R part of our CRUD app works.
The next thing to focus on is deleting entries. A nice way for this to work is for the delete link, when clicked, to pop up further links to confirm or cancel deletion. We can use jQuery to insert these links in the div for the address book entry when the delete links are created.
But this poses a problem - jQuery binds functions to the DOM elements that exist at the time. The links we are proposing adding do not exist when the $(document).ready continuation is called, so we cannot bind functions to them until they are created. Fortunately, there is a trick we can use. Events such as clicks propogate up the DOM tree if they are not processed - so a click will first be processed by the a element, if a handler exists, else it will be processed by the containing div, for example, if a handler exists, all the way up to the document itself. The continuation takes a parameter, which can be used to determine the exact element that was clicked.
In our example, we can bind a function to the click event of div#addressbook, and then check whether the click came from an edit link, a delete link, or the confirm or delete links we will create. Add the following to the $(document).ready continuation:

  1. $("div#addressbook").click(function(event) {  
  2.   var $tgt = $(event.target);  
  3.   if ($tgt.is('a')) {  
  4.      id = $tgt.attr("id");  
  5.      if ($tgt.hasClass("edit")) {  
  6.       // TODO: implement edit functionality     
  7.      }  
  8.      if ($tgt.hasClass("delete")) {  
  9.       html = '<span class="deleteconfirm">Sure? <a href="#" class="dodelete">Yes</a> <a href="#" class="canceldelete">No</a></span>';  
  10.       $tgt.parent().append(html);  
  11.      }  
  12.      if ($tgt.hasClass("dodelete")) {  
  13.       $db.openDoc(id, { success: function(doc) {  
  14.        $db.removeDoc(doc, { success: function() {  
  15.         $tgt.parents("div.address").remove();       
  16.        }})  
  17.       }});        
  18.      }  
  19.      if ($tgt.hasClass("canceldelete")) {  
  20.       $tgt.parents("span.deleteconfirm").remove();  
  21.      }  
  22.     }  
  23.    });  

This code runs when any element in the addressbook div is clicked. It gets the clicked element, the id of that target element (which we set above to be the relevant CouchDb document's id), and then runs different code depending on the class of the clicked element. If the link with class "delete" is clicked, we append two new links - "Yes" with class "dodelete", and "No" with class "canceldelete" - within a span with class deleteconfirm. Then, if a link with class "canceldelete" is clicked, we remove the relevent "confirmdelete" span from the DOM to delete the confirm message. On the other hand, if a link with class "dodelete" is clicked, we call $db.openDoc to retrieve the document to delete from CouchDb, and pass it, using a success continuation to $db.removeDoc. We need to retrieve the document prior to deletion in CouchDb, to avoid deleting from an older version than the most current in the database. If this is successful, we remove the parent address div from the DOM, to match the database. Take a deep breath, and read that again.
If you push this design document to CouchDb, you should be able to delete entries from the address book. However, you will have to use the CouchDb built-in interface to re-add them at this point.
We adopt a similar approach for editing or creating new entries - clicking the button or link exposes a form to enter new details. CouchDb allows us to save a document with an id to indicate an update, or without an id to indicate that one should be generated, and a new document created. Consequently, we can use the same form for creating and editing if we are careful.

Add the following function between the refreshAddressbook function and the $(document).ready
  1. function addUpdateForm(target, existingDoc) {  
  2.   html = '<form name="update" id="update" action=""><table>' +  
  3.     '<tr><td>Name</td><td>Number</td></tr>' +  
  4.     '<tr>' +  
  5.       '<td><input type="text" name="name" id="name" value="' +  
  6. <span style="white-space: pre;"> </span>    (existingDoc ? existingDoc.name : "") + '"/></td>' +  
  7.       '<td><input type="text" name="mobile" id="mobile"/ value="' +   
  8. <span style="white-space: pre;"> </span>    (existingDoc ? existingDoc.mobile : "") + '"></td>' +   
  9.       '<td><input type="submit" name="submit" class="update" value="' +   
  10. <span style="white-space: pre;"> </span>    (existingDoc?"Update":"Add") + '"/></td>' +   
  11.       '<td><input type="submit" name="cancel" class="cancel" value="Cancel"/></td>' +   
  12.     '</tr>' +  
  13.   '</table></form>';  
  14.   target.append(html);  
  15.   target.children("form#update").data("existingDoc", existingDoc);  
  16. }  

Apologies for the inelegant table. I would love to hear a better way for laying out forms. In the meantime, this does the job. And the job is that it appends a form to the target element. If existingDoc is provided, its name and mobile values are used to populate the form, else blanks are used. Further, the existing document is attached to the form, so it can be updated with new values provided by the user to save data later.
To display the form when an edit link is clicked, add the following in place of the edit TODO above:
  1.      if ($tgt.hasClass("edit")) {  
  2.       $("button#add").show();  
  3.       $("form#update").remove();  
  4.       $db.openDoc(id, { success: function(doc) {  
  5.         addUpdateForm($tgt.parent(), doc);  
  6.       }});        
  7.     }  
 And to display the form when the add button is clicked, add a continuation to the bottom of the $(document).ready continuation:
  1.    $("button#add").click(function(event) {     
  2.     $("form#update").remove();  
  3.     $("button#add").hide();  
  4.     addUpdateForm($("div#add"));  
  5.    });   
Note that we hide the add button when it is clicked, and show it otherwise. We also remove any existing forms, so only one form shows at any time.
Finally, we need to process the events when the update(/add) button is hit, or the cancel button. We could adopt the same approach as above, of binding to an element up the hierarchy, but I've chosen to demonstrate a different approach. The .live method can be used to bind to a selector, and to any future elements that selector applies to. That is, as new DOM elements are created, the continuation will apply to them also. Here is an example to bind the click events, of input elements of class "cancel", which hides the form and shows the add button when the cancel button is pressed:
  1.     $("input.cancel").live('click', function(event) {  
  2.     $("button#add").show();  
  3.     $("form#update").remove();  
  4.     return false;  
  5.    });    
 You should add this to the end of the $(document).ready continuation.
Finally, we need to process the update/add event. The key here is to pick up the existing CouchDb document, if it exists, from the form DOM element we attached it to. This ensures we are updating a CouchDb document with the appropriate id and revision. If there is no existing document, we can populate an empty JSON object, and CouchDb will save it as a new document with a uniquely generated id. Here is the continuation and binding to add to $(document).ready:
  1.    $("input.update").live('click', function(event) {  
  2.     var $tgt = $(event.target);  
  3.     var $form = $tgt.parents("form#update");  
  4.     var $doc = $form.data('existingDoc') || {};  
  5.     $doc.type = "address";  
  6.     $doc.name = $form.find("input#name").val();  
  7.     $doc.mobile = $form.find("input#mobile").val();  
  8.     $db.saveDoc(  
  9.      $doc,  
  10.      {success: function() {  
  11.       $("button#add").show();  
  12.       $("form#update").remove();  
  13.       refreshAddressbook();  
  14.     }});  
  15.     return false;  
  16.    });  
Note that we remove the form and show the button when the document has been saved.
Ok, that's it. Push this to CouchDb, and reload the page. You should find that you have a perfectly working, albeit minimal, CRUD webapp.
There are a few other things that you would want to do in production code, which I have left out here for clarity and brevity. Verification functions should be added to the design document, to avoid badly formed data being passed to CouchDb. Errors should be handled, as well as successes. Also, you might want slightly more from an address book webapp. Nonetheless, I hope this is a useful example of using CouchDb and jQuery to make an AJAX webapp.

No comments:

Post a Comment