Skip to content
Browse files

Work on declarative linking from html to object, and from object to o…

…bject, including nested objects.

The markup could be produced by nested jQuery templates. (To do).
Declarative binding from html to object working for top-level fields.
Nested binding from html to object not yet completed.
  • Loading branch information...
1 parent f1c9b6b commit a4a4636c98d5e2b4d568720a2989a5270fbeeef3 @BorisMoore committed
Showing with 194 additions and 52 deletions.
  1. +1 −1 README.md
  2. +36 −7 beta2/datalink.html
  3. +65 −40 beta2/jquery.datalink2.js
  4. +91 −3 beta2/jquery.tmpl2.js
  5. +1 −1 jquery.datalink.js
View
2 README.md
@@ -1,4 +1,4 @@
-# jQuery Data Link plugin plugin v1.0.0pre.
+# jQuery Data Link plugin v1.0.0pre.
_Note: This plugin is currently in beta form and may change significantly before version 1.0 is released. See tagged versions for stable Beta releases. Requires jquery version 1.4.2._
View
43 beta2/datalink.html
@@ -37,18 +37,42 @@
<script type="text/javascript">
var person = {
- firstName: "Jo",
- lastName: "Proctor",
- address: {
- city: "Redmond"
+ firstName: "Jo",
+ lastName: "Proctor",
+ address: {
+ city: "Redmond",
+ },
+ roleColor: "yellow"
},
- roleColor: "yellow"
- };
+ person2 = {
+ firstName: "Jo2",
+ lastName: "Proctor2",
+ address: {
+ city: "Redmond2",
+ },
+ roleColor: "green"
+ },
+ person3 = {
+ firstName: "Jo3",
+ lastName: "Proctor3",
+ address: {
+ city: "Redmond3",
+ },
+ roleColor: "red"
+ };
$.dataLink( person, "#myContainer" ).push();
+
$.dataLink( "#myContainer", person );
+
+ $.dataLink( person, person2 );
+ $.dataLink( person, person3 ).push();
+
+ function nameConvert( value ) {
+ return value + " lives in";
+ }
- function titleConvert( value, source, path, target ) {
+ function titleConvert( value, source, path, target, map ) {
return source.firstName + " lives in " + value;
}
@@ -62,12 +86,17 @@
// Either of these will work, thanks to the binding to intermediate objects
$.setField( person, "address.city", person.address.city + "Xxx" );
}
+
</script>
<script type="text/javascript">
function showData() {
$( "#console" ).append("-----------------");
$( "#showData" ).tmpl( person ).appendTo( "#console" );
+ $( "#console" ).append("-----------------");
+ $( "#showData" ).tmpl( person2 ).appendTo( "#console" );
+ $( "#console" ).append("-----------------");
+ $( "#showData" ).tmpl( person3 ).appendTo( "#console" );
}
</script>
View
105 beta2/jquery.datalink2.js
@@ -21,6 +21,7 @@ var linkSettings, decl,
linkAttr = "data-jq-link",
bindAttr = "data-jq-bind",
+ pathAttr = "data-jq-path",
unsupported = {
"htmlhtml": 1,
@@ -85,13 +86,47 @@ var linkSettings, decl,
function addBinding( map, from, to, callback, links ) {
- function findJqObject( jqObject, type ) {
+ // ============================
+ // Helpers for "toObject" links
+
+ function setFields( sourceObj, basePath, cb ) {
+ var field, isObject, sourceVal;
+
+ for ( field in sourceObj ) {
+ isObject = 1;
+ sourceVal = sourceObj[ field ];
+ if ( sourceObj.hasOwnProperty(field) && !( $.isFunction( sourceVal ) || sourceVal.toJSON)) {
+ setFields( sourceVal, (basePath ? basePath + "." : basePath) + field, cb );
+ }
+ }
+ return isObject || cb( sourceObj, basePath, thisMap.convert, sourceObj );
+ }
+
+ function getLinkedPath( elem, path ) {
+ var basePath = elem.getAttribute( "data-jq-path" );
+ if ( basePath ) {
+ path = basePath + (path ? "." + path : "");
+ }
+ return elem.parentNode === fromObj ? path : getLinkedPath( elem.parentNode, path );
+ }
+
+ function convertAndSetField( val, path, cnvt, sourceObj ) {
+ path = isFromHtml ? getLinkedPath( sourceObj, path ) : path;
+ $.setField( toObj, path, cnvt
+ ? cnvt( val, path, sourceObj, toObj, thisMap )
+ : val
+ );
+ }
+ // ============================
+ // Helper for --- TODO clean up between different cases....
+
+ function findJqObject( jqObject, type ) {
var object, nodeName, path, linkedElems,
length = jqObject.length;
if ( length ) {
object = jqObject[0];
- if ( object.nodeType ) {
+ if ( object.nodeType && type !== "from") {
path = thisMap[ type ];
if ( path ) {
@@ -104,7 +139,6 @@ function addBinding( map, from, to, callback, links ) {
jqObject = jqObject.find( path ).add( jqObject.filter( path ) ); // TODO REPLACE BY ABOVE in the case of default binding, and remove support for random default binding - if perf concerns require it...
thisMap[ type ] = 0;
- thisMap[ type + "Attr" ] = "default";
}
} else if ( length > 1 ) {
jqObject = $([ jqObject.get() ]); // For objects: don't wrap multiples - consider as equivalent to a jQuery object containing single object - namely the array of objects.
@@ -112,6 +146,7 @@ function addBinding( map, from, to, callback, links ) {
}
return jqObject;
}
+ // ============================
var thisMap = typeof map === "string" ? { to: map } : map && $.extend( {}, map ); // Note: "string" corresponds to 'to'. Is this intuitive? It is useful for filtering object copy: $.link( person, otherPerson, ["lastName", "address.city"] );
@@ -131,7 +166,7 @@ function addBinding( map, from, to, callback, links ) {
toObj = to[0],
toType = objectType( toObj ),
eventType = isFromHtml ? "change" : fromType + "Change",
-
+
// TODO Verify optimization for memory footprint in closure captured by handlers, and perf optimization for using function declaration rather than statement?
handler = function( ev, eventArgs ) {
var cancel, sourceValue, sourcePath,
@@ -142,7 +177,7 @@ function addBinding( map, from, to, callback, links ) {
var setter, fromAttr, $source;
fromAttr = thisMap.fromAttr;
- if ( fromAttr === "default" ) {
+ if ( !fromAttr ) {
// Merge in the default attribute bindings for this source element
fromAttr = linkSettings.merge[ source.nodeName.toLowerCase() ];
fromAttr = fromAttr ? fromAttr.from.fromAttr : "text";
@@ -178,7 +213,7 @@ function addBinding( map, from, to, callback, links ) {
to.each( function( _, elem ) {
var setter, targetPath , matchLinkAttr,
targetValue = sourceValue,
- $target = $( elem ),
+ $target = $( elem ),
htmlArrayOperation = {
"add": function() {
@@ -215,7 +250,7 @@ function addBinding( map, from, to, callback, links ) {
if ( convert && $.isFunction( convert )) {
targetValue = convert( targetValue, source, sourcePath, elem, thisMap );
}
- if ( attr === "default" ) {
+ if ( !attr ) {
// Merge in the default attribute bindings for this target element
attr = linkSettings.merge[ elem.nodeName.toLowerCase() ];
attr = attr? attr.to.toAttr : "text";
@@ -256,32 +291,14 @@ function addBinding( map, from, to, callback, links ) {
});
},
"object": function() {
- function setFields( sourceObj, basePath, cb ) {
- var field, isObject, sourceVal;
-
- for ( field in fromObj ) {
- isObject = 1;
- sourceVal = fromObj[ field ];
- if ( fromObj.hasOwnProperty(field) && !( $.isFunction( sourceVal ) || sourceVal.toJSON)) {
- setFields( sourceVal, (basePath ? basePath + "." : basePath) + field, cb );
- }
- }
- return isObject || cb( basePath, sourceObj, convert );
- }
- function convertAndSetField( toPath, val, cnvt ) {
- $.setField( toObj, toPath, cnvt
- ? cnvt( val, source, toPath, toObj, thisMap )
- : val
- );
- }
// Find toPath using thisMap.to, or if not specified, use declarative specification
// provided by decl.applyLinkInfo, applied to source element
var convert = thisMap.Convert,
toPath = thisMap.to || !isFromHtml && sourcePath;
if (toPath ) {
- convertAndSetField( toPath, thisMap.convert );
+ convertAndSetField( sourceValue, toPath, thisMap.convert, source );
} else if ( !isFromHtml ) {
// This is triggered by trigger(), and there is no thisMap.from or thisMap.to specified.
// This will set fields on existing objects or subobjects on the target, but will not create new subobjects, since
@@ -289,13 +306,15 @@ function addBinding( map, from, to, callback, links ) {
setFields( source, "", convertAndSetField );
} else { // from html. (Mapping from array to object not supported)
- var tmplItem = $.tmplItem && $.tmplItem( source );
- if ( !(tmplItem && tmplItem.key) || (tmplItem.data === toObj )) {
- decl.applyLinkInfo( source, function(all, path, declCnvt){
- // TODO support for named converters
- convertAndSetField( path, sourceValue, window[ declCnvt ] || convert );
- });
- }
+// var tmplItem = $.tmplItem && $.tmplItem( source );
+// if ( !(tmplItem && tmplItem.key) || (tmplItem.data === toObj )) {
+
+ decl.applyLinkInfo( source, function(all, path, declCnvt){
+ // TODO support for named converters
+ convertAndSetField( sourceValue, path, window[ declCnvt ] || convert, source );
+ });
+
+// }
}
},
"array": function() {
@@ -334,17 +353,17 @@ function addBinding( map, from, to, callback, links ) {
ev.stopImmediatePropagation();
}
};
-
+ var j, l;
switch ( fromType + toType ) {
case "htmlarray" :
- for ( var j=0, l=toObj.length; j<l; j++ ) {
+ for ( j=0, l=toObj.length; j<l; j++ ) {
addBinding( thisMap, from, $( toObj[j] ), callback, links );
}
break;
case "arrayhtml" :
from.bind( eventType, handler );
- for ( var j=0, l=fromObj.length; j<l; j++ ) {
+ for ( j=0, l=fromObj.length; j<l; j++ ) {
addBinding( thisMap, $( fromObj[j] ), to, callback, links );
}
break;
@@ -383,6 +402,10 @@ function addBinding( map, from, to, callback, links ) {
}
}
+
+// ============================
+// Helpers
+
function objectType( object ) {
return object
? object.nodeType
@@ -394,7 +417,7 @@ function objectType( object ) {
}
function declarativeMap( fromType, toType ) {
- return !unsupported[ fromType + toType ] && $.extend( decl.from[fromType], decl.to[toType], { decl: true } );
+ return !unsupported[ fromType + toType ] && $.extend( {}, decl.from[fromType], decl.to[toType], { decl: true } );
}
function wrapObject( object ) {
@@ -447,6 +470,7 @@ function changeArray( array, eventArgs ) {
}
return ret;
}
+// ============================
$.extend({
dataLink: function( from, to, maps, callback ) {
@@ -538,11 +562,12 @@ $.extend({
decl: {
linkAttr: linkAttr,
bindAttr: bindAttr,
+ pathAttr: pathAttr,
applyLinkInfo: function( elem, setTarget ){
var linkInfo = elem.getAttribute( decl.linkAttr );
- if ( linkInfo ) {
+ if ( linkInfo !== null ) {
// toPath: convert end
- linkInfo.replace( /([\w\.]+)(?:\:\s*(\w+)\(\)\s*)?$/g, setTarget );
+ linkInfo.replace( /([\w\.]*)(?:\:\s*(\w+)\(\)\s*)?$/, setTarget );
}
//lastName:convert1()
// Alternative using name attribute:
@@ -552,7 +577,7 @@ $.extend({
},
applyBindInfo: function( elem, setTarget ){
var bindInfo = elem.getAttribute( decl.bindAttr );
- if ( bindInfo ) {
+ if ( bindInfo !== null ) {
// toAttr: toPath convert( toPath ) end
bindInfo.replace( /(?:([\w\-]+)\:\s*)?(?:(?:([\w\.]+)|(\w+)\(\s*([\w\.]+)\s*\))(?:$|,))/g, setTarget );
}
View
94 beta2/jquery.tmpl2.js
@@ -40,6 +40,29 @@
}
}
+ function newTmplItem2( options, parentItem, fn, data, index ) {
+ // Returns a template item data structure for a new rendered instance of a template (a 'template item').
+ // The content field is a hierarchical array of strings and nested items (to be
+ // removed and replaced by nodes field of dom elements, once inserted in DOM).
+ // Prototype is $.tmpl.item, which provides both methods and fields.
+ var newItem = this;
+ newItem.parent = parentItem || null;
+ newItem.data = data || (parentItem ? parentItem.data : {});
+ newItem._wrap = parentItem ? parentItem._wrap : null;
+ //if ( options ) {
+ $.extend( newItem, options, { nodes: [], parent: parentItem } );
+ //}
+ if ( fn ) {
+ // Build the hierarchical content to be used during insertion into DOM
+ newItem.tmpl = fn;
+ newItem.index = index || 0;
+ newItem._ctnt = newItem._ctnt || newItem.tmpl( $, newItem );
+ newItem.key = ++itemKey;
+ // Keep track of new template item, until it is stored as jQuery Data on DOM element
+ (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
+ }
+ }
+
// Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
$.each({
appendTo: "append",
@@ -290,6 +313,10 @@
}
stack.push( arguments );
},
+ nest2: function( tmpl, data, options ) {
+ // nested template, using {{tmpl}} tag
+ return $.tmpl( $.template( tmpl ), data, options, this );
+ },
nest: function( tmpl, data, options ) {
// nested template, using {{tmpl}} tag
return $.tmpl( $.template( tmpl ), data, options, this );
@@ -398,14 +425,65 @@
var pluginsWrapperTmpl = $.template( null, "{{html this.html()}}" );
newTmplItem.prototype = $.tmpl.item;
+ newTmplItem2.prototype = $.tmpl.item;
//========================== Private helper functions, used by code above ==========================
function renderTemplate( tmpl, data, options, parentItem ) {
- var ret = renderTmplItems( tmpl, data, options, parentItem );
+ var ret = renderTmplItems2( tmpl, data, options, parentItem );
return (parentItem && tmpl) ? ret : buildStringArray( parentItem || topTmplItem, ret ).join("");
}
+ function renderTmplItems2( tmpl, data, options, parentItem ) {
+ // Render template against data as a tree of template items (nested template), or as a string (top-level template).
+ options = options || {};
+ var ret, topLevel = !parentItem;
+ if ( topLevel ) {
+ // This is a top-level tmpl call (not from a nested template using {{tmpl}})
+ parentItem = topTmplItem;
+ if ( !$.isFunction( tmpl ) ) {
+ tmpl = $.template[tmpl] || $.template( null, tmpl );
+ }
+ wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
+ } else if ( !tmpl ) {
+ // The template item is already associated with DOM - this is a refresh.
+ // Re-evaluate rendered template for the parentItem
+ tmpl = parentItem.tmpl;
+ newTmplItems[parentItem.key] = parentItem;
+ parentItem.nodes = [];
+ if ( parentItem.wrapped ) {
+ updateWrapped( parentItem, parentItem.wrapped );
+ }
+ // Rebuild, without creating a new template item
+ return parentItem.tmpl( $, parentItem );
+ }
+ if ( !tmpl ) {
+ return null; // Could throw...
+ }
+ // options = $.extend( {}, options, tmpl )
+ if ( typeof data === "function" ) {
+ data = data.call( parentItem || {} );
+ }
+ if ( options.wrapped ) {
+ updateWrapped( options, options.wrapped );
+ if ( options.addIds ) {
+ // TEMPORARY?
+ tmpl = $.template( null, options._wrap );
+ options._wrap = null;
+ options.wrapped = null;
+ delete options.addIds;
+ }
+ }
+ ctl = options.ctl;
+ newData = data;
+
+ return $.isArray( data ) ?
+ $.map( data, function( dataItem, index ) {
+ return dataItem ? new newTmplItem2( options, parentItem, tmpl, dataItem, index ) : null;
+ }) :
+ [ new newTmplItem2( options, parentItem, tmpl, data ) ];
+ }
+
function renderTmplItems( tmpl, data, options, parentItem ) {
// Render template against data as a tree of template items (nested template), or as a string (top-level template).
options = options || {};
@@ -456,13 +534,23 @@
[ new newTmplItem( options, parentItem, tmpl, data ) ];
}
+ function getTmplItemPath( tmplItem ) {
+ var path = tmplItem.index;
+ while ( tmplItem.parent.key ) {
+ tmplItem = tmplItem.parent;
+ path = tmplItem.index + "." + path;
+ }
+ return path;
+ }
+
function buildStringArray( tmplItem, content ) {
// Convert hierarchical content (tree of nested tmplItems) into flat string array of rendered content (optionally with attribute annotations for tmplItems)
return content ?
$.map( content, function( item ) {
return (typeof item === "string") ?
// Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
- ( tmplItem.annotate && tmplItem.key ? item.replace( /(<\w+)\s(?![^>]*data-jq-item=)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + (tmplItem.key - itemKeyOffset) + "\" $2" ) : item) :
+// ( tmplItem.annotate && tmplItem.key ? item.replace( /(<\w+)(?!\sdata-jq-item)([^>]*)/, "$1 " + tmplItmAtt + "=\"" + (tmplItem.key - itemKeyOffset) + "\" " + "data-jq-path" + "=\"" + getTmplItemPath( tmplItem ) + "\" $2" ) : item) :
+ ( tmplItem.annotate && tmplItem.key ? item.replace( /(<\w+)(?!\sdata-jq-item)([^>]*)/, "$1 " + "data-jq-path" + "=\"" + getTmplItemPath( tmplItem ) + "\" $2" ) : item) :
// This is a child template item. Build nested template.
buildStringArray( item, item._ctnt );
}) :
@@ -564,7 +652,7 @@
tag[ slash ? "close" : "open" ]
.split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )
.split( "$1a" ).join( exprAutoFnDetect )
- .split( "$3" ).join( "'" + all.slice( type.length+2, -2 ) + "'" ) // TODO Optimize for perf later...
+// .split( "$3" ).join( "'" + all.slice( type.length+2, -2 ) + "'" ) // TODO Optimize for perf later...
.split( "$1" ).join( expr )
.split( "$2" ).join( fnargs ?
fnargs
View
2 jquery.datalink.js
@@ -1,5 +1,5 @@
/*!
- * jQuery Data Link plugin 1.0.0pre
+ * jQuery Data Link plugin v1.0.0pre
* http://github.com/jquery/jquery-datalink
*
* Copyright Software Freedom Conservancy, Inc.

0 comments on commit a4a4636

Please sign in to comment.
Something went wrong with that request. Please try again.