This proposal describes how support for "data linking" can be added to the jQuery core library. 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').
In order to link a source to a target, it is necessary to be notified when a 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 facilitiate this, which are also useful on their own.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.
function report(ev) {
alert("Change attr '" + ev.attrName + "' from '" +
ev.oldValue + "' to '" + ev.newValue + "'.");
}
$("#el").attrChange(report)
// Change attr 'foo' from 'undefined' to 'bar'
.attr("foo", "bar").
// Change attr 'foo' from 'bar' to 'baz'
.attr("foo", "baz");
//restricted scope can be thought of as a filter
//only attributes changed exactly matching (===) will trigger the event
// restricted scope
$("#el").attrChange("x", report)
// Change attr 'x' from 'undefined' to '1'
// Note no event for 'y' because the scope of 'x' was passed in
.attr( { x: "1", y: "2" } );
$("#el").attrChange([ "x", "y" ], report)
// Change attr 'x' from 'undefined' to '1'
// Change attr 'y' from 'undefined' to '2'
// Note no event for 'z' because the scope of 'x' and 'y' was passed in
.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,
// works with data()
$("#el").attrChange("data:foo", report)
// Change attr 'data:foo' from 'undefined' to 'bar'
.data( "foo", "bar" )
// Change attr 'data:!' from '[Object object]' to '[Object object]'
.data( { } );
// works with val()
$("#el").attrChange("val", report)
// Change attr 'val' from 'hi' to 'bye'
.val( 'bye' );
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.
$("#el")
.attrChanging(function(ev) {
if (!confirm("Allow changing attr '" + ev.attrName + "' from '" +
ev.oldValue + "' to '" + ev.newValue + "'?")) {
ev.preventDefault();
}
});
// Allow changing attr 'foo' from 'undefined' to 'bar'?
// yes: value set, attrChange event raised
// no: value not set, attrChange event not raised
.attr("foo", "bar");
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.
var arr = [1,2,3];
$([arr])
.arrayChange(function(ev) {
alert("Array operation " + ev.change + " executed with args " + ev.arguments);
});
// Array operation push executed with args 4,5
$.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.
$([arr])
.arrayChange(["push", "pop", "splice"], function(ev) {
alert("Array operation " + ev.change + " executed with args " + ev.arguments);
});
// Array operation pop executed with args undefined
$.pop(arr);
// Array operation push executed with args 4,5
$.push(arr, 4, 5);
// nothing
$.splice(arr, 0, 1);
Exactly like the attrChanging event, but for arrays. Operation can be cancelled via the ev.preventDefault() method.
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 retrieval 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).
Sets up a link that pushes changes to any of the source objects to all target objects.
var person = {};
$.link({
source: "#name", sourceAttr: "val",
target: person, targetAttr: "name"
});
$("#name").val("foo");
alert(person.name); // foo
// ... user changes value ...
alert(person.name); //
The 'source' may be an object, DOM element, or string selector.
object
Changes to that object through a jQuery set wrapping it will trigger the link. e.g.:
$(obj).attr("foo", "bar");
DOM element or selector This sets up a link from all the matching elements (if it is a selector) to all matching targets. For example, if there are 3 inputs and 3 spans on the page, 9 links are created, one from each input to each span.
$.link({
source: "input",
target: "span"
});
Attributes and Microdata The 'sourceAttr' and 'targetAttr' fields are optional. If omitted, the attribute is determined automatically:
The source attribute is determined as follows: input, textarea, or select: "val" any other dom element: "text"
The target attribute is determined as follows: Source is a DOM element, and has 'itemprop' microdata attribute? Use the value. Otherwise, use source.name or source.id. If source is not a DOM element, use the same rules as source attribute.
This allows for simple links that target complex scenarios. For example, the following creates a link from all input elements inside #form1 to a single target object. The field set on the object is determined by first seeing if the input causing the event has an 'itemprop' attribute. If not, the input's name or id is used. In this example, the target's 'fullName' and 'birthday' fields would be set.
$.link({ source: "#form1 input", target: contact });
For example, the following sets up a link that activates whenever the val() of the input changes, and reacts by setting the text() of the span.
$.link( { source: "#input1", target: "#span1" } );
From/To Syntax
$.link supports creating multiple links with different rules at the same time. In this example, two form elements are mapped to two different fields of the same object.
$.link({
from: {
sources: "#input-first", "#input-last"
},
to: {
targets: contact,
attr: ["firstName", "lastName"]
}
});
Each value specified can be an array, or not. If not, the one value is applied to all cases. If an array, corresponding indexes of each array are used to create each link. Note that each source may also still match multiple elements if it is a selector. The full syntax is:
$.link({
from: {
sources: [source1, source2, ...] | sourceForAll,
attr: [attr1, attr2, ...] | attrForAll,
convert: [converter1, converter2, ...] | converterForAll,
update: true | false
},
to: {
targets: [target1, target2, ...] | targetForAll,
attr: [attr1, attr2, ...] | attrForAll,
convert: [converter1, converter2, ...] | converterForAll,
update: true | false
},
twoWay: true | false
});
twoWay
The twoWay option sets up links in both directions -- from source to target, and target to source. Changes in either will be reflected in the other. This is the reason for the 'convert' option on the 'to' settings -- those converters would be used when pushing changes from a target to a source (reverse).
Updating immediately
Sometimes it is desired that the target of a link reflect the source value immediately, even before the source is changed. You can tell link() to update the target immediately using the 'update' setting:
$.link({ source: source, target: target, update: true });
$.link({
from: {
sources: source
},
to: {
targets: target,
update: true
}
});
Note that this is particularly useful when relying on the automatic target attribute determination. You can quickly populate an object with a form's current values by relying on itemprop attributes or input name, and setting update to true to force an immediate update.
Note that if you put 'update' on the 'from' settings, the source is updated with the target value, even though the link usually flows from the source to the target. This allows you, for example, to setup a link from an input to an object, but have the input initially reflect the value already in the target.
Context
$.link in both direct and from/to forms allows a 2nd jQuery context parameter. This context is used if any selectors are given. For example, these are equivalent:
$.link({
from: {
sources: $(selector, context).get()
},
to: {
targets: target
}
});
$.link({
from: {
sources: selector
},
to: {
targets: target
}
}, context);
This removes a link previously established with
$.link( { source: ".foo", target: target, targetAttr: "field" } );
$.unlink( { source: "#foo1", target: target, targetAttr: "field" } );
Automatic unlinking
Links are cleaned up when its target or source is a DOM element that is being destroyed. For example, the following setups a link between an input and a span, then destroys the span by clearing it's parent html. The link is automatically removed.
$.link( { source: "#input1", target: "#span1" } );
$("#span1").parent().html("");
$.liveLink is a powerful tool that links multiple elements now or in the future. For example, to map all the input fields of a form to an object, even when form fields are dynamically added in the future:
$.liveLink({
from: {
source: "#form1 *"
},
to: {
targets: contact
}
});
Note however that currently you cannot use 'twoWay' on a live link. You may use 'update'.
Removes a live link previously created with $.linkLive. Syntax is the same as unlink. Note that unlike regular links, live links do not expand into all the possible sources and targets when they are created. This means you cannot 'unliveLink' a portion of a live link, you may only remove the entire live link.
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.
var person = {};
$.convertFn.round = function(value) {
return Math.round( Math.parseFloat( value ) );
}
$.link( { source: "#age", target: person, convert: "round" } );
$("#name").val("7.5");
alert(person.age); // 8
It is nice to reuse converters by naming them this way. But you may also specified the converter directly as a function.
var person = {};
$.link( { source: "#age", target: person, convert: function(value) {
return Math.round( Math.parseFloat( value ) );
} });
$("#name").val("7.5");
alert(person.age); // 8
Converter functions receive the value that came from the source as the first parameter. They also receive a settings object which corresponds to the parameters given to the link API (if the from/to syntax was used, the settings are expanded into the more granular source/target form). This allows you to easily parameterize a converter.
var person = {};
$.convertFn.map = function(value, settings) {
return settings.map[ value ] || value;
}
$.link( { source: "#color",
target: person, targetAttr: "favoriteColor", convert: "map",
map: { red: "#FF0000", blue: "#0000FF" } } );
$("#name").val("red");
alert(person.age); // #FF0000
The settings object also contains the source and target parameters. Say you wanted to link two different fields on the source to one field the target, as in combining the first and last name fields of an object onto a single "full name" span.
var person = { firstName: "Some", lastName: "User" };
$.convertFn.fullName = function(value, settings) {
return settings.source.firstName + " " + settings.source.lastName;
}
$.link( { source: person, sourceAttr: "firstName lastName",
target: "#fullname", targetAttr: "text", convert: "fullName" } );
alert($("#fullname").text()); // "Some User"
// update either field...
$(person).attr("firstName", "jQuery");
// and the target is updated
alert($("#fullname").text()); // "jQuery User"
- 5/26/2010 -- Completely revised the API based on forum feedback.
- 5/01/2010 -- Corrected comments about restricted scope -- event is suppressed, not the change.
- 5/01/2010 -- Fixed glitches in comments and added info about restricted scope.
- 4/29/2010 -- Expanded on converter samples.
- 4/28/2010 -- Initial proposal published