Skip to content

Commit

Permalink
Major update. Added onRender callback, used by JsViews to provide sup…
Browse files Browse the repository at this point in the history
…port

for element-based data-linking in contexts where IE does not allow HTML
comment insertion. Also multiple minor bug fixes.
  • Loading branch information
BorisMoore committed May 8, 2012
1 parent 283548a commit 53c8de7
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 63 deletions.
120 changes: 63 additions & 57 deletions jsrender.js
Expand Up @@ -6,7 +6,7 @@
* Copyright 2012, Boris Moore * Copyright 2012, Boris Moore
* Released under the MIT License. * Released under the MIT License.
*/ */
// informal pre beta commit counter: 6 // informal pre beta commit counter: 7


this.jsviews || this.jQuery && jQuery.views || (function( window, undefined ) { this.jsviews || this.jQuery && jQuery.views || (function( window, undefined ) {


Expand All @@ -20,7 +20,7 @@ var versionNumber = "v1.0pre",
jQuery = window.jQuery, jQuery = window.jQuery,


rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.]*?)(?:[.[]([\w$]+)\]?)?|(['"]).*\8)$/g, rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.]*?)(?:[.[]([\w$]+)\]?)?|(['"]).*\8)$/g,
// nil object helper view viewProperty pathTokens leafToken string // nil object helper view viewProperty pathTokens leafToken string


rParams = /(\()(?=|\s*\()|(?:([([])\s*)?(?:([#~]?[\w$.]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*!:?\/]|(=))\s*|([#~]?[\w$.]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*([)\]])([([]?))|(\s+)/g, rParams = /(\()(?=|\s*\()|(?:([([])\s*)?(?:([#~]?[\w$.]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*!:?\/]|(=))\s*|([#~]?[\w$.]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*([)\]])([([]?))|(\s+)/g,
// lftPrn lftPrn2 path operator err eq path2 prn comma lftPrn2 apos quot rtPrn prn2 space // lftPrn lftPrn2 path operator err eq path2 prn comma lftPrn2 apos quot rtPrn prn2 space
Expand Down Expand Up @@ -81,20 +81,17 @@ function setDelimiters( openChars, closeChars ) {
// Build regex with new delimiters // Build regex with new delimiters
jsv.rTag = rTag // make rTag available to JsViews (or other components) for parsing binding expressions jsv.rTag = rTag // make rTag available to JsViews (or other components) for parsing binding expressions
= secondOpenChar = secondOpenChar
// tag (followed by / space or }) or colon or html or code // tag (followed by / space or }) or cvtr+colon or html or code
+ "(?:(?:(\\w+(?=[\\/\\s" + firstCloseChar + "]))|(?:(\\w+)?(:)|(>)|(\\*)))" + "(?:(?:(\\w+(?=[\\/\\s" + firstCloseChar + "]))|(?:(\\w+)?(:)|(>)|(\\*)))"
// params // params
+ "\\s*((?:[^" + firstCloseChar + "]|" + firstCloseChar + "(?!" + secondCloseChar + "))*?)" + "\\s*((?:[^" + firstCloseChar + "]|" + firstCloseChar + "(?!" + secondCloseChar + "))*?)";
// slash or closeBlock
+ "(\\/)?|(?:\\/(\\w+)))"
// }}
+ firstCloseChar;


// Default rTag: tag converter colon html code params slash closeBlock // slash or closeBlock }}
// /{{(?:(?:(\w+(?=[\/\s\}]))|(?:(\w+)?(:)|(>)|(\*)))\s*((?:[^}]|}(?!\}))*?)(\/)?|(?:\/(\w+)))}} rTag = new RegExp( firstOpenChar + rTag + "(\\/)?|(?:\\/(\\w+)))" + firstCloseChar + secondCloseChar, "g" );

// Default rTag: tag converter colon html code params slash closeBlock
// /{{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}


// /{{(?:(?:(\w+(?=[\/!\s\}!]))|(?:(\w+)?(:)|(>)|(\*)))((?:[^\}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g;
rTag = new RegExp( firstOpenChar + rTag + secondCloseChar, "g" );
rTmplString = new RegExp( "<.*>|" + openChars + ".*" + closeChars ); rTmplString = new RegExp( "<.*>|" + openChars + ".*" + closeChars );
return this; return this;
} }
Expand Down Expand Up @@ -130,9 +127,8 @@ function convert( converter, view, text ) {
function renderTag( tag, parentView, converter, content, tagObject ) { function renderTag( tag, parentView, converter, content, tagObject ) {
// Called from within compiled template function, to render a nested tag // Called from within compiled template function, to render a nested tag
// Returns the rendered tag // Returns the rendered tag
tagObject.props = tagObject.props || {};
var ret, var ret,
tmpl = tagObject.props.tmpl, tmpl = tagObject.props && tagObject.props.tmpl,
tmplTags = parentView.tmpl.tags, tmplTags = parentView.tmpl.tags,
nestedTemplates = parentView.tmpl.templates, nestedTemplates = parentView.tmpl.templates,
args = arguments, args = arguments,
Expand All @@ -144,6 +140,7 @@ function renderTag( tag, parentView, converter, content, tagObject ) {
// Set the tmpl property to the content of the block tag, unless set as an override property on the tag // Set the tmpl property to the content of the block tag, unless set as an override property on the tag
content = content && parentView.tmpl.tmpls[ content - 1 ]; content = content && parentView.tmpl.tmpls[ content - 1 ];
tmpl = tmpl || content || undefined; tmpl = tmpl || content || undefined;

tagObject.tmpl = tagObject.tmpl =
"" + tmpl === tmpl // if a string "" + tmpl === tmpl // if a string
? nestedTemplates && nestedTemplates[ tmpl ] || templates[ tmpl ] || templates( tmpl ) ? nestedTemplates && nestedTemplates[ tmpl ] || templates[ tmpl ] || templates( tmpl )
Expand All @@ -153,9 +150,6 @@ function renderTag( tag, parentView, converter, content, tagObject ) {
tagObject.converter = converter; tagObject.converter = converter;
tagObject.view = parentView; tagObject.view = parentView;
tagObject.renderContent = renderContent; tagObject.renderContent = renderContent;
if ( parentView.ctx ) {
extend( tagObject.ctx, parentView.ctx);
}


ret = tagFn.apply( tagObject, args.length > 5 ? slice.call( args, 5 ) : [] ); ret = tagFn.apply( tagObject, args.length > 5 ? slice.call( args, 5 ) : [] );
return ret || ( ret == undefined ? "" : ret.toString()); // (If ret is the value 0 or false, will render to string) return ret || ( ret == undefined ? "" : ret.toString()); // (If ret is the value 0 or false, will render to string)
Expand Down Expand Up @@ -270,19 +264,24 @@ function converters( name, converterFn ) {
// renderContent // renderContent
//================= //=================


function renderContent( data, context, parentView, path, index ) { function renderContent( data, context, path, index, parentView ) {
// Render template against data as a tree of subviews (nested template), or as a string (top-level template). // Render template against data as a tree of subviews (nested template), or as a string (top-level template).
// tagName parameter for internal use only. Used for rendering templates registered as tags (which may have associated presenter objects) // tagName parameter for internal use only. Used for rendering templates registered as tags (which may have associated presenter objects)
var i, l, dataItem, newView, itemWrap, itemsWrap, itemResult, parentContext, tmpl, layout, var i, l, dataItem, newView, itemWrap, itemsWrap, itemResult, parentContext, tmpl, layout, onRender, props,
props = {},
swapContent = index === TRUE, swapContent = index === TRUE,
self = this, self = this,
result = ""; result = "";


if ( self.isTag ) { if ( self.isTag ) {
// This is a call from renderTag // This is a call from renderTag
tmpl = self.tmpl; tmpl = self.tmpl;
context = context || self.ctx; if ( self.props && self.ctx ) {
extend( self.ctx, self.props );
}
if ( self.ctx && context ) {
context = extend( self.ctx, context );
}
context = self.ctx || context;
parentView = parentView || self.view; parentView = parentView || self.view;
path = path || self.path; path = path || self.path;
index = index || self.index; index = index || self.index;
Expand All @@ -303,48 +302,44 @@ function renderContent( data, context, parentView, path, index ) {


// Set additional context on views created here, (as modified context inherited from the parent, and be inherited by child views) // Set additional context on views created here, (as modified context inherited from the parent, and be inherited by child views)
// Note: If no jQuery, extend does not support chained copies - so limit extend() to two parameters // Note: If no jQuery, extend does not support chained copies - so limit extend() to two parameters
// TODO make this reusable also for use in JsViews, for adding context passed in with the link() method.
context = (context && context === parentContext) context = (context && context === parentContext)
? parentContext ? parentContext
: (parentContext : context
// if parentContext, make copy // if context, make copy
? ((parentContext = extend( {}, parentContext ), context) // If context, merge context with copied parentContext
// If context, merge context with copied parentContext ? extend( extend( {}, parentContext ), context )
? extend( parentContext, context ) : parentContext;
: parentContext)
// if no parentContext, use context, or default to {} if ( context.link === FALSE ) {
: context || {});

if ( props.link === FALSE ) {
// Override inherited value of link by an explicit setting in props: link=false // Override inherited value of link by an explicit setting in props: link=false
// The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect. // The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect.
context.link = FALSE; context.onRender = FALSE;
} }
if ( !tmpl.fn ) { if ( !tmpl.fn ) {
tmpl = templates[ tmpl ] || templates( tmpl ); tmpl = templates[ tmpl ] || templates( tmpl );
} }
itemWrap = context.link && sub.onRenderItem; onRender = context.onRender;
itemsWrap = context.link && sub.onRenderItems;


if ( tmpl ) { if ( tmpl ) {
if ( $.isArray( data ) && !layout ) { if ( $.isArray( data ) && !layout ) {
// Create a view for the array, whose child views correspond to each data item. // Create a view for the array, whose child views correspond to each data item.
// (Note: if index and parentView are passed in along with parent view, treat as // (Note: if index and parentView are passed in along with parent view, treat as
// insert -e.g. from view.addViews - so parentView is already the view item for array) // insert -e.g. from view.addViews - so parentView is already the view item for array)
newView = swapContent ? parentView : (index !== undefined && parentView) || View( context, path, parentView, data, tmpl, index ); newView = swapContent ? parentView : (index !== undefined && parentView) || View( context, path, parentView, data, tmpl, index );

for ( i = 0, l = data.length; i < l; i++ ) { for ( i = 0, l = data.length; i < l; i++ ) {
// Create a view for each data item. // Create a view for each data item.
dataItem = data[ i ]; dataItem = data[ i ];
itemResult = tmpl.fn( dataItem, View( context, path, newView, dataItem, tmpl, (index||0) + i ), jsv ); itemResult = tmpl.fn( dataItem, View( context, path, newView, dataItem, tmpl, (index||0) + i ), jsv );
result += itemWrap ? itemWrap( itemResult, props ) : itemResult; result += onRender ? onRender( itemResult, tmpl, props ) : itemResult;
} }
} else { } else {
// Create a view for singleton data object. // Create a view for singleton data object.
newView = swapContent ? parentView : View( context, path, parentView, data, tmpl, index ); newView = swapContent ? parentView : View( context, path, parentView, data, tmpl, index );
result += (data || layout) ? tmpl.fn( data, newView, jsv ) : ""; result += (data || layout) ? tmpl.fn( data, newView, jsv ) : "";
} }
parentView.topKey = newView.index; parentView.topKey = newView.index;
return itemsWrap ? itemsWrap( result, path, newView.index, tmpl, props ) : result; return onRender ? onRender( result, tmpl, props, newView.index, path ) : result;
} }
return ""; // No tmpl. Could throw... return ""; // No tmpl. Could throw...
} }
Expand Down Expand Up @@ -394,7 +389,8 @@ function tmplFn( markup, tmpl, bind ) {
} }
var hash = "", var hash = "",
passedCtx = "", passedCtx = "",
block = !slash && !colon; // Block tag if not self-closing and not {{:}} or {{>}} (special case) // Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression (has bind parameter)
block = !slash && !colon && !bind;


//==== nested helper function ==== //==== nested helper function ====


Expand Down Expand Up @@ -511,7 +507,7 @@ function tmplFn( markup, tmpl, bind ) {
+ "}catch(e){return j.err(e);}"; + "}catch(e){return j.err(e);}";


try { try {
code = new Function( "data, view, j, b, u", code ); code = new Function( "data, view, j, b, u", code );
} catch(e) { } catch(e) {
syntaxError( "Error in compiled template code:\n" + code, e ); syntaxError( "Error in compiled template code:\n" + code, e );
} }
Expand Down Expand Up @@ -540,29 +536,38 @@ function parseParams( params, bind ) {
path = path || path2; path = path || path2;
prn = prn || prn2 || ""; prn = prn || prn2 || "";
operator = operator || ""; operator = operator || "";
var bindParam = bind && prn !== "(";


function parsePath( all, object, helper, view, viewProperty, pathTokens, leafToken ) { function parsePath( all, object, helper, view, viewProperty, pathTokens, leafToken ) {
// rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.]*?)(?:[.[]([\w$]+)\]?)?|(['"]).*\8)$/g, // rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.]*?)(?:[.[]([\w$]+)\]?)?|(['"]).*\8)$/g,
// object helper view viewProperty pathTokens leafToken string // object helper view viewProperty pathTokens leafToken string
if ( object ) { if ( object ) {
var ret = (helper var leaf,
? 'view.hlp("' + helper + '")' ret = (helper
: view ? 'view.hlp("' + helper + '")'
? "view" : view
: "data") ? "view"
+ (leafToken : "data")
? (viewProperty + (leafToken
? "." + viewProperty ? (viewProperty
: helper ? "." + viewProperty
? "" : helper
: (view ? "" : "." + object) ? ""
) + (pathTokens || "") : (view ? "" : "." + object)
: (leafToken = helper ? "" : view ? viewProperty || "" : object, "")); ) + (pathTokens || "")

: (leafToken = helper ? "" : view ? viewProperty || "" : object, ""));
if ( bind && prn !== "(" ) {
ret = "b(" + ret + ',"' + leafToken + '")'; leaf = (leafToken ? "." + leafToken : "")
if ( !bindParam) {
ret = ret + leaf;
}
ret = ret.slice( 0,9 ) === "view.data"
? ret.slice(5) // convert #view.data... to data...
: ret;
if ( bindParam ) {
ret = "b(" + ret + ',"' + leafToken + '")' + leaf;
} }
return ret + (leafToken ? "." + leafToken : ""); return ret;
} }
return all; return all;
} }
Expand Down Expand Up @@ -659,6 +664,7 @@ function compile( name, tmpl, parent, options ) {
} }


//==== Compile the template ==== //==== Compile the template ====
tmpl = tmpl || "";
tmplOrMarkup = tmplOrMarkupFromStr( tmpl ); tmplOrMarkup = tmplOrMarkupFromStr( tmpl );


// If tmpl is a template object, use it for options // If tmpl is a template object, use it for options
Expand Down Expand Up @@ -822,7 +828,7 @@ tags({
result = "", result = "",
args = arguments, args = arguments,
l = args.length; l = args.length;
if ( self.props.layout ) { if ( self.props && self.props.layout ) {
self.tmpl.layout = TRUE; self.tmpl.layout = TRUE;
} }
for ( i = 0; i < l; i++ ) { for ( i = 0; i < l; i++ ) {
Expand Down
10 changes: 5 additions & 5 deletions test/perf-compare.html
Expand Up @@ -77,7 +77,7 @@ <h3>Perf comparison</h3>
} }


// Test render to string perf // Test render to string perf
$( "#results" ).append( "<tr><td colspan='2'>______________________________________________</td></tr>" ); $( "#results" ).append( "<tr><td colspan='2'>________________________________________________________</td></tr>" );
$( "#results" ).append( "<tr><td colspan='2'><b>Optimized render to string</b></td></tr>"); $( "#results" ).append( "<tr><td colspan='2'><b>Optimized render to string</b></td></tr>");


test( "jQuery Template", times * 500, 1, function() { test( "jQuery Template", times * 500, 1, function() {
Expand All @@ -97,7 +97,7 @@ <h3>Perf comparison</h3>
}); });


// Test html encoding perf // Test html encoding perf
$( "#results" ).append( "<tr><td colspan='2'>______________________________________________</td></tr>" ); $( "#results" ).append( "<tr><td colspan='2'>________________________________________________________</td></tr>" );
$( "#results" ).append( "<tr><td colspan='2'><b>Render to string, with HTML encoding</b></td></tr>"); $( "#results" ).append( "<tr><td colspan='2'><b>Render to string, with HTML encoding</b></td></tr>");


test( "jQuery Template", times * 50, 1, function() { test( "jQuery Template", times * 50, 1, function() {
Expand All @@ -114,7 +114,7 @@ <h3>Perf comparison</h3>
}); });


// Test full features perf // Test full features perf
$( "#results" ).append( "<tr><td colspan='2'>______________________________________________</td></tr>" ); $( "#results" ).append( "<tr><td colspan='2'>________________________________________________________</td></tr>" );
$( "#results" ).append( "<tr><td colspan='2'><b>Full features - view hierarchy etc.</b></td></tr>"); $( "#results" ).append( "<tr><td colspan='2'><b>Full features - view hierarchy etc.</b></td></tr>");


test( "jQuery Template full features - inserted in DOM", times * 5, 0, function() { test( "jQuery Template full features - inserted in DOM", times * 5, 0, function() {
Expand All @@ -126,7 +126,7 @@ <h3>Perf comparison</h3>
}); });


// Test compile perf // Test compile perf
$( "#results" ).append( "<tr><td colspan='2'>______________________________________________</td></tr>" ); $( "#results" ).append( "<tr><td colspan='2'>________________________________________________________</td></tr>" );
$( "#results" ).append( "<tr><td colspan='2'><b>Compile</b></td></tr>"); $( "#results" ).append( "<tr><td colspan='2'><b>Compile</b></td></tr>");


test( "jQuery Template compile", times * 5, 0, function() { test( "jQuery Template compile", times * 5, 0, function() {
Expand All @@ -144,7 +144,7 @@ <h3>Perf comparison</h3>
tmpl_JsRender = $.templates( "test", jsRenderTemplate ); tmpl_JsRender = $.templates( "test", jsRenderTemplate );
}); });


$( "#results" ).append( "<tr><td colspan='2'>______________________________________________</td></tr>" ); $( "#results" ).append( "<tr><td colspan='2'>________________________________________________________</td></tr>" );


runNextTest(); runNextTest();
</script> </script>
Expand Down
3 changes: 2 additions & 1 deletion test/unit/jsrender-tests-no-jquery.js
Expand Up @@ -178,7 +178,7 @@ test("expressions", function() {


module( "{{for}}" ); module( "{{for}}" );
test("{{for}}", function() { test("{{for}}", function() {
expect(9); expect(10);
jsviews.templates( { jsviews.templates( {
forTmpl: "header_{{for people}}{{:name}}{{/for}}_footer", forTmpl: "header_{{for people}}{{:name}}{{/for}}_footer",
layoutTmpl: { layoutTmpl: {
Expand All @@ -194,6 +194,7 @@ test("{{for}}", function() {
equal( jsviews.render.layoutTmpl( people ), "header_JoBill_footer", 'layout: true... "header_{{for #data}}{{:name}}{{/for}}_footer"' ); equal( jsviews.render.layoutTmpl( people ), "header_JoBill_footer", 'layout: true... "header_{{for #data}}{{:name}}{{/for}}_footer"' );
equal( jsviews.render.pageTmpl({ people: people }), "header_JoBill_footer", '{{for people tmpl="layoutTmpl"/}}' ); equal( jsviews.render.pageTmpl({ people: people }), "header_JoBill_footer", '{{for people tmpl="layoutTmpl"/}}' );
equal( jsviews.templates( "{{for people towns}}{{:name}}{{/for}}" ).render({ people: people, towns: towns }), "JoBillSeattleParisDelhi", "concatenated targets: {{for people towns}}" ); equal( jsviews.templates( "{{for people towns}}{{:name}}{{/for}}" ).render({ people: people, towns: towns }), "JoBillSeattleParisDelhi", "concatenated targets: {{for people towns}}" );
equal( jsviews.templates( "{{for}}xxx{{/for}}" ).render({}), "", "no parameter - outputs empty string: {{for}}" );


equal( jsviews.render.simpleFor({people:[]}), "ab", 'Empty array renders empty string' ); equal( jsviews.render.simpleFor({people:[]}), "ab", 'Empty array renders empty string' );
equal( jsviews.render.simpleFor({people:["",false,null,undefined,1]}), "aContentContentContentContentContentb", 'Empty string, false, null or undefined members of array are also rendered' ); equal( jsviews.render.simpleFor({people:["",false,null,undefined,1]}), "aContentContentContentContentContentb", 'Empty string, false, null or undefined members of array are also rendered' );
Expand Down

0 comments on commit 53c8de7

Please sign in to comment.