Friday, February 26, 2010

Lists of Hashes of Hashes in CouchApp

‹prev | My Chain | next›

My exploration of couchapp is nearly at an end. I have a good grasp of edit/updates, of the various javascript callbacks when saving, and even how to upload images. The last thing that I am not quite sure about is deep data structures. More to the point, what about lists of deep data structures?

So far, I have edited only top level attributes of recipes in my cookbook: title, summary, instructions. If I want to use couchapp to edit recipes, however, I must be able to edit the list of tools used or the list of ingredients. Ingredients are especially problematic because we store the ingredient of a recipe as part of ingredient preparation:
{
"brand":"Trader Joe's",
"quantity":1.0,
"order_number":7,
"unit":"jar",
"description":"",
"ingredient":{
"kind":"marinated",
"name":"artichoke hearts"
}
}
If we want the actual ingredient used (marinated artichoke hearts), it is readily available in the "ingredient" attribute. The "ingredient" attribute is just one part of a preparation which includes meta information about the particular ingredient, the amount used and a description of any preparation done to the ingredient (e.g. minced). This might be an overly complex way of storing this information, but we grew into this data structure over the years and are not going to abandon it. The question is, how do we edit it in a web form such that couchapp can readily convert it back-and-forth to JSON for storage in CouchDB?

The most obvious way to accomplish this is to simply expose the JSON to the user in textareas. This is not all that outlandish—we used to edit the same data structure in XML by hand!

Let's presume that we want something a little less painful. Since couchapp will not work without javascript, the ultimate solution to this will likely involve a javascript widget that lists existing ingredient preparations. When an individual item is clicked (or a new item is added) the user would be presented a dialog to edit the ingredient preparation info. When done, the javascript would convert the contents of the dialog into JSON for PUTting into the database.

That seems very do-able, but how about a simple HTML form to edit the entire list?

Couchapp already has the ability to translate deep hash structures into JSON. An HTML form field named preparation-ingredient-name with a value of "artichoke heart" would get translated into the following JSON:
"preparation":{
"ingredient":{
"name": "artichoke heart"
}
}
That is pretty close to what I ultimately want. Perhaps a little tweak will get me what I want...

I create an ingredients.html template that contains fields for two ingredients:
  <tr>
<td><input type="text" name="preparations-0-brand" size="5"></td>
<td><input type="text" name="preparations-0-quantity" size="5"></td>
<td><input type="text" name="preparations-0-unit" size="5"></td>
<td><input type="text" name="preparations-0-ingredient-name" size="5"></td>
<td><input type="text" name="preparations-0-ingredient-kind" size="5"></td>
</tr>
<tr>
<td><input type="text" name="preparations-1-brand" size="5"></td>
<td><input type="text" name="preparations-1-quantity" size="5"></td>
<td><input type="text" name="preparations-1-unit" size="5"></td>
<td><input type="text" name="preparations-1-ingredient-name" size="5"></td>
<td><input type="text" name="preparations-1-ingredient-kind" size="5"></td>
</tr>
I then list each of those fields in couchapp's docForm() that maps form elements to JSON attributes:
<script src="/_utils/script/json2.js"></script>
<script src="/_utils/script/jquery.js?1.2.6"></script>
<script src="/_utils/script/jquery.couch.js?0.8.0"></script>
<script src="<%= asset_path %>/vendor/couchapp/jquery.couchapp.js"></script>
<script type="text/javascript" charset="utf-8">
$.CouchApp(function(app) {

app.docForm("form#update-recipe", {
id : <%= docid %>,
fields: ['title',
"preparations-0-brand",
"preparations-0-quantity",
"preparations-0-unit",
"preparations-0-ingredient-name",
"preparations-0-ingredient-kind",
"preparations-1-brand",
"preparations-1-quantity",
"preparations-1-unit",
"preparations-1-ingredient-name",
"preparations-1-ingredient-kind"
],
beforeSave: function(doc) {
},
success: function(res, doc) {
$('#saved').fadeIn().animate({ opacity: 1.0 },3000).fadeOut();
}
});
});
Amazingly, when I enter some values for a test document, the save succeeds:



It succeeds, but it create a hash of ingredient preparations rather than a list:



It should not be too difficult to convert that into an array. In fact, isn't there a beforeSave() callback that might help? Why yes there is:
//...
beforeSave: function(doc) {
var preparations = [];
for (var prop in doc.preparations) {
preparations.push(doc.preparations[prop]);
}
doc.preparations = preparations;
},
//...
That will collection the entries in the preparations hash, push them onto the preparations local array variable, which then replaces the hash. Now, when I save, the preparations attribute is an array:



That was actually pretty easy!

The best part about this approach, aside from the relative ease of setting up the array, is that couchapp is still able to map the array of preparations back into form fields for editing. This is because access to the name attribute of the first ingredient preparation is the same regardless of hash/array:
recipe['preparations']['0']['ingredient']['name']
If I end up using this solution in a live app I will have to come up with a way of getting the right number of ingredient preparation rows in the table. That is not trivial because the mini-template implementation that couchapp uses does not support loops. I suppose I could pass in the number of ingredients (plus some padding for new ingredients) and use a jQuery on document ready function to clone row zero. I would also need some way of dynamically populating the "fields" attribute in the couchapp docForm() method. I am not too concerned with this so I will probably let it be until/if I do this for real.

Day #26

No comments:

Post a Comment