├── README.md
├── beta2
├── 1explicitOrDeclarative.html
├── 2hierarchical.html
├── 3collectionChange.html
├── 4tmplRender.html
├── jquery.datalink2.js
├── jquery.tmpl2.js
└── simple-explicit.html
├── demos
├── demo-contacts.css
├── demo-contacts.html
└── demo-contacts.js
└── jquery.datalink.js
/README.md:
--------------------------------------------------------------------------------
1 | # jQuery Data Link plugin v1.0.0pre.
2 |
3 | _jQuery Data Link is no longer in active development, and has been superseded by [JsViews](https://github.com/BorisMoore/jsviews), which integrates data linking with templated rendering, using [JsRender templates](https://github.com/BorisMoore/jsrender) (next-generation jQuery Templates).**
4 |
5 | **See [this blog post](http://www.borismoore.com/2011/10/jquery-templates-and-jsviews-roadmap.html) for more context on the roadmap for jQuery Templates, JsRender and JsViews...**
6 |
7 |
8 | _Note: To continue to use this plugin, see tagged versions for stable Beta releases. Requires jquery version 1.4.2._
9 |
10 | Documentation for the _jQuery Data Link_ plugin can be found on the jQuery documentation site:
11 | [http://api.jquery.com/category/plugins/data-link/] (http://api.jquery.com/category/plugins/data-link)
12 |
13 |
14 | ==================================== WARNING ====================================
15 | Breaking change:
16 | In jQuery 1.5, the behavior of $(plainObject).data() has been modified. In order to work against all versions of jQuery including jQuery 1.5,
17 | current builds of jquery-datalink have therefore been modified as follows:
18 |
19 | The API to modify field values is now .setField( name, value ), rather than .data( name, value ). (Examples below).
20 | The events associated with the modified field are now "setField" and "changeField", rather than "setData" and changeData".
21 |
22 | Note: This plugin currently depends on jQuery version 1.4.3.
23 | =================================================================================
24 |
25 |
26 |
27 |
28 | Introduction
29 |
30 | This is the official jQuery DataLink plugin. 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').
31 |
32 |
33 | jQuery(..).link() API
34 |
35 |
36 | The link API 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. By default, changes to the object are also automatically pushed back onto the corresponding form field, saving you from writing even more code. Furthermore, converters lets you modify the format or type of the value as it flows between the two sides (for example, formatting a phone number, or parsing a string to a number).
37 |
38 |
39 |
40 | <script>
41 | $().ready(function() {
42 | var person = {};
43 | $("form").link(person);
44 |
45 | $("[name=name]").val("NewValue"); // Set firstName to a value.
46 | alert(person.name); // NewValue
47 |
48 | $(person).setField("name", "NewValue");
49 | alert($("[name=name]").val()); // NewValue
50 |
51 | // ... user changes value ...
52 | $("form").change(function() {
53 | // <user typed value>
54 | alert(person.name);
55 | });
56 | });
57 | </script>
58 |
59 | <form name="person">
60 | <label for="name">Name:</label>
61 | <input type="text" name="name" id="name" />
62 | </form>
63 |
64 |
65 |
66 | The jQuery selector serves as a container for the link. Any change events received by that container are processed. So linking with $("form") for example would hookup all input elements. You may also target a specific element, such as with $("#name").link(..).
67 |
68 |
Customizing the Mapping
69 |
70 |
71 | It is not always that case that the field of an object and the name of a form input are the same. You might want the "first-name" input to set the obj.firstName field, for example. Or you may only want specific fields mapped rather than all inputs.
72 |
73 |
74 | var person = {};
75 | $("form").link(person, {
76 | firstName: "first-name",
77 | lastName: "last-name",
78 | });
79 |
80 |
81 | This links only the input with name "first-name" to obj.firstName, and the input with name "last-name" to obj.lastName.
82 |
83 |
84 |
85 | Converters and jQuery.convertFn
86 |
87 |
88 | 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.
89 |
90 |
91 | The plugin comes with one converter named "!" which negates the value.
92 |
93 |
94 |
95 | <script>
96 | $().ready(function() {
97 | var person = {};
98 |
99 | $.convertFn.round = function(value) {
100 | return Math.round( parseFloat( value ) );
101 | }
102 |
103 | $("#age").link(person, {
104 | age: {
105 | convert: "round"
106 | }
107 | });
108 |
109 | /* Once the user enters their age, the change event will fire which, in turn, will
110 | * cause the round function to be called. This will then round the age up or down,
111 | * set the rounded value on the object which will then cause the input field to be
112 | * updated with the new value.
113 | */
114 | $("#age").change(function() {
115 | alert(person.age);
116 | });
117 | });
118 | </script>
119 |
120 | <form name="person">
121 | <label for="age">Age:</label>
122 | <input type="text" name="age" id="age" />
123 | </form>
124 |
125 |
126 |
127 | It is convenient to reuse converters by naming them this way. But you may also specify the converter directly as a function.
128 |
129 |
130 |
131 | var person = {};
132 | $("#age").link(person, {
133 | age: {
134 | convert: function(value) {
135 | return Math.round( Math.parseFloat( value ) );
136 | }
137 | }
138 | });
139 |
140 | $("#name").val("7.5");
141 | alert(person.age); // 8
142 |
143 |
144 |
145 | Converter functions receive the value that came from the source, the source object, and the target object. If a converter does not return a value or it returns undefined, the update does not occur. This allows you to not only be able to convert the value as it is updated, but to customize how the value is assigned.
146 |
147 |
148 | var person = {};
149 | $("#age").link(person, {
150 | age: {
151 | convert: function(value, source, target) {
152 | var age = Math.round( Math.parseFloat( value ) );
153 | target.age = age;
154 | target.canVote = age >= 18;
155 | }
156 | }
157 | });
158 | $("#name").val("7.5");
159 | alert(person.age); // 8
160 | alert(person.canVote); // false
161 | $("#name").val("18");
162 | alert(person.canVote); // true
163 |
164 |
165 | In this example, the converter sets two fields on the target, and neglects to return a value to cancel the default operation of setting the age field.
166 |
167 |
168 | Converters can also be specified for the reverse process of updating the source from a change to the target. You can use this to customize the attribute used to represent the value, rather than the default of setting the 'value'.
169 |
170 |
171 | var product = { };
172 | $("#rank").link(product, {
173 | salesRank: {
174 | convertBack: function(value, source, target) {
175 | $(target).height(value * 2);
176 | }
177 | }
178 | });
179 | $(product).setField("salesRank", 12);
180 | alert($("#rank").height()); // 24
181 |
182 |
183 | This example links the height of the element with id "rank" to the salesRank field of the product object. When the salesRank changes, so does the height of the element. Note in this case there is no linking in the opposite direction. Changing the height of the rank element will not update the product.salesRank field.
184 |
185 |
186 |
187 | Updating immediately
188 |
189 | Sometimes it is desired that the target of a link reflect the source value immediately, even before the source is changed. Currently there is no built-in API for this, but you can force by triggering a change event.
190 |
191 |
192 |
193 | $(source)
194 | .link(target)
195 | .trigger("change");
196 |
197 | alert(target.input1); // value
198 |
199 | // or in reverse
200 | $(source)
201 | .link(target);
202 |
203 | $(target)
204 | .trigger("changeField");
205 |
206 | alert($("[name=age]").val()); // target.age
207 |
208 |
209 | jQuery(..).unlink() API
210 |
211 | This removes a link previously established with link.
212 |
213 |
214 |
215 | $(source)
216 | .link(target) // create link
217 | .unlink(target); // cancel link
218 |
219 |
220 | Automatic unlinking
221 |
222 |
223 | 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.
224 |
225 |
226 |
227 | $("#input1").link("#span1", {
228 | text: "input1"
229 | });
230 | $("#span1").parent().html("");
231 |
232 |
--------------------------------------------------------------------------------
/beta2/1explicitOrDeclarative.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | show data
10 |
11 | set name and color
12 | set city
13 |
14 |
15 | Declarative
16 |
17 |
33 |
34 |
35 | Explicit
36 |
37 |
38 |
39 | City:
40 | City:
41 |
42 |
43 |
44 |
45 |
46 |
47 |
130 |
131 |
132 |
133 |
138 |
139 |
140 | Console
141 |
142 |
143 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/beta2/2hierarchical.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | show data
10 |
11 | set name
12 |
13 |
14 |
57 |
58 |
85 |
86 |
87 |
88 |
91 |
92 |
93 | Console
94 |
95 |
96 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/beta2/3collectionChange.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 | show data
17 |
18 | set last name
19 | append
20 | delete last row
21 |
22 |
23 |
63 |
64 |
78 |
79 |
125 |
126 |
127 |
128 |
131 |
132 |
133 | Console
134 |
135 |
136 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/beta2/4tmplRender.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 | show data
17 |
18 | set last name
19 | append
20 | delete last row
21 |
22 |
23 | Editable
24 |
25 |
26 |
27 | Read-only
28 |
29 |
30 |
44 |
45 |
52 |
53 |
106 |
107 |
108 |
109 |
112 |
113 |
114 | Console
115 |
116 |
117 |
118 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/beta2/jquery.datalink2.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Data Link plugin 1.0.0pre
3 | *
4 | * BETA2 INVESTIGATION
5 | *
6 | * http://github.com/jquery/jquery-datalink
7 | *
8 | * Copyright Software Freedom Conservancy, Inc.
9 | * Dual licensed under the MIT or GPL Version 2 licenses.
10 | * http://jquery.org/license
11 | */
12 | ///
13 | ///
14 |
15 | (function($, undefined) {
16 |
17 | var linkSettings, decl,
18 | fnSetters = {
19 | value: "val",
20 | html: "html",
21 | text: "text"
22 | },
23 |
24 | linkAttr = "data-jq-linkto",
25 | bindAttr = "data-jq-linkfrom",
26 | pathAttr = "data-jq-path",
27 |
28 | unsupported = {
29 | "htmlhtml": 1,
30 | "arrayobject": 1,
31 | "objectarray": 1
32 | }
33 |
34 | getEventArgs = {
35 | pop: function( arr, args ) {
36 | if ( arr.length ) {
37 | return { change: "remove", oldIndex: arr.length - 1, oldItems: [ arr[arr.length - 1 ]]};
38 | }
39 | },
40 | push: function( arr, args ) {
41 | return { change: "add", newIndex: arr.length, newItems: [ args[ 0 ]]};
42 | },
43 | reverse: function( arr, args ) {
44 | if ( arr.length ) {
45 | return { change: "reset" };
46 | }
47 | },
48 | shift: function( arr, args ) {
49 | if ( arr.length ) {
50 | return { change: "remove", oldIndex: 0, oldItems: [ arr[ 0 ]]};
51 | }
52 | },
53 | sort: function( arr, args ) {
54 | if ( arr.length ) {
55 | return { change: "reset" };
56 | }
57 | },
58 | splice: function( arr, args ) {
59 | var index = args[ 0 ],
60 | numToRemove = args[ 1 ],
61 | elementsToRemove,
62 | elementsToAdd = args.slice( 2 );
63 | if ( numToRemove <= 0 ) {
64 | if ( elementsToAdd.length ) {
65 | return { change: "add", newIndex: index, newItems: elementsToAdd };
66 | }
67 | } else {
68 | elementsToRemove = arr.slice( index, index + numToRemove );
69 | if ( elementsToAdd.length ) {
70 | return { change: "move", oldIndex: index, oldItems: elementsToRemove, newIndex: index, newItems: elementsToAdd };
71 | } else {
72 | return { change: "remove", oldIndex: index, oldItems: elementsToRemove };
73 | }
74 | }
75 | },
76 | unshift: function( arr, args ) {
77 | return { change: "add", newIndex: 0, newItems: [ args[ 0 ]]};
78 | },
79 | move: function( arr, args ) {
80 | var fromIndex,
81 | numToMove = arguments[ 1 ];
82 | if ( numToMove > 0 ) {
83 | fromIndex = arguments[ 0 ];
84 | return { change: "move", oldIndex: fromIndex, oldItems: arr.splice( fromIndex, numToMove ), newIndex: arguments[ 2 ]};
85 | }
86 | }
87 | };
88 |
89 | function changeArray( array, eventArgs ) {
90 | var $array = $([ array ]);
91 |
92 | switch ( eventArgs.change ) {
93 | case "add":
94 | array.push( eventArgs.newItems[0] ); // Todo - use concat or iterate, for inserting multiple items
95 | break;
96 |
97 | case "remove":
98 | array.splice( eventArgs.oldIndex, eventArgs.oldItems.length );
99 | break;
100 |
101 | case "reset":
102 | break;
103 |
104 | case "move":
105 | array.splice( eventArgs.newIndex, 0, array.splice( eventArgs.oldIndex, eventArgs.number));
106 | break;
107 | }
108 | if ( eventArgs ) {
109 | $array.triggerHandler( "arrayChange!", eventArgs );
110 | }
111 | }
112 |
113 | function renderTmpl( el, data, tmpl ) {
114 | $( el ).html(
115 | $( tmpl ).tmplRender( data, { annotate: true })
116 | );
117 | // var viewInfo = $.data( el, "_jqDataLink" );
118 | }
119 |
120 | function addBinding( map, from, to, callback, links, addViewItems, viewItem ) {
121 |
122 | // ============================
123 | // Helpers for "toObject" links
124 |
125 | function setFields( sourceObj, basePath, cb ) {
126 | var field, isObject, sourceVal;
127 |
128 | if ( typeof sourceObj === "object" ) {
129 | for ( field in sourceObj ) {
130 | sourceVal = sourceObj[ field ];
131 | if ( sourceObj.hasOwnProperty(field) && !$.isFunction( sourceVal ) && !( typeof sourceVal === "object" && sourceVal.toJSON )) {
132 | setFields( sourceVal, (basePath ? basePath + "." : basePath) + field, cb );
133 | }
134 | }
135 | } else {
136 | cb( sourceObj, basePath, thisMap.convert, sourceObj )
137 | }
138 | }
139 |
140 | function convertAndSetField( val, path, cnvt, sourceObj ) {
141 | //path = isFromHtml ? getLinkedPath( sourceObj, path )[0] : path; // TODO do we need to pass in path?
142 | $.setField( toObj, path, cnvt
143 | ? cnvt( val, path, sourceObj, toObj, thisMap )
144 | : val
145 | );
146 | }
147 | // ============================
148 | // Helper for --- TODO clean up between different cases....
149 |
150 | var j, l, link, innerMap, isArray, path, item, items,
151 | thisMap = typeof map === "string" ? { to: thisMap } : map && $.extend( {}, map ),
152 | // Note: "string" corresponds to 'to'. Is this intuitive? It is useful for filtering object copy: $.link( person, otherPerson, ["lastName", "address.city"] );
153 | tmpl = thisMap.tmpl,
154 | fromPath = thisMap.from,
155 | fromObj = from[0],
156 | fromType = objectType( from ),
157 | toType = objectType( to ),
158 | isFromHtml = (fromType === "html"),
159 | toObj = to[0],
160 | eventType = isFromHtml ? "change" : fromType + "Change",
161 |
162 |
163 | // TODO Verify optimization for memory footprint in closure captured by handlers, and perf optimization for using function declaration rather than statement?
164 | handler = function( ev, eventArgs ) {
165 | var cancel, sourceValue, sourcePath,
166 | source = ev.target,
167 |
168 | fromHandler = {
169 | "html": function() {
170 | var setter, fromAttr, $source;
171 |
172 | fromAttr = thisMap.fromAttr;
173 | if ( !fromAttr ) {
174 | // Merge in the default attribute bindings for this source element
175 | fromAttr = linkSettings.merge[ source.nodeName.toLowerCase() ];
176 | fromAttr = fromAttr ? fromAttr.from.fromAttr : "text";
177 | }
178 | setter = fnSetters[ fromAttr ];
179 | $source = $( source );
180 | sourceValue = setter ? $source[setter]() : $source.attr( fromAttr );
181 | },
182 | "object": function() {
183 | // For objectChange events, eventArgs provides the path (name), and value of the changed field
184 | var mapFrom = thisMap.from || (toType !== "html") && thisMap.to;
185 | if ( eventArgs ) {
186 | sourceValue = eventArgs.value;
187 | sourcePath = eventArgs.path;
188 | if ( mapFrom && sourcePath !== mapFrom ) {
189 | sourceValue = undefined;
190 | }
191 | } else {
192 | // This is triggered by .trigger(), where source is an object. So no eventArgs passed.
193 | sourcePath = mapFrom || sourcePath;
194 | // TODO - verify for undefined source fields. Do we set target to ""? sourceValue = sourcePath ? getField( source, sourcePath ) : "";
195 | sourceValue = sourcePath && getField( source, sourcePath ) || linkAttr; // linkAttr used as a marker of trigger events
196 | }
197 | },
198 | "array": function() {
199 | // For objectChange events, eventArgs is a data structure of info on the array change
200 | sourceValue = eventArgs ? eventArgs.change : linkAttr; // linkAttr used as a marker of trigger events
201 | }
202 | },
203 |
204 | toHandler = {
205 | "html": function() {
206 | to.each( function( _, el ) {
207 | var setter, targetPath , matchLinkAttr,
208 | targetValue = sourceValue,
209 | $target = $( el );
210 |
211 | function setTarget( all, attr, toPath, convert, toPathWithConvert ) {
212 | attr = attr || thisMap.toAttr;
213 | toPath = toPath || toPathWithConvert;
214 | convert = window[ convert ] || thisMap.convert; // TODO support for named converters
215 |
216 | matchLinkAttr = (!sourcePath || sourcePath === toPath );
217 | if ( !eventArgs ) {
218 | // This is triggered by trigger(), and there is no thisMap.from specified,
219 | // so use the declarative specification on each target element to determine the sourcePath.
220 | // So need to get the field value and run converter
221 | targetValue = getField( source, toPath );
222 | }
223 | // If the eventArgs is specified, then this came from a real field change event (not ApplyLinks trigger)
224 | // so only modify target elements which have a corresponding target path.
225 | if ( targetValue !== undefined && matchLinkAttr) {
226 | if ( convert && $.isFunction( convert )) {
227 | targetValue = convert( targetValue, source, sourcePath, el, thisMap );
228 | }
229 | if ( !attr ) {
230 | // Merge in the default attribute bindings for this target element
231 | attr = linkSettings.merge[ el.nodeName.toLowerCase() ];
232 | attr = attr? attr.to.toAttr : "text";
233 | }
234 |
235 | if ( css = attr.indexOf( "css-" ) === 0 && attr.substr( 4 ) ) {
236 | if ( $target.css( css ) !== targetValue ) {
237 | $target.css( css, targetValue );
238 | }
239 | } else {
240 | setter = fnSetters[ attr ];
241 | if ( setter && $target[setter]() !== targetValue ) {
242 | $target[setter]( targetValue );
243 | } else if ( $target.attr( attr ) !== targetValue ){
244 | $target.attr( attr, targetValue );
245 | }
246 | }
247 | }
248 | }
249 |
250 | if ( fromType === "array" ) {
251 | if ( eventArgs ) {
252 | //htmlArrayOperation[ eventArgs.change ](); // TODO support incremental rendering for different operations
253 | renderTmpl( el, source, thisMap.tmpl );
254 | $.dataLink( source, el, thisMap.tmpl ).pushValues();
255 | }
256 | } else {
257 | // Find path using thisMap.from, or if not specified, use declarative specification
258 | // provided by decl.applyBindInfo, applied to target element
259 | targetPath = thisMap.from;
260 | if ( targetPath ) {
261 | setTarget( "", "", targetPath );
262 | } else {
263 | viewItem = viewItem || $.viewItem( el );
264 | if ( !viewItem || viewItem.data === source ) {
265 | decl.applyBindInfo( el, setTarget );
266 | }
267 | }
268 | }
269 | });
270 | },
271 | "object": function() {
272 |
273 | // Find toPath using thisMap.to, or if not specified, use declarative specification
274 | // provided by decl.applyLinkInfo, applied to source element
275 | var convert = thisMap.Convert,
276 | toPath = thisMap.to || !isFromHtml && sourcePath;
277 |
278 | if (toPath ) {
279 | convertAndSetField( sourceValue, toPath, thisMap.convert, source );
280 | } else if ( !isFromHtml ) {
281 | // This is triggered by trigger(), and there is no thisMap.from or thisMap.to specified.
282 | // This will set fields on existing objects or subobjects on the target, but will not create new subobjects, since
283 | // such objects are not linked so this would not trigger events on them. For deep copying without triggering events, use $.extend.
284 | setFields( source, "", convertAndSetField );
285 | } else { // from html. (Mapping from array to object not supported)
286 | decl.applyLinkInfo( source, function(all, path, declCnvt){
287 | // TODO support for named converters
288 | var cnvt = window[ declCnvt ] || convert;
289 |
290 | viewItem = $.viewItem( source );
291 |
292 | $.setField( viewItem.data, path, cnvt
293 | ? cnvt( sourceValue, path, source, viewItem.data, thisMap )
294 | : sourceValue
295 | );
296 | });
297 | }
298 | },
299 | "array": function() {
300 | // For arrayChange events, eventArgs is a data structure of info on the array change
301 | if ( fromType === "array" ) {
302 | if ( !eventArgs ) {
303 | var args = $.map( fromObj, function( obj ){
304 | return $.extend( true, {}, obj );
305 | });
306 | args.unshift( toObj.length );
307 | args.unshift( 0 );
308 | eventArgs = getEventArgs.splice( toObj, args );
309 | }
310 | changeArray( toObj, eventArgs );
311 | }
312 | }
313 | };
314 |
315 | fromHandler[ fromType ]();
316 |
317 | if ( !callback || !(cancel = callback.call( thisMap, ev, eventArgs, to, thisMap ) === false )) {
318 | if ( toObj && sourceValue !== undefined ) {
319 | toHandler[ toType ]();
320 | }
321 | }
322 | if ( cancel ) {
323 | ev.stopImmediatePropagation();
324 | }
325 | },
326 |
327 | link = {
328 | handler: handler,
329 | from: from,
330 | to: to,
331 | map: thisMap,
332 | type: eventType
333 | };
334 |
335 | if ( addViewItems && thisMap.decl ) {
336 | items = setViewItems( toObj, from[ 0 ], thisMap, callback, links );
337 | viewItem = items[0];
338 | for ( j=1, l = items.length; j < l; j++ ) {
339 | item = items[ j ];
340 | addBinding( thisMap, $( item.data ), $( item.bind ), callback, links, false, item );
341 | }
342 | //DO object bindings on returned content- since now bindings are added to items.
343 | }
344 |
345 | switch ( fromType + toType ) {
346 | case "htmlarray" :
347 | for ( j=0, l=toObj.length; j 1 ) {
383 | fromObj = fromObj[ fromPath.shift() ];
384 | if ( fromObj ) {
385 | innerMap = $.extend( { inner: 1 }, thisMap ) // 1 is special value for 'inner' maps on intermediate objects, to prevent trigger() calling handler.
386 | innerMap.from = fromPath.join(".");
387 | addBinding( innerMap, $( fromObj ), to, callback, links );
388 | }
389 | }
390 | }
391 | }
392 |
393 | // ============================
394 | // Helpers
395 |
396 | function setViewItems( root, object, map, callback, links ) {
397 | // If bound add to nodes. If new path create new item, if prev path add to prev item. (LATER WILL ADD TEXT SIBLINGS). If bound add to prev item bindings.
398 | // Walk all elems. Find bound elements. Look up parent chain.
399 |
400 | var nodes, bind, prevPath, prevNode, item, elems, i, l, index = 0;
401 | items = [];
402 |
403 | function addItem( el, path ) {
404 | var parent;
405 | bind = [];
406 | nodes = [];
407 | if ( path ) {
408 | index = 0;
409 | } else if ( path !== undefined ) {
410 | path = "" + index++;
411 | }
412 | unbindLinkedElem( el );
413 | item = {
414 | path: path,
415 | nodes: nodes,
416 | bind: bind
417 | };
418 |
419 | if ( path === undefined ) {
420 | item.data = object;
421 | } else {
422 | item.parent = parent = $.viewItem( el );
423 | item.data = getPathObject( parent.data, path );
424 | }
425 | items.push( item );
426 | item = $.data( el, "_jsViewItem", item );
427 | }
428 |
429 | function processElem( el ) {
430 | var node = prevNode = el,
431 | path = el.getAttribute( pathAttr ),
432 | binding = el.getAttribute( bindAttr );
433 | if ( el.parentNode === prevNode ) {
434 | index = -1;
435 | }
436 | if ( path !== null ) {
437 | addItem( el, path );
438 | nodes.push( node );
439 | while ( node = node.nextSibling ) {
440 | if ( node.nodeType === 1 ) {
441 | if ( node.getAttribute( pathAttr ) === null ) {
442 | $.data( node, "_jsViewItem", item );
443 | } else {
444 | break;
445 | }
446 | }
447 | nodes.push( node );
448 | }
449 | // TODO Later support specifying preceding text nodes
450 | }
451 | if ( binding ) {
452 | bind.push( el );
453 | }
454 | }
455 |
456 | addItem( root );
457 |
458 | // $.data( root, "_jqDataLink", { viewItems: items, });
459 |
460 | if ( map.decl ) {
461 | // Walk all elems. Find bound elements. Look up parent chain.
462 | elems = root.getElementsByTagName( "*" );
463 | for ( i = 0, l = elems.length; i < l; i++ ) {
464 | processElem( elems[ i ]);
465 | }
466 | } else {
467 | bind.push( root );
468 | }
469 | return items;
470 | }
471 |
472 | function objectType( object ) {
473 | object = object[0];
474 | return object
475 | ? object.nodeType
476 | ? "html"
477 | : $.isArray( object )
478 | ? "array"
479 | : "object"
480 | : "none";
481 | }
482 |
483 | function wrapObject( object ) {
484 | return object instanceof $ ? object : $.isArray( object ) ? $( [object] ) : $( object ); // Ensure that an array is wrapped as a single array object
485 | }
486 |
487 | function getField( object, path ) {
488 | if ( object && path ) {
489 | var leaf = getLeafObject( object, path );
490 | object = leaf[0];
491 | if ( object ) {
492 | return object[ leaf[1] ];
493 | }
494 | }
495 | }
496 |
497 | function getLinkedPath( el ) {
498 | var path, result = [];
499 | // TODO Do we need basePath? If so, result = basePath ? [ basePath ] : []'
500 | while ( !$.data( el, "_jqDataLink" )) {
501 | if ( path = el.getAttribute( "_jsViewItem" )) {
502 | result.unshift( path );
503 | }
504 | el = el.parentNode;
505 | }
506 | return [ result.join( "." ), el ];
507 | }
508 |
509 | function getLeafObject( object, path ) {
510 | if ( object && path ) {
511 | var parts = path.split(".");
512 |
513 | path = parts.pop();
514 | while ( object && parts.length ) {
515 | object = object[ parts.shift() ];
516 | }
517 | return [ object, path ];
518 | }
519 | return [];
520 | }
521 |
522 | function getPathObject( object, path ) {
523 | if ( object && path ) {
524 | var parts = path.split(".");
525 | while ( object && parts.length ) {
526 | object = object[ parts.shift() ];
527 | }
528 | return object;
529 | }
530 | }
531 |
532 | function unbindLinkedElem( el ) {
533 | var item = $.data( el, "_jsViewItem" );
534 | if ( item && item.handler ) {
535 | var isArray = $.isArray( item.data );
536 | if ( isArray ) {
537 | $( [item.data] ).unbind( "arrayChange", item.handler );
538 | } else {
539 | $( item.data ).unbind( "objectChange", item.handler );
540 | }
541 | }
542 | }
543 |
544 | // ============================
545 |
546 | var oldcleandata = $.cleanData;
547 |
548 | $.extend({
549 | cleanData: function( elems ) {
550 | for ( var j, i = 0, el; (el = elems[i]) != null; i++ ) {
551 | // remove any links with this element as the target
552 | unbindLinkedElem( el );
553 | }
554 | oldcleandata( elems );
555 | },
556 |
557 | dataLink: function( from, to, maps, callback ) {
558 | var args = $.makeArray( arguments ),
559 | l = args.length - 1;
560 |
561 | if ( !callback && $.isFunction( args[ l ])) {
562 | // Last argument is a callback.
563 | // So make it the fourth parameter (our named callback parameter)
564 | args[3] = args.pop();
565 | return $.dataLink.apply( $, args );
566 | }
567 |
568 | var i, map, links = [],
569 | linkset = { // TODO Consider exposing as prototype, for extension
570 | links: links,
571 | pushValues: function() {
572 | var link, i = 0, l = links.length;
573 | while ( l-- ) {
574 | link = links[ l ];
575 | if ( !link.map.inner ) { // inner: 1 - inner map for intermediate object.
576 | link.from.each( function(){
577 | link.handler({
578 | type: link.type,
579 | target: this
580 | });
581 | });
582 | }
583 | }
584 | return linkset;
585 | },
586 | render: function() {
587 | var topLink = linkset.links[0];
588 | topLink.to.each( function() {
589 | renderTmpl( this, topLink.from[0], topLink.map.tmpl );
590 | });
591 | return linkset;
592 | },
593 | unlink: function() {
594 | var link, l = links.length;
595 | while ( l-- ) {
596 | link = links[ l ];
597 | link.from.unbind( link.type, link.handler );
598 | }
599 | return linkset;
600 | }
601 | },
602 | from = wrapObject( from ),
603 | to = wrapObject( to ),
604 | targetElems = to,
605 | fromType = objectType( from ),
606 | toType = objectType( to ),
607 | tmpl = (toType === "html" && typeof maps === "string") && maps;
608 |
609 | if ( tmpl ) {
610 | maps = undefined;
611 | }
612 |
613 | maps = maps
614 | || !unsupported[ fromType + toType ]
615 | && {
616 | decl: true,
617 | tmpl: tmpl
618 | };
619 |
620 | if ( fromType === "html" && maps.decl ) {
621 | maps.from = "input[" + linkAttr + "]";
622 | }
623 |
624 | maps = $.isArray( maps ) ? maps : [ maps ];
625 |
626 | i = maps.length;
627 |
628 | while ( i-- ) {
629 | map = maps[ i ];
630 | if ( toType === "html" ) {
631 | path = map.to;
632 | if ( path ) {
633 | targetElems = to.find( path ).add( to.filter( path ));
634 | map.to = undefined;
635 | }
636 | targetElems.each( function(){
637 | addBinding( map, from, $( this ), callback, links, true );
638 | });
639 | } else {
640 | addBinding( map, from, to, callback, links );
641 | }
642 | }
643 | return linkset;
644 | },
645 |
646 | dataPush: function( from, to, maps, callback ) {
647 | // TODO - provide implementation
648 | },
649 | dataPull: function( from, to, maps, callback ) {
650 | // TODO - provide implementation
651 | },
652 | setField: function( object, path, value ) { // TODO add support for passing in object (map) with newValues to copy from.
653 | if ( path ) {
654 | var $object = $( object ),
655 | args = [{ path: path, value: value }],
656 | leaf = getLeafObject( object, path );
657 |
658 | object = leaf[0], path = leaf[1];
659 | if ( object && (object[ path ] !== value )) {
660 | // $object.triggerHandler( setFieldEvent + "!", args );
661 | object[ path ] = value;
662 | $object.triggerHandler( "objectChange!", args );
663 | }
664 | }
665 | },
666 | getField: function( object, path ) {
667 | return getField( object, path );
668 | },
669 | viewItem: function( el ) {
670 | var item, node = el;
671 | while ( (node = node.parentNode) && node.nodeType === 1 && !(item = $.data( node, "_jsViewItem" ))) {}
672 | return item;
673 | },
674 |
675 | // operations: pop push reverse shift sort splice unshift move
676 | changeArray: function( array, operation ) {
677 | var args = $.makeArray( arguments );
678 | args.splice( 0, 2 );
679 | return changeArray( array, getEventArgs[ operation ]( array, args ));
680 | },
681 |
682 | dataLinkSettings: {
683 | decl: {
684 | linkAttr: linkAttr,
685 | bindAttr: bindAttr,
686 | pathAttr: pathAttr,
687 | applyLinkInfo: function( el, setTarget ){
688 | var linkInfo = el.getAttribute( decl.linkAttr );
689 | if ( linkInfo !== null ) {
690 | // toPath: convert end
691 | linkInfo.replace( /([\w\.]*)(?:\:\s*(\w+)\(\)\s*)?$/, setTarget );
692 | }
693 | //lastName:convert1()
694 | // Alternative using name attribute:
695 | // return el.getAttribute( decl.linkAttr ) || (el.name && el.name.replace( /\[(\w+)\]/g, function( all, word ) {
696 | // return "." + word;
697 | // }));
698 | },
699 | applyBindInfo: function( el, setTarget ){
700 | var bindInfo = el.getAttribute( decl.bindAttr );
701 | if ( bindInfo !== null ) {
702 | // toAttr: toPath convert( toPath ) end
703 | bindInfo.replace( /(?:([\w\-]+)\:\s*)?(?:(?:([\w\.]+)|(\w+)\(\s*([\w\.]+)\s*\))(?:$|,))/g, setTarget );
704 | }
705 | }
706 | },
707 | merge: {
708 | input: {
709 | from: {
710 | fromAttr: "value"
711 | },
712 | to: {
713 | toAttr: "value"
714 | }
715 | }
716 | }
717 | }
718 | });
719 |
720 | linkSettings = $.dataLinkSettings;
721 | decl = linkSettings.decl;
722 |
723 | })( jQuery );
724 |
--------------------------------------------------------------------------------
/beta2/jquery.tmpl2.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Templates Plugin 1.0.0pre
3 | * http://github.com/jquery/jquery-tmpl
4 | * Requires jQuery 1.4.2
5 | *
6 | * Copyright Software Freedom Conservancy, Inc.
7 | * Dual licensed under the MIT or GPL Version 2 licenses.
8 | * http://jquery.org/license
9 | */
10 |
11 |
12 | // TODO REMOVE clt, newData, itemKeyOffset? Merge recent fixes on jQuery.tmpl.js, such as __, etc.
13 |
14 |
15 | (function( $, undefined ){
16 | var oldManip = $.fn.domManip, tmplItmAtt = "data-jq-item", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
17 | newTmplItems = {}, ctl, newData, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} },
18 | itemKey = 0, itemKeyOffset = 0, cloneIndex = 0, stack = [],
19 | getLinkAttr = $.dataLinkSettings.decl.applyBindInfo;
20 |
21 | function newTmplItem( options, parentItem, fn, data ) {
22 | // Returns a template item data structure for a new rendered instance of a template (a 'template item').
23 | // The content field is a hierarchical array of strings and nested items (to be
24 | // removed and replaced by nodes field of dom elements, once inserted in DOM).
25 | // Prototype is $.tmpl.item, which provides both methods and fields.
26 | var newItem = this;
27 | newItem.parent = parentItem || null;
28 | newItem.data = data || (parentItem ? parentItem.data : {});
29 | newItem._wrap = parentItem ? parentItem._wrap : null;
30 | //if ( options ) {
31 | $.extend( newItem, options, { nodes: [], parent: parentItem } );
32 | //}
33 | if ( fn ) {
34 | // Build the hierarchical content to be used during insertion into DOM
35 | newItem.tmpl = fn;
36 | newItem._ctnt = newItem._ctnt || newItem.tmpl( $, newItem );
37 | newItem.key = ++itemKey;
38 | // Keep track of new template item, until it is stored as jQuery Data on DOM element
39 | (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
40 | }
41 | }
42 |
43 | function newTmplItem2( options, parentItem, fn, data, index ) {
44 | // Returns a template item data structure for a new rendered instance of a template (a 'template item').
45 | // The content field is a hierarchical array of strings and nested items (to be
46 | // removed and replaced by nodes field of dom elements, once inserted in DOM).
47 | // Prototype is $.tmpl.item, which provides both methods and fields.
48 | var newItem = this;
49 | newItem.parent = parentItem || null;
50 | newItem.data = data || (parentItem ? parentItem.data : {});
51 | newItem._wrap = parentItem ? parentItem._wrap : null;
52 | //if ( options ) {
53 | $.extend( newItem, options, { nodes: [], parent: parentItem } );
54 | //}
55 | if ( fn ) {
56 | // Build the hierarchical content to be used during insertion into DOM
57 | newItem.tmpl = fn;
58 | newItem.index = index || 0;
59 | newItem._ctnt = newItem._ctnt || newItem.tmpl( $, newItem );
60 | newItem.key = ++itemKey;
61 | // Keep track of new template item, until it is stored as jQuery Data on DOM element
62 | (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
63 | }
64 | }
65 |
66 | // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
67 | $.each({
68 | appendTo: "append",
69 | prependTo: "prepend",
70 | insertBefore: "before",
71 | insertAfter: "after",
72 | replaceAll: "replaceWith"
73 | }, function( name, original ) {
74 | $.fn[ name ] = function( selector ) {
75 | var ret = [], insert = $( selector ), elems, i, l, tmplItems,
76 | parent = this.length === 1 && this[0].parentNode;
77 |
78 | appendToTmplItems = newTmplItems || {};
79 | if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
80 | insert[ original ]( this[0] );
81 | ret = this;
82 | storeTmplItems( [this[0]] ); // SHOULD NOT DO THIS FOR REGULAR DOM MANIP
83 | } else {
84 | var insertedElems = [];
85 | for ( i = 0, l = insert.length; i < l; i++ ) {
86 | insertedElems.push( elems = (i > 0 ? this.clone(true) : this).get() );
87 | $( insert[i] )[ original ]( elems );
88 | ret = ret.concat( elems );
89 | }
90 | while ( insertedElems.length ) {
91 | storeTmplItems( insertedElems.shift() );
92 | cloneIndex++;
93 | }
94 | cloneIndex = 0;
95 | ret = this.pushStack( ret, name, insert.selector );
96 | }
97 | tmplItems = appendToTmplItems;
98 | appendToTmplItems = null;
99 | $.tmpl.complete( tmplItems, ctl, newData );
100 | ctl = null;
101 | newData = null;
102 | return ret;
103 | };
104 | });
105 |
106 | $.fn.extend({
107 | // Use first wrapped element as template markup.
108 | // Return wrapped set of template items, obtained by rendering template against data.
109 | tmplRender: function( data, options, parentItem ) {
110 | return $.tmplRender( this[0], data, options, parentItem );
111 | },
112 |
113 | tmpl: function( data, options, parentItem ) {
114 | return $.tmpl( this[0], data, options, parentItem );
115 | },
116 | // Find which rendered template item the first wrapped DOM element belongs to
117 | tmplItem: function() {
118 | return $.tmplItem( this[0] );
119 | },
120 |
121 | tmplActivate: function( tmpl, data, options, parentItem ) {
122 | var test = renderTmplItems( $.template( tmpl ), data, options, parentItem );
123 | // TODO Optimize between rendering string and activating, so as not to build the template items twice
124 | storeTmplItems( this );
125 | if ( $.dataLink ) {
126 | var self = this;
127 | $.dataLink( data, this, function( ev, eventArgs, to, thisMap ) {
128 | switch ( eventArgs.change ) {
129 | case "add":
130 | // self.append( $( tmpl ).tmplRender( eventArgs.newItems, { annotate: true } ));
131 | $( tmpl ).tmpl( eventArgs.newItems ).appendTo( self );
132 | break;
133 | }
134 |
135 | });
136 |
137 | $.dataLink( this, data );
138 | }
139 | },
140 |
141 | // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
142 | template: function( name, options ) {
143 | return $.template( name, this[0], options );
144 | },
145 |
146 | domManip: function( args, table, callback, options ) {
147 | if ( args[0] && $.isArray( args[0] )) {
148 | var dmArgs = $.makeArray( arguments ), elems = args[0], elemsLength = elems.length, i = 0, tmplItem;
149 | while ( i < elemsLength && !(tmplItem = $.data( elems[i++], "tmplItem" ))) {}
150 | if ( tmplItem && cloneIndex ) {
151 | dmArgs[2] = function( fragClone ) {
152 | // Handler called by oldManip when rendered template has been inserted into DOM.
153 | $.tmpl.afterManip( this, fragClone, callback );
154 | };
155 | }
156 | oldManip.apply( this, dmArgs );
157 | } else {
158 | oldManip.apply( this, arguments );
159 | }
160 | cloneIndex = 0;
161 | if ( ctl && !appendToTmplItems ) { // Bug in current code
162 | $.tmpl.complete( newTmplItems, ctl, newData );
163 | }
164 | return this;
165 | }
166 | });
167 |
168 | $.extend({
169 | // Return string obtained by rendering template against data.
170 | tmplRender: function( tmpl, data, options ) {
171 | var ret = renderTemplate( tmpl, data, options );
172 | itemKey = 0;
173 | newTmplItems = {}; // THIS MAY BE THE CAUSE OF THE ISSUE ON MEMORY LEAK - THAT THIS IS NOT SET TO {} IN THE CASE OF tmpl(), SINCE THAT HAPPENS ON APPEND (BY DESIGN). BUT WHAT ABOUT APPEND USING TMPLPLUS?
174 | ctl = newData = null;
175 | return ret;
176 | },
177 |
178 | // Return jQuery object wrapping the result of rendering the template against data.
179 | // For nested template, return tree of rendered template items.
180 | tmpl: function ( tmpl, data, options, parentItem ) {
181 | options = options || {};
182 | options.annotate = options.activate || parentItem && parentItem.annotate;
183 | var content = renderTemplate( tmpl, data, options, parentItem );
184 | return (parentItem && tmpl) ? content : jqObjectWithTextNodes( content );
185 | },
186 |
187 | // tmpl: function ( tmpl, data, options, parentItem ) {
188 | // options = options || {};
189 | // options.annotate = options.renderOnly ? false : (parentItem ? parentItem.annotate : true);
190 | // var content = tmplRenders( tmpl, data, options, parentItem );
191 | // // return parentItem ? content : $( options.annotate ? activate( content ) : content );
192 | // return parentItem ? content : jQuery( activate( content.join("") ));
193 | // },
194 |
195 | // Return rendered template item for an element.
196 | tmplItem: function( elem ) {
197 | var tmplItem;
198 | if ( elem instanceof $ ) {
199 | elem = elem[0];
200 | }
201 | while ( elem && elem.nodeType === 1 && !(tmplItem = $.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}
202 | return tmplItem || topTmplItem;
203 | },
204 |
205 | // Set:
206 | // Use $.template( name, tmpl ) to cache a named template,
207 | // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
208 | // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
209 |
210 | // Get:
211 | // Use $.template( name ) to access a cached template.
212 | // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
213 | // will return the compiled template, without adding a name reference.
214 | // If templateString includes at least one HTML tag, $.template( templateString ) is equivalent
215 | // to $.template( null, templateString )
216 | template: function( name, tmpl ) {
217 | if (tmpl) {
218 | // Compile template and associate with name
219 | if ( typeof tmpl === "string" ) {
220 | // This is an HTML string being passed directly in.
221 | tmpl = buildTmplFn( tmpl )
222 | } else if ( tmpl instanceof $ ) {
223 | tmpl = tmpl[0] || null; // WAS || {};
224 | }
225 | if ( tmpl && tmpl.nodeType ) {
226 | // If this is a template block, use cached copy, or generate tmpl function and cache.
227 | tmpl = $.data( tmpl, "tmpl" ) || $.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));
228 | }
229 | return typeof name === "string" ? ($.template[name] = tmpl) : tmpl;
230 | }
231 | // Return named compiled template
232 | return name ? (typeof name !== "string" ? $.template( null, name ):
233 | ($.template[name] ||
234 | // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
235 | $.template( null, htmlExpr.test( name ) ? name : $( name )))) : null;
236 | },
237 |
238 | encode: function( text ) {
239 | // Do HTML encoding replacing < > & and ' and " by corresponding entities.
240 | return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'");
241 | }
242 | });
243 |
244 | var defaultOpen = "$item.calls($item,_,$1,$2);_=[];",
245 | defaultClose = ["call=$item.calls();_=call[1].concat($item.", "(call,_));"];
246 |
247 | $.extend( $.tmpl, {
248 | tag: {
249 | "tmpl": {
250 | _default: { $2: "null" },
251 | open: "if($notnull_1){_=_.concat($item.nest($1,$2));}"
252 | // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
253 | // This means that {{tmpl foo}} treats foo as a template (which IS a function).
254 | // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
255 | },
256 | "wrap": {
257 | _default: { $2: "null" },
258 | open: defaultOpen,
259 | close: defaultClose.join( "wrap" )
260 | },
261 | "plugin": {
262 | _default: { $2: "null" },
263 | open: defaultOpen,
264 | close: defaultClose.join( "plugin" )
265 | },
266 | "link": { // Include an encode option?
267 | _default: { $2: "$data" },
268 | open: defaultOpen,
269 | close: defaultClose.join( "link" )
270 | },
271 | "bind": {
272 | _default: { $2: "$data" },
273 | open: "_.push($item.bind($item,_,$1,$2));"
274 | },
275 | "each": {
276 | _default: { $2: "$index, $value" },
277 | open: "if($notnull_1){$.each($1a,function($2){with(this){",
278 | close: "}});}"
279 | },
280 | "if": {
281 | open: "if(($notnull_1) && $1a){",
282 | close: "}"
283 | },
284 | "else": {
285 | _default: { $1: "true" },
286 | open: "}else if(($notnull_1) && $1a){"
287 | },
288 | "html": {
289 | // Unencoded expression evaluation.
290 | open: "if($notnull_1){_.push($1a);}"
291 | },
292 | ":": {
293 | // Code execution
294 | open: "$1"
295 | },
296 | "=": {
297 | // Encoded expression evaluation. Abbreviated form is ${}.
298 | _default: { $1: "$data" },
299 | open: "if($notnull_1){_.push($.encode($1a));}"
300 | },
301 | "!": {
302 | // Comment tag. Skipped by parser
303 | open: ""
304 | }
305 | },
306 |
307 | item: {
308 | tmpl: null,
309 | nodes: [],
310 | calls: function( content ) {
311 | if ( !content ) {
312 | return stack.pop();
313 | }
314 | stack.push( arguments );
315 | },
316 | nest2: function( tmpl, data, options ) {
317 | // nested template, using {{tmpl}} tag
318 | return $.tmpl( $.template( tmpl ), data, options, this );
319 | },
320 | nest: function( tmpl, data, options ) {
321 | // nested template, using {{tmpl}} tag
322 | return $.tmpl( $.template( tmpl ), data, options, this );
323 | },
324 | wrap: function( call, wrapped ) {
325 | // nested template, using {{wrap}} tag
326 | var options = call[4] || {};
327 | options.wrapped = wrapped;
328 | // Apply the template, which may incorporate wrapped content,
329 | return $.tmpl( $.template( call[2] ), call[3], options, call[0] ); // tmpl, data, options, item
330 | },
331 | html: function( filter, textOnly ) {
332 | var wrapped = this._wrap;
333 | return $.map(
334 | $( $.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ),
335 | function(e) {
336 | return textOnly ?
337 | e.innerText || e.textContent :
338 | e.outerHTML || outerHtml(e);
339 | });
340 | },
341 | update: function( options ) { // ISSUE - does not work correctly with top-level text nodes, or or similar
342 | var coll = this.nodes; // These are only the elements, not all nodes!!
343 | this.annotate = true;
344 | $.tmpl( null, null, options, this ).insertBefore( coll[0] ); // What if there is a text node in the rendered template, before the first node.
345 | $( coll ).remove(); // what if there are text nodes between the elements.
346 | },
347 | pluginOLD: function( call, wrapped ) {
348 | var pluginName = call[2];
349 | return $.tmpl( pluginsWrapperTmpl, null, {wrapped: wrapped, addIds: true, elementCreated: function( element ) {
350 | var tmplItem = this;
351 | if ( $.fn[ pluginName ] ) {
352 | loadPlugin();
353 | } else {
354 | $.req[ pluginName ]( loadPlugin );
355 | }
356 | function loadPlugin() {
357 | var self = $( element );
358 | self[ pluginName ].apply( self, $.makeArray( call ).slice(3) );
359 | }
360 | }}, call[0] );
361 | },
362 | plugin: function( call, wrapped ) {
363 | var pluginName = call[2];
364 | return $.tmpl( pluginsWrapperTmpl, null, {wrapped: wrapped, addIds: true, elementCreated: function( element ) {
365 | var tmplItem = this;
366 | if ( $.fn[ pluginName ] ) {
367 | loadPlugin();
368 | } else {
369 | $.req[ pluginName ]( loadPlugin );
370 | }
371 | function loadPlugin() {
372 | var self = $( element );
373 | self[ pluginName ].apply( self, $.makeArray( call ).slice(3) );
374 | if ( call[2] ) {
375 | window[call[2]] = self.data( pluginName ); // TODO Replace by $.tmpl.ctls or similar. TODO deal with async templates in plugin
376 | }
377 | }
378 | }}, call[0] );
379 | },
380 | link: function( call, wrapped ) {
381 | var pluginCall = $.makeArray( call );
382 | pluginCall.splice( 2, 2, "link", pluginCall[3], pluginCall[2] );
383 | return this.plugin( pluginCall, wrapped );
384 | },
385 | bind: function( item, content, map, from ) {
386 | return from[map];
387 | }
388 | },
389 |
390 | // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
391 | complete: function( tmplItems, ctl, data ) {
392 | var tmplItem;
393 | for ( tmplItem in tmplItems ) {
394 | tmplItem = tmplItems[ tmplItem ];
395 | // Raise rendered event
396 | if ( tmplItem.ctl && tmplItem.ctl.onItemRendered ) {
397 | tmplItem.ctl.onItemRendered( tmplItem );
398 | }
399 | }
400 | if ( ctl && ctl.onItemsRendered ) { //BUG ctl &&
401 | ctl.onItemsRendered( tmplItems, data );
402 | }
403 | itemKey = 0;
404 | newTmplItems = {};
405 | ctl = newData = null;
406 | },
407 |
408 | // Call this from code which overrides domManip, or equivalent
409 | // Manage cloning/storing template items etc.
410 | afterManip: function afterManip( elem, fragClone, callback ) {
411 | // Provides cloned fragment ready for fixup prior to and after insertion into DOM
412 | var content = fragClone.nodeType === 11 ?
413 | $.makeArray(fragClone.childNodes) :
414 | fragClone.nodeType === 1 ? [fragClone] : [];
415 |
416 | // Return fragment to original caller (e.g. append) for DOM insertion
417 | callback.call( elem, fragClone );
418 |
419 | // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
420 | storeTmplItems( content );
421 | cloneIndex++;
422 | }
423 | });
424 |
425 | var pluginsWrapperTmpl = $.template( null, "{{html this.html()}}" );
426 |
427 | newTmplItem.prototype = $.tmpl.item;
428 | newTmplItem2.prototype = $.tmpl.item;
429 |
430 | //========================== Private helper functions, used by code above ==========================
431 |
432 | function renderTemplate( tmpl, data, options, parentItem ) {
433 | var ret = renderTmplItems2( tmpl, data, options, parentItem );
434 | return (parentItem && tmpl) ? ret : buildStringArray( parentItem || topTmplItem, ret ).join("");
435 | }
436 |
437 | function renderTmplItems2( tmpl, data, options, parentItem ) {
438 | // Render template against data as a tree of template items (nested template), or as a string (top-level template).
439 | options = options || {};
440 | var ret, topLevel = !parentItem;
441 | if ( topLevel ) {
442 | // This is a top-level tmpl call (not from a nested template using {{tmpl}})
443 | parentItem = topTmplItem;
444 | if ( !$.isFunction( tmpl ) ) {
445 | tmpl = $.template[tmpl] || $.template( null, tmpl );
446 | }
447 | wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
448 | } else if ( !tmpl ) {
449 | // The template item is already associated with DOM - this is a refresh.
450 | // Re-evaluate rendered template for the parentItem
451 | tmpl = parentItem.tmpl;
452 | newTmplItems[parentItem.key] = parentItem;
453 | parentItem.nodes = [];
454 | if ( parentItem.wrapped ) {
455 | updateWrapped( parentItem, parentItem.wrapped );
456 | }
457 | // Rebuild, without creating a new template item
458 | return parentItem.tmpl( $, parentItem );
459 | }
460 | if ( !tmpl ) {
461 | return null; // Could throw...
462 | }
463 | // options = $.extend( {}, options, tmpl )
464 | if ( typeof data === "function" ) {
465 | data = data.call( parentItem || {} );
466 | }
467 | if ( options.wrapped ) {
468 | updateWrapped( options, options.wrapped );
469 | if ( options.addIds ) {
470 | // TEMPORARY?
471 | tmpl = $.template( null, options._wrap );
472 | options._wrap = null;
473 | options.wrapped = null;
474 | delete options.addIds;
475 | }
476 | }
477 | ctl = options.ctl;
478 | newData = data;
479 |
480 | return $.isArray( data ) ?
481 | $.map( data, function( dataItem, index ) {
482 | return dataItem ? new newTmplItem2( options, parentItem, tmpl, dataItem, index ) : null;
483 | }) :
484 | [ new newTmplItem2( options, parentItem, tmpl, data ) ];
485 | }
486 |
487 | function renderTmplItems( tmpl, data, options, parentItem ) {
488 | // Render template against data as a tree of template items (nested template), or as a string (top-level template).
489 | options = options || {};
490 | var ret, topLevel = !parentItem;
491 | if ( topLevel ) {
492 | // This is a top-level tmpl call (not from a nested template using {{tmpl}})
493 | parentItem = topTmplItem;
494 | if ( !$.isFunction( tmpl ) ) {
495 | tmpl = $.template[tmpl] || $.template( null, tmpl );
496 | }
497 | wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
498 | } else if ( !tmpl ) {
499 | // The template item is already associated with DOM - this is a refresh.
500 | // Re-evaluate rendered template for the parentItem
501 | tmpl = parentItem.tmpl;
502 | newTmplItems[parentItem.key] = parentItem;
503 | parentItem.nodes = [];
504 | if ( parentItem.wrapped ) {
505 | updateWrapped( parentItem, parentItem.wrapped );
506 | }
507 | // Rebuild, without creating a new template item
508 | return parentItem.tmpl( $, parentItem );
509 | }
510 | if ( !tmpl ) {
511 | return null; // Could throw...
512 | }
513 | // options = $.extend( {}, options, tmpl )
514 | if ( typeof data === "function" ) {
515 | data = data.call( parentItem || {} );
516 | }
517 | if ( options.wrapped ) {
518 | updateWrapped( options, options.wrapped );
519 | if ( options.addIds ) {
520 | // TEMPORARY?
521 | tmpl = $.template( null, options._wrap );
522 | options._wrap = null;
523 | options.wrapped = null;
524 | delete options.addIds;
525 | }
526 | }
527 | ctl = options.ctl;
528 | newData = data;
529 |
530 | return $.isArray( data ) ?
531 | $.map( data, function( dataItem ) {
532 | return dataItem ? new newTmplItem( options, parentItem, tmpl, dataItem ) : null;
533 | }) :
534 | [ new newTmplItem( options, parentItem, tmpl, data ) ];
535 | }
536 |
537 | function getTmplItemPath( tmplItem ) {
538 | // var path = tmplItem.index;
539 | // while ( tmplItem.parent.key ) {
540 | // tmplItem = tmplItem.parent;
541 | // path = tmplItem.key + "." + path;
542 | // }
543 | // return path;
544 | return "";
545 | }
546 |
547 | function buildStringArray( tmplItem, content ) {
548 | // Convert hierarchical content (tree of nested tmplItems) into flat string array of rendered content (optionally with attribute annotations for tmplItems)
549 | return content ?
550 | $.map( content, function( item ) {
551 | return (typeof item === "string") ?
552 | // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
553 | // ( tmplItem.annotate && tmplItem.key ? item.replace( /(<\w+)(?!\sdata-jq-item)([^>]*)/, "$1 " + tmplItmAtt + "=\"" + (tmplItem.key - itemKeyOffset) + "\" " + "data-jq-path" + "=\"" + getTmplItemPath( tmplItem ) + "\" $2" ) : item) :
554 | ( tmplItem.annotate && tmplItem.key ? item.replace( /(<\w+)(?!\sdata-jq-item)([^>]*)/, "$1 " + "data-jq-path" + "=\"" + getTmplItemPath( tmplItem ) + "\" $2" ) : item) :
555 | // This is a child template item. Build nested template.
556 | buildStringArray( item, item._ctnt );
557 | }) :
558 | // If content is not defined, return tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
559 | tmplItem;
560 | }
561 |
562 | function jqObjectWithTextNodes( content ) {
563 | // take string content and create jQuery wrapper, including initial or final text nodes
564 | // Also support HTML entities within the HTML markup.
565 | var ret;
566 | content.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) {
567 | ret = $( middle );
568 | if ( before || after ) {
569 | ret = ret.get();
570 | if ( before ) {
571 | ret = unencode( before ).concat( ret );
572 | }
573 | if ( after ) {
574 | ret = ret.concat(unencode( after ));
575 | }
576 | ret = $( ret );
577 | }
578 | });
579 | return ret || $();
580 | }
581 |
582 | function unencode( text ) {
583 | // Use createElement, since createTextNode will not render HTML entities correctly
584 | var el = document.createElement( "div" );
585 | el.innerHTML = text || "";
586 | return $.makeArray(el.childNodes);
587 | }
588 |
589 | // Generate a reusable function that will serve to render a template against data
590 | function buildTmplFn( markup ) {
591 | var o="{", c="}"
592 | markup.replace(/{jquery-tmpl-chars(.)(.)}/, function( all, open, close ){
593 | o=open;
594 | c=close;
595 | return "";
596 | })
597 | var regExShortCut = /\$\{([^\}]*)\}/g, // new RegExp("\$\{([^\}]*)\}", "g");
598 | // regExBind = /<[^<]*\{\{bind\s*([^<]*)\s*\}\}[^<]*>/g;
599 | regExBind = /<[^<]*\{\{bind[^<]*>/g;
600 |
601 | markup = markup.replace( regExBind, function(all) {
602 | return "{{link}}" + all.replace(/\{\{bind\s*([^\}(?=\})]*)\s*\}\}/g, "{bind$1/bind}") + "{{/link}}";
603 | });
604 |
605 |
606 | return new Function("jQuery","$item",
607 | "var $=jQuery,call,_=[],$data=$item.data,$ctl=$item.ctl;" +
608 |
609 | // Introduce the data as local variables using with(){}
610 | "with($data){_.push('" +
611 |
612 | // Convert the template into pure JavaScript
613 | $.trim(markup)
614 | .replace( /([\\'])/g, "\\$1" )
615 | .replace( /[\r\t\n]/g, " " )
616 | .replace( regExShortCut, "{{= $1}}" )
617 | .replace( regExBind, function(all) {
618 | return "before" + all.replace(/\{\{bind\s*([^<]*)\s*\}\}/g, "xxx$1yyy") + "after";
619 | })
620 | .replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*)\))?\s*\}\}/g,
621 | function( all, slash, type, fnargs, target, parens, args ) {
622 | var tag = $.tmpl.tag[ type ], def, expr, exprAutoFnDetect;
623 | if ( !tag ) { // Unregistered template tags are treated as generic plugins
624 | tag = $.tmpl.tag[ type ] = {
625 | _default: { $2: "null" },
626 | open: $.tmpl.tag.plugin.open,
627 | // open: defaultOpen, // This was for pluginOLD
628 | close: defaultClose.join( type ) // Will call the method on the item
629 | };
630 | $.tmpl.item[ type ] = $.tmpl.item[ type ] || function( call, wrapped ) { // Define the method on the item, called on parsing close tag.
631 | var pluginCall = $.makeArray( call );
632 | pluginCall.splice( 2, 0, type );
633 | return this.plugin( pluginCall, wrapped );
634 | };
635 | }
636 | def = tag._default || [];
637 | if ( parens && !/\w$/.test(target)) {
638 | target += parens;
639 | parens = "";
640 | }
641 | if ( target ) {
642 | target = unescape( target );
643 | args = args ? ("," + unescape( args ) + ")") : ( parens ? ")" : "");
644 | // Support for target being things like a.toLowerCase();
645 | // In that case don't call with template item as 'this' pointer. Just evaluate...
646 | expr = parens ? (target.indexOf(".") > -1 ? target + unescape( parens ) : ("(" + target + ").call($item" + args)) : target;
647 | exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
648 | } else {
649 | exprAutoFnDetect = expr = def.$1 || "null";
650 | }
651 | fnargs = unescape( fnargs );
652 | var test = "');" +
653 | tag[ slash ? "close" : "open" ]
654 | .split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )
655 | .split( "$1a" ).join( exprAutoFnDetect )
656 | // .split( "$3" ).join( "'" + all.slice( type.length+2, -2 ) + "'" ) // TODO Optimize for perf later...
657 | .split( "$1" ).join( expr )
658 | .split( "$2" ).join( fnargs ?
659 | fnargs
660 | // fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) {
661 | // params = params ? ("," + params + ")") : (parens ? ")" : "");
662 | // return params ? ("(" + name + ").call($item" + params) : all;
663 | // })
664 | : (def.$2||"")
665 | ) +
666 | "_.push('";
667 | return test;
668 | }) +
669 | "');}return _;"
670 | );
671 | }
672 |
673 | function updateWrapped( options, wrapped ) {
674 | // Build the wrapped content.
675 | options._wrap = buildStringArray( options,
676 | // Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string.
677 | $.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : $( wrapped ).html()]
678 | ).join("");
679 | }
680 |
681 | function unescape( args ) {
682 | return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null;
683 | }
684 |
685 | function outerHtml( elem ) {
686 | var div = document.createElement("div");
687 | div.appendChild( elem.cloneNode(true) );
688 | return div.innerHTML;
689 | }
690 |
691 | // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance.
692 | function storeTmplItems( content ) {
693 | var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m, elemCreated = [];
694 | for ( i = 0, l = content.length; i < l; i++ ) {
695 | if ( (elem = content[i]).nodeType !== 1 ) {
696 | continue;
697 | }
698 | elems = elem.getElementsByTagName("*");
699 | for ( m = elems.length - 1; m >= 0; m-- ) {
700 | processItemKey( elems[m] );
701 | }
702 | processItemKey( elem );
703 | }
704 | while ( elemCreated.length ) {
705 | elem = elemCreated.pop();
706 | $.data( elem, "tmplItem" ).elementCreated( elem );
707 | }
708 | function processItemKey( el ) {
709 | var pntKey, pntNode = el, pntItem, tmplItem, key;
710 | // Ensure that each rendered template inserted into the DOM has its own template item,
711 | if ( (key = el.getAttribute( tmplItmAtt ))) {
712 | while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }
713 | if ( pntKey !== key ) {
714 | // The next ancestor with a _tmplitem expando is on a different key than this one.
715 | // So this is a top-level element within this template item
716 | // Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
717 | pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0;
718 | if ( !(tmplItem = newTmplItems[key]) ) {
719 | // The item is for wrapped content, and was copied from the temporary parent wrappedItem.
720 | tmplItem = wrappedItems[key];
721 | tmplItem = new newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode], null, true );
722 | tmplItem.key = ++itemKey;
723 | newTmplItems[itemKey] = tmplItem;
724 | }
725 | if ( cloneIndex ) {
726 | tmplItem = cloneTmplItem( tmplItem ); // BUG - NEED TO CLONE PARENT BEFORE CLONING CHILD, SO POINTS TO CLONED PARENT, NOT ORIGINAL PARENT...
727 | }
728 | }
729 | el.removeAttribute( tmplItmAtt );
730 | }
731 | // else if ( cloneIndex && (tmplItem = $.data( el, "tmplItem" )) ) {
732 | // // This was a rendered element, cloned during append or appendTo etc.
733 | // // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
734 | // cloneTmplItem( tmplItem.key );
735 | // newTmplItems[tmplItem.key] = tmplItem;
736 | // pntNode = $.data( el.parentNode, "tmplItem" );
737 | // pntNode = pntNode ? pntNode.key : 0;
738 | // }
739 | if ( tmplItem ) {
740 | pntItem = tmplItem;
741 | // Find the template item of the parent element.
742 | // (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
743 | while ( pntItem && pntItem.key != pntNode ) {
744 | // Add this element as a top-level node for this rendered template item, as well as for any
745 | // ancestor items between this item and the item of its parent element
746 | pntItem.nodes.push( el );
747 | pntItem = pntItem.parent;
748 | }
749 | // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
750 | delete tmplItem._ctnt;
751 | delete tmplItem._wrap;
752 | if ( tmplItem.elementCreated ) {
753 | elemCreated.push( el );
754 | }
755 | // Store template item as jQuery data on the element
756 | $.data( el, "tmplItem", tmplItem );
757 | }
758 | function cloneTmplItem( item ) {
759 | var key = item.key;
760 | if ( !key ) {
761 | return item;
762 | }
763 | key += keySuffix;
764 | return newClonedItems[key] =
765 | (newClonedItems[key] || new newTmplItem( item, cloneTmplItem( item.parent ), null, true ));
766 | }
767 | }
768 | }
769 | })( jQuery );
770 |
--------------------------------------------------------------------------------
/beta2/simple-explicit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | show data
10 |
11 | set name and color
12 | set city
13 |
14 |
15 | Declarative
16 |
17 |
32 |
33 |
99 |
100 |
101 |
102 |
107 |
108 |
109 | Console
110 |
111 |
112 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/demos/demo-contacts.css:
--------------------------------------------------------------------------------
1 | /*
2 | css adapted from:
3 | http://veerle.duoh.com/blog/comments/a_css_styled_table/
4 | */
5 | a {
6 | color: #6D929B;
7 | }
8 | .contacts {
9 | border: 1px solid #C1DAD7;
10 | margin:20px;
11 | }
12 | .contacts td {
13 | border-right: 1px solid #C1DAD7;
14 | border-bottom: 1px solid #C1DAD7;
15 | padding: 6px 6px 6px 12px;
16 | color: #6D929B;
17 | vertical-align: middle;
18 | }
19 | .contacts th {
20 | border-right: 1px solid #C1DAD7;
21 | border-bottom: 1px solid #C1DAD7;
22 | border-top: 1px solid #C1DAD7;
23 | letter-spacing: 2px;
24 | text-transform: uppercase;
25 | text-align: left;
26 | padding: 6px 6px 6px 12px;
27 | }
28 | .agebar {
29 | color: #000000;
30 | background-color: #8888FF;
31 | text-align: center;
32 | font-weight: bold;
33 | border: solid 1px blue;
34 | width: 2em;
35 | height: 25px;
36 | }
37 | .phones td, .phones th {
38 | border: none;
39 | padding: 2px 2px 2px 4px;
40 | vertical-align: top;
41 | }
42 |
--------------------------------------------------------------------------------
/demos/demo-contacts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | My Contacts - Linking Demo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
52 |
53 |
54 |
55 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/demos/demo-contacts.js:
--------------------------------------------------------------------------------
1 | jQuery( function( $ ){
2 |
3 | // define some basic default data to start with
4 | var contacts = [
5 | { firstName: "Dave", lastName: "Reed", age: 32, phones: [
6 | { type: "Mobile", number: "(555) 121-2121" },
7 | { type: "Home", number: "(555) 123-4567" } ] },
8 | { firstName: "John", lastName: "Doe", age: 87, phones: [
9 | { type: "Mobile", number: "(555) 444-2222" },
10 | { type: "Home", number: "(555) 999-1212" } ] }
11 | ];
12 |
13 | $.extend( $.convertFn, {
14 | // linking converter that normalizes phone numbers
15 | phone: function( value ) {// turn a string phone number into a normalized one with dashes
16 | // and parens
17 | value = (parseInt( value.replace( /[\(\)\- ]/g, "" ), 10 ) || 0 ).toString();
18 | value = "0000000000" + value;
19 | value = value.substr( value.length - 10 );
20 | value = "(" + value.substr( 0, 3 ) + ") " + value.substr( 3, 3 ) + "-" + value.substr( 6 );
21 | return value;
22 | },
23 | fullname: function( value, source, target ) {
24 | return source.firstName + " " + source.lastName;
25 | }
26 | });
27 |
28 | // show the results of the linking -- object graph is already full of the data
29 | $( "#save" ).click( function() {
30 | $( "#results" ).html( JSON.stringify( contacts, null, 4 ));
31 | });
32 |
33 | // add a new contact when clicking the insert button.
34 | // notice that no code here exists that explicitly redraws
35 | // the template.
36 | $( "#insert" ).click( function() {
37 | contacts.push({ firstName: "first", lastName: "last", phones: [], age:20 });
38 | refresh();
39 | });
40 |
41 | $( "#sort" ).click( function() {
42 | contacts.sort( function( a, b ) {
43 | return a.lastName < b.lastName ? -1 : 1;
44 | });
45 | refresh();
46 | });
47 |
48 | // function that clears the current template and renders it with the
49 | // current state of the global contacts variable.
50 | function refresh() {
51 | $( ".contacts" ).empty();
52 | $( "#contacttmpl" ).tmpl( {contacts:contacts} ).appendTo( ".contacts" );
53 | // bind inputs to the data items
54 | $( "tr.contact" ).each( function(i) {
55 | var contact = contacts[i];
56 | $( "input.contact", this ).link( contact );
57 | $( '.agebar', this ).link( contact, {
58 | age: {
59 | convertBack: function( value, source, target ) {
60 | $( target ).width( value + "px" );
61 | }
62 | }
63 | });
64 | $( contact ).trigger( "changeField", ["age", contact.age] );
65 |
66 | // todo: "update" feature
67 |
68 | $( ".contact-remove", this ).click( function() {
69 | contacts.splice( i, 1 );
70 | refresh();
71 | });
72 | var original_firstName = contact.firstName,
73 | original_lastName = contact.lastName;
74 | $( ".contact-reset", this ).click( function() {
75 | $( contact )
76 | .setField( "firstName", original_firstName )
77 | .setField( "lastName", original_lastName );
78 | });
79 |
80 | $( "tr.phone", this ).each( function(i) {
81 | var phone = contact.phones[i];
82 | $( this ).link( phone, {
83 | type: "type",
84 | number: {
85 | name: "number",
86 | convert: "phone"
87 | }
88 | });
89 | $( ".phone-remove", this ).click( function() {
90 | // note: I'd like to only redraw the phones portion of the
91 | // template, but jquery.tmpl.js does not support nested templates
92 | // very easily. So here I am triggering an arrayChange event on
93 | // the main contacts array to force the entire thing to refresh.
94 | // Note that user input is not lost since the live linking has
95 | // already stored the values in the object graph.
96 | contact.phones.splice( i, 1 );
97 | refresh();
98 | });
99 | });
100 | $( ".newphone", this ).click( function() {
101 | contact.phones.push({ type: "", number: "" });
102 | refresh();
103 | });
104 | });
105 | }
106 |
107 | // initial view on load
108 | refresh();
109 |
110 | });
111 |
112 |
--------------------------------------------------------------------------------
/jquery.datalink.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Data Link plugin v1.0.0pre
3 | * http://github.com/jquery/jquery-datalink
4 | *
5 | * Copyright Software Freedom Conservancy, Inc.
6 | * Dual licensed under the MIT or GPL Version 2 licenses.
7 | * http://jquery.org/license
8 | */
9 | (function( $, undefined ){
10 |
11 | var oldcleandata = $.cleanData,
12 | links = [],
13 | fnSetters = {
14 | val: "val",
15 | html: "html",
16 | text: "text"
17 | },
18 | eventNameSetField = "setField",
19 | eventNameChangeField = "changeField";
20 |
21 | function getLinks(obj) {
22 | var data = $.data( obj ),
23 | cache,
24 | fn = data._getLinks || (cache={s:[], t:[]}, data._getLinks = function() { return cache; });
25 | return fn();
26 | }
27 |
28 | function bind(obj, wrapped, handler) {
29 | wrapped.bind( obj.nodeType ? "change" : eventNameChangeField, handler );
30 | }
31 | function unbind(obj, wrapped, handler) {
32 | wrapped.unbind( obj.nodeType ? "change" : eventNameChangeField, handler );
33 | }
34 |
35 | $.extend({
36 | cleanData: function( elems ) {
37 | for ( var j, i = 0, elem; (elem = elems[i]) != null; i++ ) {
38 | // remove any links with this element as the source
39 | // or the target.
40 | var links = $.data( elem, "_getLinks" );
41 | if ( links ) {
42 | links = links();
43 | // links this element is the source of
44 | var self = $(elem);
45 | $.each(links.s, function() {
46 | unbind( elem, self, this.handler );
47 | if ( this.handlerRev ) {
48 | unbind( this.target, $(this.target), this.handlerRev );
49 | }
50 | });
51 | // links this element is the target of
52 | $.each(links.t, function() {
53 | unbind( this.source, $(this.source), this.handler );
54 | if ( this.handlerRev ) {
55 | unbind( elem, self, this.handlerRev );
56 | }
57 | });
58 | links.s = [];
59 | links.t = [];
60 | }
61 | }
62 | oldcleandata( elems );
63 | },
64 | convertFn: {
65 | "!": function(value) {
66 | return !value;
67 | }
68 | },
69 | setField: function(target, field, value) {
70 | if ( target.nodeType ) {
71 | var setter = fnSetters[ field ] || "attr";
72 | $(target)[setter](value);
73 | } else {
74 | var parts = field.split(".");
75 | parts[1] = parts[1] ? "." + parts[1] : "";
76 |
77 | var $this = $( target ),
78 | args = [ parts[0], value ];
79 |
80 | $this.triggerHandler( eventNameSetField + parts[1] + "!", args );
81 | if ( value !== undefined ) {
82 | target[ field ] = value;
83 | }
84 | $this.triggerHandler( eventNameChangeField + parts[1] + "!", args );
85 | }
86 | }
87 | });
88 |
89 | function getMapping(ev, changed, newvalue, map) {
90 | var target = ev.target,
91 | isSetData = ev.type === eventNameChangeField,
92 | mappedName,
93 | convert,
94 | name;
95 | if ( isSetData ) {
96 | name = changed;
97 | if ( ev.namespace ) {
98 | name += "." + ev.namespace;
99 | }
100 | } else {
101 | name = (target.name || target.id);
102 | }
103 |
104 | if ( !map ) {
105 | mappedName = name;
106 | } else {
107 | var m = map[ name ];
108 | if ( !m ) {
109 | return null;
110 | }
111 | mappedName = m.name;
112 | convert = m.convert;
113 | if ( typeof convert === "string" ) {
114 | convert = $.convertFn[ convert ];
115 | }
116 | }
117 | return {
118 | name: mappedName,
119 | convert: convert,
120 | value: isSetData ? newvalue : $(target).val()
121 | };
122 | }
123 |
124 | $.extend($.fn, {
125 | link: function(target, mapping) {
126 | var self = this;
127 | if ( !target ) {
128 | return self;
129 | }
130 | function matchByName(name) {
131 | var selector = "[name=" + name + "], [id=" + name +"]";
132 | // include elements in this set that match as well a child matches
133 | return self.filter(selector).add(self.find(selector));
134 | }
135 | if ( typeof target === "string" ) {
136 | target = $( target, this.context || null )[ 0 ];
137 | }
138 | var hasTwoWay = !mapping,
139 | map,
140 | mapRev,
141 | handler = function(ev, changed, newvalue) {
142 | // a dom element change event occurred, update the target
143 | var m = getMapping( ev, changed, newvalue, map );
144 | if ( m ) {
145 | var name = m.name,
146 | value = m.value,
147 | convert = m.convert;
148 | if ( convert ) {
149 | value = convert( value, ev.target, target );
150 | }
151 | if ( value !== undefined ) {
152 | $.setField( target, name, value );
153 | }
154 | }
155 | },
156 | handlerRev = function(ev, changed, newvalue) {
157 | // a change or changeData event occurred on the target,
158 | // update the corresponding source elements
159 | var m = getMapping( ev, changed, newvalue, mapRev );
160 | if ( m ) {
161 | var name = m.name,
162 | value = m.value,
163 | convert = m.convert;
164 | // find elements within the original selector
165 | // that have the same name or id as the field that updated
166 | matchByName(name).each(function() {
167 | newvalue = value;
168 | if ( convert ) {
169 | newvalue = convert( newvalue, target, this );
170 | }
171 | if ( newvalue !== undefined ) {
172 | $.setField( this, "val", newvalue );
173 | }
174 | });
175 | }
176 |
177 | };
178 | if ( mapping ) {
179 | $.each(mapping, function(n, v) {
180 | var name = v,
181 | convert,
182 | convertBack,
183 | twoWay;
184 | if ( $.isPlainObject( v ) ) {
185 | name = v.name || n;
186 | convert = v.convert;
187 | convertBack = v.convertBack;
188 | twoWay = v.twoWay !== false;
189 | hasTwoWay |= twoWay;
190 | } else {
191 | hasTwoWay = twoWay = true;
192 | }
193 | if ( twoWay ) {
194 | mapRev = mapRev || {};
195 | mapRev[ n ] = {
196 | name: name,
197 | convert: convertBack
198 | };
199 | }
200 | map = map || {};
201 | map[ name ] = { name: n, convert: convert, twoWay: twoWay };
202 | });
203 | }
204 |
205 | // associate the link with each source and target so it can be
206 | // removed automaticaly when _either_ side is removed.
207 | self.each(function() {
208 | bind( this, $(this), handler );
209 | var link = {
210 | handler: handler,
211 | handlerRev: hasTwoWay ? handlerRev : null,
212 | target: target,
213 | source: this
214 | };
215 | getLinks( this ).s.push( link );
216 | if ( target.nodeType ) {
217 | getLinks( target ).t.push( link );
218 | }
219 | });
220 | if ( hasTwoWay ) {
221 | bind( target, $(target), handlerRev );
222 | }
223 | return self;
224 | },
225 | unlink: function(target) {
226 | this.each(function() {
227 | var self = $(this),
228 | links = getLinks( this ).s;
229 | for (var i = links.length-1; i >= 0; i--) {
230 | var link = links[ i ];
231 | if ( link.target === target ) {
232 | // unbind the handlers
233 | //wrapped.unbind( obj.nodeType ? "change" : "changeData", handler );
234 | unbind( this, self, link.handler );
235 | if ( link.handlerRev ) {
236 | unbind( link.target, $(link.target), link.handlerRev );
237 | }
238 | // remove from source links
239 | links.splice( i, 1 );
240 | // remove from target links
241 | var targetLinks = getLinks( link.target ).t,
242 | index = $.inArray( link, targetLinks );
243 | if ( index !== -1 ) {
244 | targetLinks.splice( index, 1 );
245 | }
246 | }
247 | }
248 | });
249 | },
250 | setField: function(field, value) {
251 | return this.each(function() {
252 | $.setField( this, field, value );
253 | });
254 | }
255 | });
256 |
257 | })(jQuery);
258 |
--------------------------------------------------------------------------------