
In early drafts of the book, Sofa was purely an Ajax application. As CouchDB gained features to allow it to render HTML, Sofa was upgraded to use _show and _list functions, making it crawlable by webspiders and more accessible via screenreaders. The techniques to build Ajax applications with CouchDB are important, so we’ve left Sofa’s comment handling as client-side Ajax code.
It would be possible to render a blog post and all its comments with a single list query, but if we do that for the book, we miss the chance to teach _show functions, as well as the opportunity to handle comments with Ajax. If you are interested in the view you’d use to get a blog post and it’s comments in a single query, the first and best resource on the topic is Christopher Lenz’s post on _CouchDB Joins, which can be found here: http://www.cmlenz.net/archives/2007/10/couchdb-joins
Let’s jump back to the post page. The show function renders the blog post into HTML, but has no access to the comments. In order to add comments, we’ll need an Ajax request to the database to load a view of them. Then we’ll need to render them in the browser. But before we can render any comments we’ll need a form to accept them, so we’ll start from that end, creating our first comment before we write the code to display them.
The first thing we’ll need is an HTML form to accept user input. In the previous section, we started with document format design, and here we’re starting from the HTML. Comments aren’t as intergral to the application, so instead of starting from a document schema, we’ll just start from the UI. Sometimes it can be more relaxing to just build the form you want, and let CouchDB handle the rest.
This is what it looks like:
The form for adding comments is presented here, in impressionistic fashion. We’re missing the HTML context, but we’ll be going through the JavaScript in a bit, so seeing the form alone should explain what we’re starting with.
$("#preview").click(function() { var doc = commentForm.localDoc(); var html = B.formatBody(doc.comment, doc.format); $('#comment-preview').html(html); });
The next step is coming up with the document format, which should be pretty obvious. We’ve got the comment, and the person who wrote it. There are a few other administrative details, like the created_at timestamp, but now that we’ve gone through the exercise of designing the post JSON document format, the pieces should be familiar.
The _id field of a comment is a UUID. Using universally unique identifiers (UUIDs) means we never have to worry about _id collisions, while at the same time not needing to query all future nodes that will interact with our document to ensure that the _id is not taken.
When we saved posts, we saved them with ids set as slugs - to make for better looking urls. Comments follow the more common pattern of using UUIDs for CouchDB documents. There is an API endpoint which provides UUIDs to clients, even thin clients like browsers can request a batch of UUIDs and use them one at a time as they create documents.
Each comment contains a post_id field, which is used to link it to the originating post. The view query run by each post page uses a range that encompasses all and only the comments on that particular post.
The validation function covers all the required fields. In particular it requires that commenters provide a name and email address. The function also checks timestamps and for the presence of the html comment body.
if (newDoc.type == 'comment') { require("created_at", "post_id", "comment", "html", "format", "commenter"); assert(newDoc.commenter.name && newDoc.commenter.email, "Comments must include name and email."); if (newDoc.commenter.url) { assert(newDoc.commenter.url.match(/^https?:\/\/[^.]*\..*/), "Commenter URL must start with http://."); } }
Apply callbacks…
Couchapp…
save and reload
function(doc) { // !code helpers/md5.js if (doc.type == "comment") { doc.commenter.gravatar = hex_md5(doc.commenter.email); emit([doc.post_id, doc.created_at], doc); } };
{"id":"63b450e76decf62cad4a5d02b5a99b55","key":["Fixing-HTML-5-Storage","2009/07/17 04:59:30 +0000"],"value":{"_id":"63b450e76decf62cad4a5d02b5a99b55","_rev":"1-2233552691","type":"comment","post_id":"Fixing-HTML-5-Storage","format":"markdown","commenter":{"name":"J Chris A","email":"jchris@apache.org","url":"http://jchrisa.net","gravatar":"85115a9ef8f1cdece656c7527c866635"},"comment":"What about in the 64-core future?","html":"<p>Even today, web workers can't access the lock. What about in the 64-core future?</p>","created_at":"2009/07/17 04:59:30 +0000"}}, {"id":"63981899ad6860460c2b68b79b9f711b","key":["Fixing-HTML-5-Storage","2009/07/17 05:57:23 +0000"],"value":{"_id":"63981899ad6860460c2b68b79b9f711b","_rev":"1-1987501438","type":"comment","post_id":"Fixing-HTML-5-Storage","format":"markdown","commenter":{"name":"David","email":"ddahl@mozilla.com","url":"http://daviddahl.blogspot.com","gravatar":"dc1340eac16f491c03f11cd7ee06d0a6"},"comment":"There is no reason why there cannot be both a new-style \"couchy\" API and SQL for when you actually need speedy database queries...","html":"<p>There is no reason why there cannot be both a new-style \"couchy\" API ..</p>","created_at":"2009/07/17 05:57:23 +0000"}}, {"id":"77ef88974aa14201467001892fd24958","key":["Fixing-HTML-5-Storage","2009/07/21 20:06:24 +0000"],"value":{"_id":"77ef88974aa14201467001892fd24958","_rev":"1-3790901815","type":"comment","post_id":"Fixing-HTML-5-Storage","format":"markdown","commenter":{"name":"Jens Alfke","email":"jens@mooseyard.com","url":"http://mooseyard.com/Jens","gravatar":"edbd5f1c2f535b14165ae883fa7c3f37"},"comment":"The restriction against allowing worker threads to access local storage is pretty serious, IMHO... to enable these kinds of things.","html":"<p>The ... things.</p>","created_at":"2009/07/21 20:06:24 +0000"}} ]}
easiest way to build a CouchApp. not restful… working to make it just as easy using show and list. getting there.
function displayComments() { app.design.view("comments", { startkey: [docid], endkey: [docid, {}], reduce : false, success: function(json) { var ul = $("#comments ul"); for (var i=0; i < json.rows.length; i++) { var c = json.rows[i].value; ul.append(B.commentListing(c)); } $('#comments').fadeIn(2000); } }); };