Proposal for adding Data Linking to jQuery

Proposal for adding Data Linking to jQuery

Hi Everyone,

I am a developer from the Microsoft ASP.NET team. We've put together a proposal for supporting "data linking" in jQuery, and would like your feedback.

I describe data linking at a high level in this post, but this feature is described more fully in a wiki entry posted here:

We created a prototype of data linking and published it on github here:

The repository includes demo-contacts.html which demonstrates the use of data linking in a practical manner, and which also utilizes the jQuery templating library being proposed. I encourage you to try the demo, as the samples posted in the wiki and in this post are contrived just to define the API, and do not convey the usefulness of this plugin as well as the demo does.

The term "data linking" is used here to mean "automatically linking the field of an object to another field of another object." That is to say, the two objects are "linked" to each other, where changing the value of one object (the 'source') automatically updates the value in the other object (the 'target'). Linking may happen between any two objects, such as plain objects, arrays, dom elements, or browser plugins.

Example

As a simple example, you can link an input element to a field of an object like so:

  1. var person = {};
  2. $("#name").linkTo("val", person, "name");
  3. $("#name").val("foo");
  4. alert(person.name); // foo
  5. // ... user changes value ...
  6. alert(person.name); // user typed value

Mutation Events

In order to link a source to a target, it is necessary to be notified when data associated with the source object changes, so that it can be pushed onto the target object. This plugin adds some special events to jQuery to facilitate this, which are also useful on their own.

attrChange

The 'attrChange' event fires when an attribute of a DOM element or object is changed through the jQuery.fn.attr or jQuery.attr methods. An interesting feature of this plugin is that it specifically allows for jQuery.fn.attr to be usable on plain objects or arrays. The data(), bind(), and trigger() methods all work with plain objects, so this is a natural extension which already mostly works.

However, a small change was necessary to jQuery.attr to avoid the special cases applied when the target is a plain object, like class->className, and readonly->readOnly, and that negative values of "width" are ignored, etc. So this plugin also makes it officially possible to use attr() to set fields of an object as you would expect.

  1. function report(ev) {
  2.     alert("Change attr '" + ev.attrName + "' from '" +
  3.         ev.oldValue + "' to '" + ev.newValue + "'.");
  4. }

  5. $("#el").attrChange(report)
  6.     // Change attr 'foo' from 'undefined' to 'bar'
  7.     .attr("foo", "bar").
  8.     // Change attr 'foo' from 'bar' to 'baz'
  9.     .attr("foo", "baz");
  10.     
  11. // restricted scope
  12. $("#el").attrChange("x", report)
  13.    // Change attr 'x' from 'undefined' to '1'
  14.    .attr( { x: "1", y: "2" } );
  15.     
  16. $("#el").attrChange([ "x", "y" ], report)
  17.    // Change attr 'x' from 'undefined' to '1'
  18.    // Change attr 'y' from 'undefined' to '2'
  19.    .attr( { x: "1", y: "2", z: "3" } );
   
The attrChange event can also be used to capture changes made through the val() and data() methods. Notice that special treatment is given to how the change is represented by the event. This consolidation of the different mutation methods causing the same event makes it simpler to handle and prevents the need for separate "dataChange" and "valChange" events. It would be nice, actually, if attr() was thought of as a general purpose mutation method and also supported this construct. For example, $(..).attr(“data:foo”, “bar”) or $(..).attr(“val”, “123”). However, that is not implemented and open to discussion.

  1. // works with data()
  2. $("#el").attrChange("data:foo", report)
  3.    // Change attr 'data:foo' from 'undefined' to 'bar'
  4.    .data( "foo", "bar" )
  5.    // Change attr 'data:!' from '[Object object]' to '[Object object]'
  6.    .data( { } );
  7.    
  8. // works with val()
  9. $("#el").attrChange("val", report)
  10.    // Change attr 'val' from 'hi' to 'bye'
  11.    .val( 'bye' );
   
attrChanging

The plugin also fires an event prior to a change, giving the handler a chance to cancel the operation. This is not strictly required to support linking, but is a simple addition and so is included as a point of discussion. The 'attrChanging' event fires when an attribute of a DOM element or object is about to be changed. The ev.preventDefault() method may be called in order to prevent the change from occuring.

  1. $("#el")
  2.     .attrChanging(function(ev) {
  3.         if (!confirm("Allow changing attr '" + ev.attrName + "' from '" +
  4.             ev.oldValue + "' to '" + ev.newValue + "'?")) {
  5.             
  6.             ev.preventDefault();
  7.         }
  8.     });
  9.     // Allow changing attr 'y' from 'undefined' to '2'?
  10.     // yes: value set, attrChange event raised
  11.     // no: value not set, attrChange event not raised
  12.     .attr("foo", "bar");

arrayChange

Like the attrChange event, but fires when an Array is mutated through any of the new array mutation APIs. Information about what the mutation was is available on the event object.

  1. var arr = [1,2,3];
  2. $([arr])
  3.     .arrayChange(function(ev) {
  4.         alert("Array operation " + ev.change + " executed with args " + ev.arguments);
  5.     });
  6. // Array operation push executed with args 4,5
  7. $.push(arr, 4, 5);

The following array mutation events are available as static methods on the jQuery object: push, pop, splice, shift, unshift, reverse, sort. The arguments supported for each are exactly like their built-ins, except the array is passed as the first parameter.

Like 'attrChange', the 'arrayChange' event supports filtering by the operation.

  1. $([arr])
  2.     .arrayChange(["push", "pop", "splice"], function(ev) {
  3.         alert("Array operation " + ev.change + " executed with args " + ev.arguments);
  4.     });
  5. // Array operation pop executed with args undefined
  6. $.pop(arr);
  7. // Array operation push executed with args 4,5
  8. $.push(arr, 4, 5);
  9. // nothing
  10. $.splice(arr, 0, 1);

arrayChanging

Exactly like the attrChanging event, but for arrays. Operation can be cancelled via the ev.preventDefault() method.

Linking Objects

When objects are linked, changes to one are automatically forwarded to another. For example, this allows you to very quickly and easily link fields of a form to an object. Any changes to the form fields are automatically pushed onto the object, saving you from writing code. Furthermore, built-in support for converters lets you modify the format or type of the value as it flows between objects (for example, formatting a phone number, or parsing a string to a number).

jQuery.fn.linkTo

Sets up a link that pushes changes to any of the source objects to a single target object.

  1. var person = {};
  2. $("#name").linkTo("val", person, "name");
  3. $("#name").val("foo");
  4. alert(person.name); // foo
  5. // ... user changes value ...
  6. alert(person.name); // user typed value

It also supports passing in a settings object instead of multiple parameters.

  1. var person = {};
  2. $("#name").linkTo({
  3.     sourceAttr: "val",
  4.     target: person,
  5.     targetAttr: "name"
  6. });
  7. $("#name").val("foo");
  8. alert(person.name); // foo
  9. // ... user changes value ...
  10. alert(person.name); // user typed value

jQuery.fn.linkFrom

Sets up a link that pulls changes from a source object to each of the target objects.

  1. var person = {};
  2. $(person).linkFrom("name", "#name", "val");
  3. $("#name").val("foo");
  4. alert(person.name); // foo
  5. // ... user changes value ...
  6. alert(person.name); // user typed value

jQuery.fn.linkBoth

Sets up two links, linking two objects to each other. A change in either object is pushed to the other. Equivalent to calling both linkTo and linkFrom.

  1. var person = {};
  2. $(person).linkBoth("name", "#name", "val");
  3. $("#name").val("foo");
  4. alert(person.name); // foo
  5. $(person).attr("name", "bob");
  6. alert($("#name").val()); // bob

jQuery.convertFn

Often times, it is necessary to modify the value as it flows from one side of a link to the other. For example, to convert null to “None”, to format or parse a date, or parse a string to a number. The link APIs support specifying a converter function, either as a name of a function defined on jQuery.convertFn, or as a function itself.

The plugin comes with one converter named "!" which negates the value, as an example implementation.

  1. var person = {};
  2. $.convertFn.round = function(value) {
  3.     return Math.round( Math.parseFloat( value ) );
  4. }
  5. $("#name").linkTo("val", person, "age", "round");
  6. $("#name").val("7.5");
  7. alert(person.age); // 8

Converter functions receive the value as the first parameter. They also receive a settings object which corresponds to the parameters given to the link API. This allows you to easily parameterize a converter.

  1. var person = {};
  2. $.convertFn.map = function(value, settings) {
  3.     return settings.map[ value ] || value;
  4. }
  5. $("#name").linkTo({
  6.     sourceAttr: "val",
  7.     target: person,
  8.     targetAttr: "favoriteColor",
  9.     converter: "map",
  10.     map: { red: "#FF0000", blue: "#0000FF" }
  11. });
  12. $("#name").val("red");
  13. alert(person.age); // #FF0000

We are very much looking forward to hearing your feedback!

- Dave Reed

EDIT: I added some samples in the wiki entry showing other ways you can use converters, such as inline instead of by name, or to set multiple fields.