Sunday, August 7, 2011

Client Side Javascript MVC: The Immutable Model

Imagine I have the following code in my Controller:
... Groups.load(groupId, function(groups){ groups.each(function(i,group){ doSomethingWithTheGroup(group); }); });

The above code calls into the following DAO and Business Object:
var Groups = (function(){ function load(id, callback){ $.get("/pathTO/"+id+"/data.xml", {}, function(xml){ $(xml).find('resultData').each(function(i,xml){ groups.push(Group(xml)); }); callback(groups); }); } //The following function defines the Business Object: function Group(adXML){ var xmlParser = XmlParser(adXML); return{ getColor:function(){return xmlParser.extractNumber("color");}, getName:function(){return xmlParser.extractText("name");} } } return{ load:load } }());

Notice that the XML parser is called every time a field is read.  My XML parser object has a caching mechanism that stores calls in a map to make things go a little faster.  Group is a Business Object. In this example it's anemic and immutable, but some additional behavior can be added to make it more useful.  An anemic domain model in client-side code isn't necessarily a bad thing because this is only a representation of the true domain model on the server (which should NEVER be anemic). If I want to change data, I pass a form containing named fields into an object capable of extracting the data - a form serializer which can then be dispached the above or some other DAO which would then return a fresh Business Object.  That controller code would look like this:
function submitForm(element){ var form = getFormHandlerForElement(element); GroupSerializer.saveGroup(form, { success: function(group){alert("success")}, error: function(errors){alert("error")} }); }

And, the GroupSerializer would look like this:
var GroupSerializer = function(){ function serialize(form){ ... var ret = createNode("data", ""); ret.append(createNode("name", form.get("name"))); return ret; } function saveGroup(form, handlers) { $.ajax({ url: "/member/advertiser/"+ Contact.getContact().getCid() +"/ad.xml", data: serialize(form), contentType: "application/xml", dataType: "xml", success: function(response){ if($(response).find("errors>error").size()>0){ handlers.error(Error.errors($(response))); }else{ handlers.success(Groups.Group($(response))); } } }); } return{ saveGroup:saveGroup } }

Purists may exclaim, "But, that's putting the part of the DOM, a view-layer object, into the DAO, and that's violating the multi-layered approach to OOP!"  Perhaps.  I consider the DOM a view/controller-specific tree of objects, but a portion of the DOM can be considered cross-cutting domain objects.  Certain attributes in the HTML are intended for this purpose, such as the input's name attribute specifically intended to map the input object to some named field for the purpose of serialization.  I maintain that this is satisfactorily reusable and easy to test.

But, some HTML fields (such a file upload fields, or selects) are a little harder to access than others, and the TYPE of field should be left in the view where it belongs.  The FormSerializer and DAOs should treat all form elements as standard input elements with a name and a val().  To accomplish this, the view should look kind of like:
<script id="formTemplate" type="text/x-jquery-tmpl"> <form> <input type="hidden" name="groupId" value="${getId()}"> <select onchange="GroupController.updateHiddenWithSelected('groupId',this)"> <option {{if isInGroup(0)}} selected {{/if}} value="0">all groups</option> <option {{if isInGroup(1)}} selected {{/if}} value="1">another group</option> </select> </form> </script>

The key is the updateHiddenWithSelected in the controller and its associated hidden field. Any data the user changes that can't be expressed in an input with a val() using a name can be pushed into a hidden field that can by copying the state of the visible field into the invisible field every time the visible field changes.

No comments:

Post a Comment


Bookmark and Share