├── 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 |

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 | 10 | 11 | 12 | 13 |

14 | 15 |

Declarative

16 | 17 |
18 |

19 | Name: 20 |

21 |

22 | 23 | 24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 | 32 |
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 | 10 | 11 | 12 |

13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 28 | 32 | 33 | 34 | 35 | 38 | 42 | 43 | 44 | 45 | 48 | 52 | 53 | 54 | 55 | 56 |
17 | 18 |

19 | 20 | 21 |
26 | Name: 27 | 29 | 30 | 31 |
36 | Name: 37 | 39 | 40 | 41 |
46 | Name: 47 | 49 | 50 | 51 |
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 | 17 | 18 | 19 | 20 | 21 |

22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 36 | 37 | 38 | 42 | 43 | 44 | 48 | 49 | 50 | 54 | 55 | 56 | 60 | 61 | 62 |
27 | 28 | Name: 29 |
33 | 34 | 35 |
39 | 40 | Name: 41 |
45 | 46 | 47 |
51 | 52 | Name: 53 |
57 | 58 | 59 |
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 | 17 | 18 | 19 | 20 | 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 | 10 | 11 | 12 | 13 |

14 | 15 |

Declarative

16 | 17 |
18 |

19 | Name: 20 |

21 |

22 | 23 | 24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 |
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 | 56 |
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 | --------------------------------------------------------------------------------