JavaScript Repeater Data Binding
In a previous post, I wrote about a way to convert JSON to XAML. The sample I provided with that post didn't really stop with a simple conversion example, but it also demonstrated how to create an use a simple JavaScript data binding repeater. It's just something I threw together in two minutes to bind an array of objects to a div to basically do in JavaScript/XHTML what ASP.NET does with the Repeater control. I think many people will be interested in this technique to see how they can bind data to a browser in a more efficient way.
As far as my requirements, I wanted this to be like ASP.NET in that the data is bound from a data source matching the fields in the data source objects to the data fields declared in this repeater and after all is said and done, the original declarative code should disappear. As far as the data source, I wanted it to be a series of the same type of object... kind of like a generic List.
So, to start off with, I simply declared the code that I wanted to write as the client. Some people call this the 'Sellsian' method (after Chris Sells at Microsoft), though I simply call it... common sense (as I honestly suspect Chris would too!). So, here is the declarative code I wanted to write for my data binding:
<div id="rptData"> <h2>Title</h2> <div id="title" class="bind"></div> <h2>XAML Output </h2> <div id="output" class="bind"></div> </div>
In this situation the data source fields are matched to element with the bind class and the element id. This is much like <%#Bind("") %> in ASP.NET.
On the other side, I would like my data source to be in JSON format and I would like to be able to bind in a way that 'feels' like a static class. Here is what I decided on:
var dataSource = [{ title: 'Ellipse', output: Xaml.CreateXamlFromJSON(jsonElement1) }, { title: 'Rectangle', output: Xaml.CreateXamlFromJSON(jsonElement2) }, { title: 'Canvas', output: Xaml.CreateXamlFromJSON(jsonElement3) } ]; DataBinder.BindTextRepeater(D('rptData'), dataSource);
In the sample above you can see that I have a data source with 3 objects with the object being defined with the interface of a string field named 'title' and another string field named 'output'. Furthermore, I wanted to call the method what it is: a text repeater, not a fancy object repeater (though building that shouldn't be much more difficult), so my static method is called BindTextRepeater and accepts the declarative repeater object and the data source as parameters. In my examples I use the D('id') syntax where D is simply an alias for document.getElementById. I know some people use a dollar sign for that, but that just looks really weird to me.
Now onto the code. Here is the basic shell:
var DataBinder = { BindTextRepeater: function(obj, ds) { } }
The first thing we do in this situation is look at the data source and see what the objects look like. For this we simply need to create an array to record what fields we are looking at and iterate through the object to record it's fields. Put another way... we simply need to do simple JavaScript reflection and record the object interface, something that's incredibly simple in JavaScript.
var fields = new Array( ); for(var f in ds[0]) { fields.push(f); }
Now that we know what the object looks like, let's iterate through the datasource and bind each field to it's proper place in the repeater. This is what the rest of the method does and it should be fairly self explanatory, except for the following things:
First, I said that data is bound to fields with the 'bind' class. What if you had your own class on it? That's not a problem. JavaScript classes are a bit like .NET interfaces (where as JavaScript ids are a bit like .NET classes), so you can "apply" (or implement in .NET) a few of them. So, if you wanted the apply the class "message-log" to the bindable element, you would simply have the following:
<div class="bind message-log"></div>
In this case this is possible because I'm simply checking to see if the class STARTS with "bind", rather than simply checking to see if it IS "bind":
if(obj.childNodes[e].id && obj.childNodes[e].className && obj.childNodes[e].className.substring(0, 4) == 'bind') { /* ... */ }
Second, if the element if found to be bindable, the method looks through the fields array to see if that element has data for in the specified data field. If so, it binds. If not... there's not much it can do (though ideally you would throw an exception). One thing to note about this is that it replicates the element and binds the text as a child. This is seen by the following line:
var bindableObj = obj.childNodes[e].cloneNode(false);
When you clone a node, you can say true, which means to clone it's children, or you can say false, which means to clone only that particular element. In this case, we don't need the children as this is a text repeater and we are going to put our own text as a child. If we were to say true, we would have to go out of our way to remove the children.
If the element is not found to be bindable, it copies the element and it's children as can be seen as the cloneNode(true).
Third, after the data object is ready you have a duplicate of the original repeater, but now filled with data from the data source. This data object is then bound to the browser's DOM as a element immediately before the repeater template. After all data objects have been bound, the original repeater is removed. Thus, you replaced the repeater template with data bound controls and you're done.
Here is the final implementation of the BindTextRepeater method:
var DataBinder = { BindTextRepeater: function(obj, ds) { var fields = new Array( ); for(var f in ds[0]) { fields.push(f); } for(var r in ds) { var outputObj = DOM.createElement('div'); if(ds[r]) { var d = ds[r] for(var e in obj.childNodes) { if(obj.childNodes[e].nodeType && obj.childNodes[e].nodeType == 1) { if(obj.childNodes[e].id && obj.childNodes[e].className && obj.childNodes[e].className.substring(0, 4) == 'bind') { for(var i in fields) { if(obj.childNodes[e].id == fields[i]) { var bindableObj = obj.childNodes[e].cloneNode(false); bindableObj.appendChild(DOM.createTextNode(d[fields[i]])); outputObj.appendChild(bindableObj); } } } else { outputObj.appendChild(obj.childNodes[e].cloneNode(true)); } } } } obj.parentNode.insertBefore(outputObj, obj); } obj.parentNode.removeChild(obj); } };
Using this same approach of templating XHTML elements and reflecting JavaScript JSON data sources, you could actually create a full scale data binding solution for all your client-side data binding needs. Furthermore, since we used a JSON data source we can now bind data directly from JSON services accessed via Ajax techniques. Lastly, as I hope you can see, there wasn't really much magic in this example and absolutely no proprietary technology. It's simply a usage of what we already have and have had for many, many years.
Links