Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

_.template: allow undefined keys #237

Closed
mren opened this Issue · 22 comments

14 participants

Mark Engel Jeremy Ashkenas Dmitry Polushkin Liam R. Black Ilya Shaisultanov Adrian Sieber John-David Dalton Casey Foster Anthony Mckale Nik Martin Josh Anton Khlynovskiy Phil djkmiles
Mark Engel

When the template function needs an element that is not defined it returns an Error.
It would be great if the template function would act equivalent as if the value of the element is undefined

_.template('<%= foo %>', {foo: undefined});

returns "", but

_.template('<%= foo %>', {});

returns a ReferenceError

The reason I'm requesting this is, that I usually use underscore to template my views. But sometimes the attributes of the model are not yet available.
In this case the application returns an error, it would be better if the template function would be more lax.

Jeremy Ashkenas
Owner

I'm afraid that this is simply the way that with(){} works in JS. If the variable isn't declared, it's a ReferenceError. There's nothing we can do about it, while preserving the rest of template behavior.

Jeremy Ashkenas jashkenas closed this
Dmitry Polushkin

What do you can suggest in this case? @jashkenas
Thanks!

Liam R. Black

As a solution you could potentially put type x !='undefined' && x || undefined throughout your script. For example:

print(typeof x !='undefined' && x || undefined)

Ugly but it works.

Ultimately their is no way to initiate all variables to default undefined/null values. If you just want values inside of <%=value%> to be optionally you could do the following in place of the interpolate code.

.replace(/<%=([\s\S]+?)%>/g, function(match, code) {
 var v = code.replace(/\\'/g, "'");
 return "',(typeof(" + v + ") != 'undefined' && '"+v+" ' || null),'";
})

Without writing a lexer/parser there isn't an foolproof way wrap all identifiers in typeof x=!'undefined' && x || undefined.

Another alternative is we could allow an default object to be passed to _.template(str,data,default). That could be merged with the passed in data. It shouldn't add to much code, add could avoid the problem above.

Liam R. Black

How about binding this also to the current object?
Then you could go along doing.

var foo = "<% print(this.bar); %>";
var t = template(foo);
t({});
t({bar:'foo'});

It would only involve changing the following line

return data ? func(data) : func;

to

var bind = function(obj){return func.call(obj,obj)};
return data ? bind(data) : bind;
Jeremy Ashkenas
Owner

All of these are unnecessary. If you really want to not have to define the variables you're going to use, just use a data object with properties.

Instead of:

templateFunction({a: a, b: b, c: c});

Do:

templateFunction({data: {a: a, b: b, c: c}});

And then in your code, you can refer to data.a, data.b, or data.z without getting a reference error.

Ilya Shaisultanov

Is this also the reason the template doesn't render if it encounters an undefined variable, like here

<% if(myvar){/* do stuff */} %>

?
If myvar is undefined in any of the objects passed to template - template doesn't render.

Liam R. Black

@diversario Yeah same reason.

You could pass an object as the first parameter, referring to the object by value.

For example:

_.template('<% if( data.a ){ /* do stuff */ } %>', {data: {a: a, b: b, c: c}} );

It is slightly annoying, but avoidable.

Adrian Sieber

This bug really sucks. I'm not going to use underscore templates again. -.-

John-David Dalton

@aduis specify a variable option to avoid it

_.template(
  '<% if( data.a ){ } %>',
  { 'a': a, 'b': b, 'c': c },
  { 'variable': 'data' }
);
Casey Foster

@adius, using variable as @jdalton said is also more performant. I always use {variable: 'o'} because it's short and sweet. I can write if (o.someFlag) do blah and never worry about a ReferenceError.

Adrian Sieber

Yeah, thanks! It works, but it makes the templates even uglier. :-/

Anthony Mckale

I've just started using underscore, ps it's fab

I've written a wee patch/hack with solves this issue for me and creates a wee config flag -> _.templateSettings.tryCatchReferenceErrorProtection

use like so (assumes JSON is in the right place and working)

_.templateSettings.tryCatchReferenceErrorProtection = true;
var compiled = _.template("hello: <%= thing1 %> <%= IDontExist %>");
console.log(compiled({thing1:'moe'}}));

My two ideas to solve it were :
Idea one was to wrap the interpolate and escape in a try catch and do a if ReferenceError check, basically it ruined performance and didn't work in IE8

My second idea worked much better, do a wee looks like variable regex, create a object with all the variable look alikes and auto default it

It's just really a proof of concept, my 2nd solution is about x1.1 slower compiling and x3 faster evaluating for a simple example in chrome, and ontop of that it's still a comfortable x7 faster than my current prototypejs solution ;-)

Ps I have no idea why do a defaults merge and providing a handy keys var provides a slight speed increase (subsequent runs are less pronounced than the 37ms/90ms, more 78ms/90ms), maybe the _.defaults kills the scope, meh ?

Hope it's useful for someone as a idea to solve this wee issue

Diff ->

Index: proteus-web/src/main/webapp/js/lib/underscorejs/underscore_1_4_4.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>ISO-8859-1
===================================================================
--- proteus-web/src/main/webapp/js/lib/underscorejs/underscore_1_4_4.js (revision 23353)
+++ proteus-web/src/main/webapp/js/lib/underscorejs/underscore_1_4_4.js (revision )
@@ -1089,8 +1089,27 @@
    _.templateSettings = {
        evaluate    : /<%([\s\S]+?)%>/g,
        interpolate : /<%=([\s\S]+?)%>/g,
-       escape      : /<%-([\s\S]+?)%>/g
+       escape      : /<%-([\s\S]+?)%>/g,
+       tryCatchReferenceErrorProtection : true
    };
+   var simpleVarMatcher = /[a-zA-Z0-9][\.a-zA-Z0-9]*/g;
+   /**
+    * create a namespace from a string eg 'proteus.widgets'
+    * @param {Object} obj this is where to make namespace
+    * @param {string} nsStr eg "hello.world"
+    * @return {Object}
+    */
+   var namespace = function(obj, nsStr){
+       var ns = nsStr.split("."), o = obj;
+       for(var i = 0, len = ns.length; i < len; i++){
+           if(i == ns.length - 1) {
+               o = o[ns[i]] = o[ns[i]] || null;
+           } else {
+               o = o[ns[i]] = o[ns[i]] || {};
+           }
+       }
+       return obj;
+   };

    // When customizing `templateSettings`, if you don't want to define an
    // interpolation, evaluation or escaping regex, we need one that is
@@ -1128,10 +1147,20 @@
        // Compile the template source, escaping string literals appropriately.
        var index = 0;
        var source = "__p+='";
+       var keys = {};
        text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
            source += text.slice(index, offset)
                .replace(escaper, function(match) { return '\\' + escapes[match]; });

+           if(settings.tryCatchReferenceErrorProtection) {
+               var matchTxt = escape || interpolate || evaluate;
+               if(matchTxt) {
+                   _.each(matchTxt.match(simpleVarMatcher), function(varStr) {
+                       namespace(keys, varStr);
+                   });
+               }
+           }
+
            if (escape) {
                source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
            }
@@ -1147,7 +1176,11 @@
        source += "';\n";

        // If a variable is not specified, place data values in local scope.
+       if(settings.tryCatchReferenceErrorProtection) {
+           if (!settings.variable) source = 'with(_.defaults(obj||{}, ' + JSON.stringify(keys) + ')){\n' + source + '}\n';
+       } else {
-       if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+           if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+       }

        source = "var __t,__p='',__j=Array.prototype.join," +
            "print=function(){__p+=__j.call(arguments,'');};\n" +
@@ -1167,6 +1200,7 @@

        // Provide the compiled function source as a convenience for precompilation.
        template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
+       template.keys = _.keys(keys);

        return template;
    };
\ No newline at end of file
Anthony Mckale

ps that's doing ->

CHROME BENCHMARK

5.594 underscore recompile start with ReferenceErrorProtection
0.034 underscore compiled start with ReferenceErrorProtection
5.128 underscore recompile start without ReferenceErrorProtection
0.090 underscore compiled start without ReferenceErrorProtection
0.658 prototype recompile start
0.654 prototype compile start
0.089 underscore compile start with ReferenceErrorProtection and Reference Errors
0.707 prototype compile start and Reference Errors

// ps insert fav timed log func for console.timedLog
_.templateSettings.tryCatchReferenceErrorProtection = true;
console.timedLog("reset");console.timedLog("underscore recompile start with ReferenceErrorProtection");
for(var a = 0; a < 10000; a++) {
    var compiled = _.template("hello: <%= name %>");
    compiled({name : 'moe'});
}
console.timedLog("underscore recompile end");

console.timedLog("underscore compiled start with ReferenceErrorProtection");
var compiled = _.template("hello: <%= name %>");
for(var a = 0; a < 10000; a++) {
    compiled({name : 'moe'});
}
console.timedLog("underscore compiled end");

_.templateSettings.tryCatchReferenceErrorProtection = false;
console.timedLog("underscore recompile start without ReferenceErrorProtection");
for(var a = 0; a < 10000; a++) {
    var compiled = _.template("hello: <%= name %>");
    compiled({name : 'moe'});
}
console.timedLog("underscore recompile end");

console.timedLog("underscore compiled start without ReferenceErrorProtection");
var compiled = _.template("hello: <%= name %>");
for(var a = 0; a < 10000; a++) {
    compiled({name : 'moe'});
}
console.timedLog("underscore compiled end");

console.timedLog("prototype recompile start");
for(var a = 0; a < 10000; a++) {
    var compiled = new Template("hello: #{name}");
    compiled.eval({name : 'moe'});
}
console.timedLog("prototype recompile end");

console.timedLog("prototype compile start");
var compiled = new Template("hello: #{name}");
for(var a = 0; a < 10000; a++) {
    compiled.eval({name : 'moe'});
}
console.timedLog("prototype compile end");



_.templateSettings.tryCatchReferenceErrorProtection = true;
console.timedLog("underscore compile start with ReferenceErrorProtection and Reference Errors");
var compiled = _.template("hello: <%= name %> <%= IDontExist %>");
for(var a = 0; a < 10000; a++) {
    compiled({name : 'moe'});
}
console.timedLog("underscore recompile end");

console.timedLog("prototype compile start and Reference Errors");
var compiled = new Template("hello: #{name} #{IDontExist}");
for(var a = 0; a < 10000; a++) {
    compiled.eval({name : 'moe'});
}
console.timedLog("prototype compile end");
John-David Dalton

I had something similar to this for a bit. The problem I ran into was devs wanting to access global variables. It would incorrectly prefix their identifier with obj.foo instead of letting it access foo on the global.

Anthony Mckale

hmm yah, maybe i should rename it "isolateAndContain!" flag or wrap some "window.get(key)?window.get(key):key" action on it

Nik Martin

All of the workarounds fail if the object/attribute you are trying to access is deeply nested:

{data:{foo:1}}

data.bar.bat is still a reference error, so the only way to do this is nested ifs, which is horrible

if (data.bar){ 
    if (data.bar.bat){ 
        do something 
    }
}
Josh

Ok. Now I'm really confused.

I'm populating often complex data objects that may or may not contain certain properties.

So, I want to populate: data.store.phone.ext, but in many cases the property data.store.phone itself doesn't exist.

I've written a little function, exists(), that tests for undefined properties and parents and returns an empty string "" if true, but was hoping to get rid of it in my code because the following looks plain ugly:
<% exists(data.store.phone.ext) %>

Are you saying I can get rid of my exists function, or would doing so produce reference errors?

Nik Martin

@halfnibble I don't think so - that's the same problem I have: you have to start at the 'top' of the object heairarchy testing all the way down or you'll get a reference error

Anthony Mckale

basically where data == undefined

<% exists(data.store.phone.ext) %> == reference error

but

<% exists(data?data.store.phone.ext:undefined) %> wouldn't

the reference error happens on the evalutation of the data.MEH.FOO.BAR <-- it the hash map lookups eg the '.'s, it happens before it's put into the exists function

you could actually wrap the thing in a try catch as well

hope that clears up the issue :)

ant

Anton Khlynovskiy

@jdalton

The problem I ran into was devs wanting to access global variables.

<crockford-mode>Why should anyone ever want this? This is obviously a nonsense.</crockford-mode>

Phil

I ran into the same problem, attempted to share the same identical template across 2 very similar but non identical models using value1 ? value1.value : value2 gives a reference error if value 1 is undefined, got around it by providing a defaults object and just used extend so

var defaultObject = { value1 : false };
_.template($.extend(defaultObject, inputModel));

I'm sure you could provide whatever defaults are most appropriate

djkmiles

Avoided by using _.templateSettings.variable = "rc"; then prefixing with that chosen variable prefix of rc.

_.templateSettings.variable = "rc";
_.template('<%= rc.foo %>', {});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.