Permalink
Browse files

Handlebars: allow dots on helpers, methods, and undefineds

  • Loading branch information...
1 parent 6049771 commit 021cca9ca3db2d3f9a61012c36515c5611c50916 @dgreensp dgreensp committed Jun 16, 2012
Showing with 205 additions and 24 deletions.
  1. +53 −24 packages/handlebars/evaluate.js
  2. +46 −0 packages/templating/templating_tests.html
  3. +106 −0 packages/templating/templating_tests.js
@@ -95,40 +95,69 @@ Handlebars.evaluate = function (ast, data, options) {
var eval_value = function (stack, id) {
if (typeof(id) !== "object")
return id;
- if (id.length === 2 && id[0] === 0 && (id[1] in helpers))
- return helpers[id[1]]; // found helper
+
+ var helperThis = stack.data;
+
+ // follow '..' in {{../../foo.bar}}
for (var i = 0; i < id[0]; i++) {
if (!stack.parent)
throw new Error("Too many '..' segments");
else
stack = stack.parent;
}
- var ret = stack.data;
- if (id.length > 1 && typeof ret !== 'object') {
- // Fail with better error than "can't read property of undefined".
- // Looking up id[1] as a property will fail, because
- // there is no data context object. Probably the developer
- // intended to use a helper that doesn't exist.
- if (typeof (function() {})[id[1]] !== 'undefined') {
- // An even more specific case for a helpful error.
- // The developer probably tried to name a helper 'name',
- // 'length', or some other built-in function property.
- // Assignments to these properties are no-ops, so the
- // helper declaration is undetectable.
- // We can't always catch this mistake, because if there is any
- // object as data context, it's legal for the developer to
- // ask for {{name}} as a property of the object, perhaps an
- // optional one. But if there is no data context, we get
- // to be helpful.
+
+ if (id.length === 1)
+ // no name: {{this}}, {{..}}, {{../..}}
+ return stack.data;
+
+ var data;
+ if (id[0] === 0 && (id[1] in helpers)) {
+ // first path segment is a helper
+ data = helpers[id[1]];
+ } else {
+ if ((! data instanceof Object) &&
+ (typeof (function() {})[id[1]] !== 'undefined')) {
+ // Give a helpful error message if the user tried to name
+ // a helper 'name', 'length', or some other built-in property
+ // of function objects. Unfortunately, this case is very
+ // hard to detect, as Template.foo.name = ... will fail silently,
+ // and {{name}} will be silently empty if the property doesn't
+ // exist (per Handlebars rules).
+ // However, if there is no data context at all, we jump in.
throw new Error("Can't call a helper '"+id[1]+"' because "+
"it is a built-in function property in JavaScript");
}
- throw new Error("Unknown helper '"+id[1]+"'");
+ // first path segment is property of data context
+ data = (stack.data && stack.data[id[1]]);
}
- for (var i = 1; i < id.length; i++)
- // XXX error (and/or unknown key) handling
- ret = ret[id[i]];
- return ret;
+
+ // handle dots, as in {{foo.bar}}
+ for (var i = 2; i < id.length; i++) {
+ // Call functions when taking the dot, to support
+ // for example currentUser.name.
+ //
+ // In the case of {{foo.bar}}, we end up returning one of:
+ // - helpers.foo.bar
+ // - helpers.foo().bar
+ // - stack.data.foo.bar
+ // - stack.data.foo().bar.
+ //
+ // The caller does the final application with any
+ // arguments, as in {{foo.bar arg1 arg2}}, and passes
+ // the current data context in `this`. Therefore,
+ // we use the current data context (`helperThis`)
+ // for all function calls.
+ if (typeof data === 'function')
+ data = data.call(helperThis);
+ else if (data === undefined || data === null)
+ // Handlebars fails silently and returns "" if
+ // we start to access properties that don't exist.
+ data = '';
+
+ data = data[id[i]];
+ }
+
+ return data;
};
// 'extra' will be clobbered, but not 'params'
@@ -111,3 +111,49 @@
{{fooprop}} {{{fooprop}}} {{barprop}} {{{barprop}}}
{{#fooprop}}{{/fooprop}} {{#barprop}}{{/barprop}}
</template>
+
+<template name="test_helpers_a">
+ platypus={{platypus}}
+ watermelon={{watermelon}}
+ daisy={{daisy}}
+ tree={{tree}}
+ warthog={{warthog}}
+</template>
+
+<template name="test_helpers_b">
+ unknown={{unknown}}
+ zero={{zero}}
+</template>
+
+<template name="test_helpers_c">
+ platypus.X={{platypus.X}}
+ watermelon.X={{watermelon.X}}
+ daisy.X={{daisy.X}}
+ tree.X={{tree.X}}
+ warthog.X={{warthog.X}}
+</template>
+
+<template name="test_helpers_d">
+ daisygetter={{daisygetter}}
+ thisTest={{thisTest}}
+</template>
+
+<template name="test_helpers_e">
+ fancy.foo={{fancy.foo}}
+ fancy.apple.banana={{fancy.apple.banana}}
+ fancy.currentFruit={{fancy.currentFruit}}
+ fancy.currentCountry.name={{fancy.currentCountry.name}}
+ fancy.currentCountry.population={{fancy.currentCountry.population}}
+ fancy.currentCountry.unicorns={{fancy.currentCountry.unicorns}}
+ fancy.currentCountry.daisyGetter={{fancy.currentCountry.daisyGetter}}
+</template>
+
+<template name="test_helpers_f">
+ fancyhelper.foo={{fancyhelper.foo}}
+ fancyhelper.apple.banana={{fancyhelper.apple.banana}}
+ fancyhelper.currentFruit={{fancyhelper.currentFruit}}
+ fancyhelper.currentCountry.name={{fancyhelper.currentCountry.name}}
+ fancyhelper.currentCountry.population={{fancyhelper.currentCountry.population}}
+ fancyhelper.currentCountry.unicorns={{fancyhelper.currentCountry.unicorns}}
+ fancyhelper.currentCountry.daisyGetter={{fancyhelper.currentCountry.daisyGetter}}
+</template>
@@ -126,3 +126,109 @@ Tinytest.add("templating - safestring", function(test) {
"1&lt;2 1<2 3<4 3<4 1<2 3<4");
});
+
+Tinytest.add("templating - helpers and dots", function(test) {
+ Handlebars.registerHelper("platypus", function() {
+ return "eggs";
+ });
+ Handlebars.registerHelper("watermelon", function() {
+ return "seeds";
+ });
+
+ Handlebars.registerHelper("daisygetter", function() {
+ return this.daisy;
+ });
+
+ var getFancyObject = function() {
+ return {
+ foo: 'bar',
+ apple: {banana: 'smoothie'},
+ currentFruit: function() {
+ return 'guava';
+ },
+ currentCountry: function() {
+ return {name: 'Iceland',
+ population: function() {
+ return 321007;
+ },
+ unicorns: 0, // falsy value
+ daisyGetter: function() {
+ return this.daisy;
+ }
+ };
+ }
+ };
+ };
+
+ Handlebars.registerHelper("fancyhelper", getFancyObject);
+
+ Template.test_helpers_a.platypus = 'bill';
+ Template.test_helpers_a.warthog = function() {
+ return 'snout';
+ };
+
+ var dataObj = {
+ zero: 0,
+ platypus: 'weird',
+ watermelon: 'rind',
+ daisy: 'petal',
+ tree: function() { return 'leaf'; },
+ thisTest: function() { return this.tree(); },
+ fancy: getFancyObject()
+ };
+
+ test.equal(Template.test_helpers_a(dataObj).match(/\S+/g), [
+ 'platypus=bill', // helpers on Template object take first priority
+ 'watermelon=seeds', // global helpers take second priority
+ 'daisy=petal', // unshadowed object property
+ 'tree=leaf', // function object property
+ 'warthog=snout' // function Template property
+ ]);
+
+ test.equal(Template.test_helpers_b(dataObj).match(/\S+/g), [
+ // unknown properties silently fail
+ 'unknown=',
+ // falsy property comes through
+ 'zero=0'
+ ]);
+
+ test.equal(Template.test_helpers_c(dataObj).match(/\S+/g), [
+ // property gets are supposed to silently fail
+ 'platypus.X=',
+ 'watermelon.X=',
+ 'daisy.X=',
+ 'tree.X=',
+ 'warthog.X='
+ ]);
+
+ test.equal(Template.test_helpers_d(dataObj).match(/\S+/g), [
+ // helpers should get current data context in `this`
+ 'daisygetter=petal',
+ // object methods should get current data context in `this`
+ 'thisTest=leaf'
+ ]);
+
+ test.equal(Template.test_helpers_e(dataObj).match(/\S+/g), [
+ 'fancy.foo=bar',
+ 'fancy.apple.banana=smoothie',
+ 'fancy.currentFruit=guava',
+ 'fancy.currentCountry.name=Iceland',
+ 'fancy.currentCountry.population=321007',
+ 'fancy.currentCountry.unicorns=0',
+ // all functions receive the current data context in `this`,
+ // for consistency
+ 'fancy.currentCountry.daisyGetter=petal'
+ ]);
+
+ test.equal(Template.test_helpers_f(dataObj).match(/\S+/g), [
+ 'fancyhelper.foo=bar',
+ 'fancyhelper.apple.banana=smoothie',
+ 'fancyhelper.currentFruit=guava',
+ 'fancyhelper.currentCountry.name=Iceland',
+ 'fancyhelper.currentCountry.population=321007',
+ 'fancyhelper.currentCountry.unicorns=0',
+ // all functions receive the current data context in `this`,
+ // for consistency
+ 'fancyhelper.currentCountry.daisyGetter=petal'
+ ]);
+});

0 comments on commit 021cca9

Please sign in to comment.