Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with:

FLEX On jQuery: Array Collections For jQuery Data Binding

Posted by Ben Nadel

The other day, on my FLEX On jQuery blog entry, Johan brought up data binding in the comments. Several other people have also mentioned data binding in the context of jQuery. Data binding, as a concept, is something that I am only vaguely familiar with; as such, I wanted to start playing around with some data binding ideas. The first thing I did was look at the FLEX documentation to see how data binding works.

 
 
 
 
 
 
 
 
 
 

Data binding is based on a class of components known as data providers. Data providers wrap around common data collections and expose event hooks in addition to standardized access and mutation methods. Because core data collection objects (such as the Array) don't announce mutation events, the data providers proxy the mutation methods, altering the internal data representation and triggering "collectionChange" events. UI components then subscribe to these "collectionChange" events and update their visual rendering as the underlying data collection is altered.

From what I have read, all data providers implement three interfaces:

  • IList
  • ICollectionView
  • IViewCursor

Since I don't have the understanding at this time to explore all of these interfaces, I poked around a bit and picked out what I thought would be the most useful methods for this experiment. From the IList interface, I selected the following access and mutation methods:

  • addItem( item )
  • addItemAt( item, index )
  • getItemAt( index )
  • getItemIndex( item )
  • removeAll()
  • removeItemAt( index )
  • setItemAt( item, index )
  • toArray()

Then, I looked at the ICollectionView interface and got the idea of the "collectionChange" event. This event will be triggered whenever the data provider's internal data collection is altered. The collectionChange event can have several different "kind" values depending on the way in which the data collection was altered (ADD, REMOVE, MOVE, RESET, etc.). After reading the documentation, I don't fully understand how the various kind values are implemented, so I just did some rough guesstimations.

Based on these interfaces, I created an ArrayCollection Javascript class that wraps around a core Array object. This ArrayCollection can act as a data provider; but, before we look at how the data provider interfaces are implemented, let's take a look at how this might be used. In the following demo, I have a select box and an input box. The input box will be used to alter the ArrayCollection data provider. The ArrayCollection will then announce "collectionChange" events which the select box will respond to by updating its own list of options.

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>jQuery Array Collection And Data Binding</title>
  • <script type="text/javascript" src="../jquery-1.4.2.js"></script>
  • <script type="text/javascript" src="arraycollection.js"></script>
  • <script type="text/javascript">
  •  
  • // When the DOM is ready, initialize.
  • jQuery(function( $ ){
  •  
  • // Create an instance of our Array Collection bindable
  • // data provider.
  • var collection = new ArrayCollection() ;
  •  
  • // Get our select object reference.<br>
  • var list = $( "select" );
  •  
  • // Get our item input.
  • var item = $( "input[ name = 'item' ]" );
  •  
  • // Get our form.
  • var form = $( "form" );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Bind to the form submit so we can add and remove items
  • // from the collection.
  • form.submit(
  • function( event ){
  • // Prevent the default event - we don't actually
  • // want to submit the form.
  • event.preventDefault();
  •  
  • // Try to get the index of the given value in the
  • // collection.
  • var index = collection.getItemIndex( item.val() );
  •  
  • // Check to see if the item was found. If it was
  • // found, then we will remove it - if not, we
  • // will add it.
  • if (index == -1){
  •  
  • // We are adding an item.
  • collection.addItem( item.val() );
  •  
  • } else {
  •  
  • // We are removing an item.
  • collection.removeItemAt( index );
  •  
  • }
  •  
  • // Clear the item form field.
  • item.val( "" );
  • }
  • );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Bind to the collection change event so that we can
  • // update the select options as the collection changes.
  • // In this way, we can have external forces changing
  • // the collection and then we can re-render / respond to
  • // such changes in a centralized way.
  • collection.bind(
  • "collectionChange",
  • function( event ){
  • // Check to see what kind of event it was - if we
  • // were adding or removing an item.
  • if (event.kind == ArrayCollection.CollectionEventKind.ADD){
  •  
  • // We are adding an option.
  • list[ 0 ].options[ event.location ] = new Option(
  • event.items[ 0 ]
  • );
  •  
  • } else {
  •  
  • // We are removing an option.
  • list[ 0 ].options[ event.location ] = null;
  •  
  • }
  • }
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • jQuery Array Collection And Data Binding
  • </h1>
  •  
  • <form>
  •  
  • <p>
  • <strong>Collection:</strong><br />
  • <select size="8" style="width: 200px ;">
  • <!-- To be populated. -->
  • </select>
  • </p>
  •  
  • <p>
  • <input type="text" name="item" size="29" />
  • <input type="submit" value="Add / Remove Item" />
  • </p>
  •  
  • </form>
  •  
  • </body>
  • </html>

As you can see above, I am binding to the submit event on the form. When the user submits the form, I am checking to see if the item value is in the current ArrayCollection instance. If it is, I remove it from the ArrayCollection; if it is not, I add the item value to the ArrayCollection.

I am also binding to the "collectionChange" event on the ArrayCollection. When the "collectionChange" event is triggered, I am using the "kind" property to figure out how to respond to the change - adding or removing Options. In a FLEX application, this change response would be encapsulated in a UI component class (ex. DataGrid); but, since we don't have a real UI component class in this demo, the change response is just being handled in a plain old event binding.

Now that we see how the data binding can be used with a data provider, let's take a look at the ArrayCollection Javascript class. If you are a FLEX developer, I am sure you will see that I taking some huge stabs in the dark here; I am not yet fully aware of how the events are supposed to be triggered. For example, I have no idea at all when a MOVE event would be triggered?

  • // Define the Array Collection class.
  • ;var ArrayCollection = (function( $ ){
  •  
  • // I am the constructor for the array collection class.
  • function Collection( data ){
  • var self = this;
  •  
  • // I am the internal data stucture used to store the
  • // collection items. This must be an array - if is not,
  • // then just store an empty array.
  • this.data = ($.isArray( data ) ? data : [] );
  •  
  • // I am the event manager - I am a jQuery'ized reference
  • // for use with event binding / triggering. This makes use
  • // of the built-in jQuery event framework that can be bound
  • // to Javascript objects.
  • this.eventManager = $( this );
  • }
  •  
  •  
  • // I define the type of ways in which a collection can be
  • // changed in a mutation event.
  • Collection.CollectionEventKind = {
  • ADD: "add",
  • MOVE: "move",
  • REMOVE: "remove",
  • REPLACE: "replace",
  • REFRESH: "refresh",
  • RESET: "reset"
  • };
  •  
  •  
  • // Define the collection class methods.
  • Collection.prototype = {
  •  
  • // I add the specified item to the end of the list.
  • addItem: function( item ){
  • // Push the item at the end.
  • this.data.push( item );
  •  
  • // Trigger the collection change to indicate that an
  • // item was added to the collection.
  • this.trigger({
  • type: "collectionChange",
  • kind: Collection.CollectionEventKind.ADD,
  • location: (this.data.length - 1),
  • oldLocation: -1,
  • items: [ item ]
  • });
  • },
  •  
  •  
  • // I insert the specified item at the given index.
  • addItemAt: function( item, index ){
  • // Insert the item at the given index.
  • this.data.splice( index, 0, item );
  •  
  • // Trigger the collection change to indicate that an
  • // item was added to the collection.
  • this.trigger({
  • type: "collectionChange",
  • kind: Collection.CollectionEventKind.ADD,
  • location: index,
  • oldLocation: -1,
  • items: [ item ]
  • });
  • },
  •  
  •  
  • // I handle the binding of events to the event manager.
  • bind: function( eventType ){
  • // Pass the bind off to the event manager.
  • $.fn.bind.apply( this.eventManager, arguments );
  • },
  •  
  •  
  • // I get the item stored at the given index.
  • getItemAt: function( index ){
  • // Return the given item.
  • return( this.data[ index ] );
  • },
  •  
  •  
  • // I get the index of the first matching object within
  • // the data collection.
  • getItemIndex: function( item ){
  • // Loop over the items looking for the first match.
  • for( var i = 0 ; i < this.data.length ; i++){
  •  
  • // Check to see if this item matches.
  • if (this.data[ i ] == item){
  •  
  • // Return this matching index.
  • return( i );
  •  
  • }
  •  
  • }
  •  
  • // If we made it this far, the item could not be found.
  • // Return -1 to indicate failure.
  • return( -1 );
  • },
  •  
  •  
  • // I remove all items from the data collection.
  • removeAll: function(){
  • // Reset the data collection.
  • this.data = [];
  •  
  • // Trigger the collection change to indicate that the
  • // collection has been reset.
  • this.trigger({
  • type: "collectionChange",
  • kind: Collection.CollectionEventKind.RESET,
  • location: -1,
  • oldLocation: -1,
  • items: []
  • });
  • },
  •  
  •  
  • // I remove the item at the given index.
  • removeItemAt: function( index ){
  • // Remove the item at the given index.
  • var item = this.data.splice( index, 1 )[ 0 ];
  •  
  • // Update the length.
  • this.length = this.data.length;
  •  
  • // Trigger the collection change to indicate that an
  • // item was removed from the collection.
  • this.trigger({
  • type: "collectionChange",
  • kind: Collection.CollectionEventKind.REMOVE,
  • location: index,
  • oldLocation: -1,
  • items: [ item ]
  • });
  • },
  •  
  •  
  • // I set the item in the given index (overridding any
  • // existing value that might be there).
  • setItemAt: function( item, index ){
  • // Set the data into the collection at the given index.
  • this.data[ index ] = item;
  •  
  • // Trigger the collection change to indicate that an
  • // item was updated within the collection.
  • this.trigger({
  • type: "collectionChange",
  • kind: Collection.CollectionEventKind.ADD,
  • location: index,
  • oldLocation: -1,
  • items: [ item ]
  • });
  • },
  •  
  •  
  • // I get the current size of the collection.
  • size: function(){
  • return( this.data.length );
  • },
  •  
  •  
  • // I return an array of the internal data.
  • toArray: function(){
  • // Simply return the underlying array.
  • return( this.data );
  • },
  •  
  •  
  • // I handle the triggering of events on the event manager.
  • trigger: function( options ){
  • // Pass the trigger off to the event manager.
  • $.fn.trigger.apply( this.eventManager, arguments );
  • }
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  • // Return the collection class.
  • return( Collection );
  •  
  • })( jQuery );

As you can see, almost every class method of the data provider, ArrayCollection, simply proxies methods on the core data array. These proxies provide two key features:

  • Uniform access and mutation methods. I happen to be using an array in this demo, but I suppose the underlying data item could be any type of object.
  • Mutation events. When the underlying data collection is altered, the data provider triggers events to alert all data consumers to the change.

At it's core, data binding seems to be nothing more than an event subscription and propagation architecture. I would assume that the aspect of data binding that people really want is the automatically updated visual rendering. Ironically, I don't think this part really has all that much to do with the actual data binding. It is up to the individual UI components to implement the UI changes. If you were to build a totally new UI component, data binding wouldn't magically do anything for you - you'd still have to wire the event response and alter the UI yourself.




Reader Comments

Ah Ben, you synergistic beauty, you. This is exactly the stuff I am working on right now, and I can always count on you to use a scalpel where I start with a chainsaw (ew ... messy metaphor).

It's interesting that you bind on submit(), but what about cases where the bindings must happen actively as they often do in FLEX? In those cases, perhaps adding a $(":input") binding to the form could trigger your data bindings, i.e.:

$(":input).keyup( function( event ) {
...
}
});

... of course it would need to be more complex if you have multiple field types (i.e. select = .change()), but this would remove the necessity to hit 'submit'.

synergy: ~~~~~~I am now thinking about a push architecture via JSON... ;)

Reply to this Comment

Hey Ben,

Just so you don't actually have to spend any time looking them up, collection classes don't have to implement all 3 of the interfaces. In fact the thing that makes binding work isn't the result of any of the interfaces (that i am aware of). The fact that ArrayCollection and XMLListCollection both do implement all three is just because they do it all.

IList interface adds to the standard array notation the ability to use functions to access data. This is where you get the addItem getItem removeItem functions.

ICollectionView interface defines the ability for collections to be sorted and filtered. They create a "view" into the data that allows you to display a chunk of data or a "view" of the data that is not nessicarily the whole thing.

IViewCursor interface allows you to search a collection without the usual looping. You can create a cursor then seek for an object. If the object is in the list it does an identity find, if the search object is not it tries to do a property match. Additional from the cursor position you have the ability to move forward and backward through the list.

So each interface defines some unique set of features that the collection classes in Flex implement.

And to that end Flex 4 has a lighter collection class that just implements IList - ArrayList. So you can get all the binding goodness and the get/add/remove functions with a little less bulk than the array collection class.

Probably more info that you actually cared to know, but hey that's what I am good for :)

sim

Reply to this Comment

@Grant,

Yeah, I think you could definitely bind to the inputs themselves; I was just binding to the form since it felt like the easiest approach at the time.

If you are looking for Push stuff, I just signed up for this beta invite (not got the invite yet). Looks like HTML5-based push stuff:

http://pusherapp.com

@Simeon,

Ahh, gotcha. So the binding stuff is more core than any of those interfaces then? Do data providers extend some sort of shared data provider class that accounts for the bindability?

Reply to this Comment

Though I am a toddler in the field of jQuery, I am glad to tell you that I could easily understand data binding that you have provided here. The best of the information is provided here. But I did have trouble implementing it in my system. Will you be able to help me out.

http://www.microsoftproblems.com

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.