Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

_.template: allow undefined keys #237

Closed
mren opened this Issue Jul 2, 2011 · 25 comments

Comments

Projects
None yet

mren commented Jul 2, 2011

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.

Owner

jashkenas commented Jul 6, 2011

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.

@jashkenas jashkenas closed this Jul 6, 2011

dmitry commented Aug 11, 2011

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

Contributor

limeblack commented Aug 11, 2011

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.

Contributor

limeblack commented Aug 11, 2011

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;
Owner

jashkenas commented Aug 11, 2011

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.

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.

Contributor

limeblack commented Nov 6, 2011

@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.

adius commented Apr 13, 2013

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

Contributor

jdalton commented Apr 13, 2013

@aduis specify a variable option to avoid it

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

caseywebdev commented Apr 13, 2013

@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.

adius commented Apr 13, 2013

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

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

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");
Contributor

jdalton commented Jul 2, 2013

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.

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

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 
    }
}

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?

@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

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

Contributor

subzey commented Nov 14, 2013

@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>

JoryUK commented Nov 20, 2013

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 commented Jan 6, 2015

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

_.templateSettings.variable = "rc";
_.template('<%= rc.foo %>', {});

@gasp gasp referenced this issue in ingro/gulp-template-compile Mar 4, 2015

Closed

bumping up lodash to 3.3.0 allows to use the 'variable' option #7

Why doesn't _.undefined(foo) even work within templates? This is absurd

Contributor

jdalton commented Jul 14, 2015 edited

@andrewpurkett
There's a nice post on undeclared variable access over on MDN
and bit on with-statements and reference errors too.

@dciccale dciccale referenced this issue in dciccale/node-htmlprocessor Aug 13, 2016

Closed

Unfriendly error message for undefined variables #23

paztis commented Mar 2, 2017

It's an old subject, but you always can access all variables with arguments[0]

<%= arguments[0].foo %> works if foo is undefined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment