From 6ad634b92d7d000ca42c0876800c6f7e24cba96b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 16 Oct 2012 12:20:09 -0400 Subject: [PATCH 1/6] Support for the block form of {{#unbound}}. None of the output within the block will be bound. Example: {{#unbound}} {{firstName}}, {{age}} {{/unbound}} Is equivalent to: {{unbound firstName}}, {{unbound age}} --- packages/ember-handlebars/lib/ext.js | 35 ++++- .../ember-handlebars/lib/helpers/unbound.js | 95 ++++++++++++- .../tests/helpers/unbound_test.js | 134 ++++++++++++++++++ 3 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 packages/ember-handlebars/tests/helpers/unbound_test.js diff --git a/packages/ember-handlebars/lib/ext.js b/packages/ember-handlebars/lib/ext.js index 1886a56cd8b..b962b4f1558 100644 --- a/packages/ember-handlebars/lib/ext.js +++ b/packages/ember-handlebars/lib/ext.js @@ -85,6 +85,37 @@ Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) return "data.buffer.push("+string+");"; }; +/** + @private + + Supports re-writing {{property}} as {{unbound property}} when within a + {{#unbound}} block. Also will re-write if/unless as unboundIf/unboundUnless. + + @method block + @for Ember.Handlebars.Compiler + @param block +*/ +Ember.Handlebars.Compiler.prototype.block = function(block) { + + // If we have an {{unbound}} block, set the option so nested output can + // be automatically unbound. + if (block.mustache.id.string === "unbound") { + var originalValue = this.options.insideUnboundBlock; + this.options.insideUnboundBlock = true; + var result = Handlebars.Compiler.prototype.block.call(this, block); + this.options.insideUnboundBlock = originalValue; + return result; + } + + // Substitute unboundIf/unboundUnless for if/unless + if (this.options.insideUnboundBlock) { + if (block.mustache.id.string === "if") block.mustache.id.parts[0] = "unboundIf"; + if (block.mustache.id.string === "unless") block.mustache.id.parts[0] = "unboundUnless"; + } + + return Handlebars.Compiler.prototype.block.call(this, block); +}; + /** @private @@ -100,7 +131,9 @@ Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { if (mustache.params.length || mustache.hash) { return Handlebars.Compiler.prototype.mustache.call(this, mustache); } else { - var id = new Handlebars.AST.IdNode(['_triageMustache']); + // If we're inside a {{unbound}}, rewrite the output to be {{unbound foo}}. Otherwise, + // set it up for the triage. + var id = new Handlebars.AST.IdNode(this.options.insideUnboundBlock ? ['unbound'] : ['_triageMustache']); // Update the mustache node to include a hash value indicating whether the original node // was escaped. This will allow us to properly escape values when the underlying value diff --git a/packages/ember-handlebars/lib/helpers/unbound.js b/packages/ember-handlebars/lib/helpers/unbound.js index 4c3632ce67d..3d3e82a3c25 100644 --- a/packages/ember-handlebars/lib/helpers/unbound.js +++ b/packages/ember-handlebars/lib/helpers/unbound.js @@ -17,12 +17,101 @@ var handlebarsGet = Ember.Handlebars.get;
{{unbound somePropertyThatDoesntChange}}
``` + If you call `unbound` with a block, it will unbind all the outputs in the + block: + + ``` handlebars + + {{#unbound}} + {{firstName}} + {{lastName}} + {{age}} + {{/unbound}} + + ``` + @method unbound @for Ember.Handlebars.helpers @param {String} property @return {String} HTML string */ -Ember.Handlebars.registerHelper('unbound', function(property, fn) { - var context = (fn.contexts && fn.contexts[0]) || this; - return handlebarsGet(context, property, fn); +Ember.Handlebars.registerHelper('unbound', function() { + var context; + + // Are we outputting a property? + if (arguments.length === 2) { + var property = arguments[0], fn = arguments[1]; + context = (fn.contexts && fn.contexts[0]) || this; + return handlebarsGet(context, property, fn); + } + + // Otherwise we are being called with a block: + var options = arguments[0]; + context = (options.contexts && options.contexts[0]) || this; + return options.fn(context); }); + + +/** + `unboundIf` allows you to evaluate a conditional expression without + creating a binding. *Important:* The conditional will not be re-evaluated if + the property changes. Use with caution. + + ``` handlebars +
+ {{unboundIf somePropertyThatDoesntChange}} + Hi! I won't go away even if the expression becomes false! + {{/unboundIf}} +
+ ``` + @method unboundIf + @for Ember.Handlebars.helpers + @param {String} property to test + @param {Hash} options + @return {String} HTML string +*/ +Ember.Handlebars.registerHelper('unboundIf', function(property, options) { + Ember.assert("You must pass exactly one argument to the unboundIf helper", arguments.length === 2); + Ember.assert("You must pass a block to the unboundIf helper", options.fn && options.fn !== Handlebars.VM.noop); + + var context = (options.contexts && options.contexts[0]) || this; + var normalized = Ember.Handlebars.normalizePath(context, property, options.data); + + if (Ember.get(normalized.root,normalized.path,options)) + return options.fn(context,property); + else + return options.inverse(context,property); + +}); + +/** + `unboundUnless` allows you to evaluate the opposite of a conditional expression + without creating a binding. *Important:* The conditional will not be re-evaluated + if the property changes. Use with caution. + + ``` handlebars +
+ {{unboundUnless somePropertyThatDoesntChange}} + Hi! I won't go away even if the expression becomes true! + {{/unboundUnless}} +
+ ``` + @method unboundUnless + @for Ember.Handlebars.helpers + @param {String} property to test + @param {Hash} options + @return {String} HTML string +*/ +Ember.Handlebars.registerHelper('unboundUnless', function(property, options) { + Ember.assert("You must pass exactly one argument to the unboundUnless helper", arguments.length === 2); + Ember.assert("You must pass a block to the unboundUnless helper", options.fn && options.fn !== Handlebars.VM.noop); + + var context = (options.contexts && options.contexts[0]) || this; + var normalized = Ember.Handlebars.normalizePath(context, property, options.data); + + if (Ember.get(normalized.root,normalized.path,options)) + return options.inverse(context,property); + else + return options.fn(context,property); +}); + diff --git a/packages/ember-handlebars/tests/helpers/unbound_test.js b/packages/ember-handlebars/tests/helpers/unbound_test.js new file mode 100644 index 00000000000..8e278605bdc --- /dev/null +++ b/packages/ember-handlebars/tests/helpers/unbound_test.js @@ -0,0 +1,134 @@ +var appendView = function(view) { + Ember.run(function() { view.appendTo('#qunit-fixture'); }); +}; + +var compile = function(template) { + return Ember.Handlebars.compile(template); +}; + +var view; + +module("Handlebars {{unbound}} helpers", { + teardown: function() { + Ember.run(function () { + if (view) { + view.destroy(); + } + }); + } +}); + +module("{{unbound}}"); +test("unbound should output the property", function() { + view = Ember.View.create({ + mrStubborn: 'NO!', + template: Ember.Handlebars.compile("Do you like anything? {{unbound view.mrStubborn}}") + }); + appendView(view); + equal(view.$().text(), "Do you like anything? NO!"); +}); + +test("property will not update when unbound", function() { + view = Ember.View.create({ + mrStubborn: 'NO!', + template: Ember.Handlebars.compile("Do you like anything? {{unbound view.mrStubborn}}") + }); + appendView(view); + + Ember.run(function () { + view.set('mrStubborn', 'YES!'); + }); + + equal(view.$().text(), "Do you like anything? NO!"); +}); + +module("{{unboundIf}}"); +test("unboundIf should output if the condition is true", function() { + view = Ember.View.create({ + cool: true, + template: Ember.Handlebars.compile("{{#unboundIf view.cool}}sgb{{/unboundIf}}") + }); + appendView(view); + equal(view.$().text(), "sgb"); +}); + +test("unboundIf doesn't output if the condition is false", function() { + view = Ember.View.create({ + cool: false, + template: Ember.Handlebars.compile("{{#unboundIf view.cool}}sgb{{/unboundIf}}") + }); + appendView(view); + equal(view.$().text(), ""); +}); + +test("unboundIf doesn't output again if the condition changes", function() { + view = Ember.View.create({ + cool: true, + template: Ember.Handlebars.compile("{{#unboundIf view.cool}}sgb{{/unboundIf}}") + }); + appendView(view); + Ember.run(function () { + view.set('cool', false); + }); + equal(view.$().text(), "sgb"); +}); + +module("{{unboundUnless}}"); +test("unboundUnless should output if the condition is false", function() { + view = Ember.View.create({ + cool: false, + template: Ember.Handlebars.compile("{{#unboundUnless view.cool}}sbb{{/unboundUnless}}") + }); + appendView(view); + equal(view.$().text(), "sbb"); +}); + +test("unboundUnless doesn't output if the condition is false", function() { + view = Ember.View.create({ + cool: true, + template: Ember.Handlebars.compile("{{#unboundUnless view.cool}}sbb{{/unboundUnless}}") + }); + appendView(view); + equal(view.$().text(), ""); +}); + +test("unboundUnless doesn't output again if the condition changes", function() { + view = Ember.View.create({ + cool: false, + template: Ember.Handlebars.compile("{{#unboundUnless view.cool}}sbb{{/unboundUnless}}") + }); + appendView(view); + Ember.run(function () { + view.set('cool', true); + }); + equal(view.$().text(), "sbb"); +}); + + +test("unbound all output within a block", function() { + view = Ember.View.create({ + occasion: 'bar mitzvah', + notSpooky: false, + werewolf: true, + template: Ember.Handlebars.compile("{{#unbound}}{{occasion}}: {{#unless notSpooky}}spooky! {{/unless}}{{#if werewolf}}scary!{{/if}}{{/unbound}}") + }); + appendView(view); + equal(view.$().text(), "bar mitzvah: spooky! scary!"); +}); + +test("unbound blocks don't change", function() { + view = Ember.View.create({ + occasion: 'bar mitzvah', + notSpooky: false, + werewolf: true, + template: Ember.Handlebars.compile("{{#unbound}}{{occasion}}: {{#unless notSpooky}}spooky! {{/unless}}{{#if werewolf}}scary!{{/if}}{{/unbound}}") + }); + appendView(view); + Ember.run(function () { + view.set('occasion', 'wedding'); + view.set('werewolf', false); + view.set('notSpooky', true); + }); + equal(view.$().text(), "bar mitzvah: spooky! scary!"); +}); + From 8d872506301601fde82721d33bc19202c75acc35 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 16 Oct 2012 17:44:17 -0400 Subject: [PATCH 2/6] {{#unboundIf}} and {{#unboundUnless}} should use Ember.Handlebars.getPath --- packages/ember-handlebars/lib/helpers/unbound.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ember-handlebars/lib/helpers/unbound.js b/packages/ember-handlebars/lib/helpers/unbound.js index 3d3e82a3c25..a4b76cc2aaf 100644 --- a/packages/ember-handlebars/lib/helpers/unbound.js +++ b/packages/ember-handlebars/lib/helpers/unbound.js @@ -77,7 +77,7 @@ Ember.Handlebars.registerHelper('unboundIf', function(property, options) { var context = (options.contexts && options.contexts[0]) || this; var normalized = Ember.Handlebars.normalizePath(context, property, options.data); - if (Ember.get(normalized.root,normalized.path,options)) + if (Ember.Handlebars.getPath(normalized.root,normalized.path,options)) return options.fn(context,property); else return options.inverse(context,property); @@ -109,7 +109,7 @@ Ember.Handlebars.registerHelper('unboundUnless', function(property, options) { var context = (options.contexts && options.contexts[0]) || this; var normalized = Ember.Handlebars.normalizePath(context, property, options.data); - if (Ember.get(normalized.root,normalized.path,options)) + if (Ember.Handlebars.getPath(normalized.root,normalized.path,options)) return options.inverse(context,property); else return options.fn(context,property); From 3494362ddad506582e7c3d8f6de19e18c2e6cbe2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 29 Oct 2012 10:48:48 -0400 Subject: [PATCH 3/6] Put insideUnboundBlock into options.ember --- packages/ember-handlebars/lib/ext.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ember-handlebars/lib/ext.js b/packages/ember-handlebars/lib/ext.js index b962b4f1558..81a4a06de78 100644 --- a/packages/ember-handlebars/lib/ext.js +++ b/packages/ember-handlebars/lib/ext.js @@ -99,16 +99,17 @@ Ember.Handlebars.Compiler.prototype.block = function(block) { // If we have an {{unbound}} block, set the option so nested output can // be automatically unbound. + var emberOptions = this.options.ember = (this.options.ember || {}); if (block.mustache.id.string === "unbound") { - var originalValue = this.options.insideUnboundBlock; - this.options.insideUnboundBlock = true; + var originalValue = emberOptions.insideUnboundBlock; + emberOptions.insideUnboundBlock = true; var result = Handlebars.Compiler.prototype.block.call(this, block); - this.options.insideUnboundBlock = originalValue; + emberOptions.insideUnboundBlock = originalValue; return result; } // Substitute unboundIf/unboundUnless for if/unless - if (this.options.insideUnboundBlock) { + if (emberOptions.insideUnboundBlock) { if (block.mustache.id.string === "if") block.mustache.id.parts[0] = "unboundIf"; if (block.mustache.id.string === "unless") block.mustache.id.parts[0] = "unboundUnless"; } @@ -132,8 +133,9 @@ Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { return Handlebars.Compiler.prototype.mustache.call(this, mustache); } else { // If we're inside a {{unbound}}, rewrite the output to be {{unbound foo}}. Otherwise, - // set it up for the triage. - var id = new Handlebars.AST.IdNode(this.options.insideUnboundBlock ? ['unbound'] : ['_triageMustache']); + // set it up for the triage.\ + var insideUnboundBlock = this.options.ember && this.options.ember.insideUnboundBlock; + var id = new Handlebars.AST.IdNode(insideUnboundBlock ? ['unbound'] : ['_triageMustache']); // Update the mustache node to include a hash value indicating whether the original node // was escaped. This will allow us to properly escape values when the underlying value From cb78e5a29f6eb37e5add19a7c9f5977ce39a9e37 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 29 Oct 2012 10:59:52 -0400 Subject: [PATCH 4/6] Asset the parameter to the Compiler block prototype. --- packages/ember-handlebars/lib/ext.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ember-handlebars/lib/ext.js b/packages/ember-handlebars/lib/ext.js index 81a4a06de78..7309be0f20a 100644 --- a/packages/ember-handlebars/lib/ext.js +++ b/packages/ember-handlebars/lib/ext.js @@ -97,6 +97,9 @@ Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) */ Ember.Handlebars.Compiler.prototype.block = function(block) { + Ember.assert("You must pass exactly one argument to the block prototype", arguments.length === 1 ); + Ember.assert("You must pass a block", block.type === 'block' ); + // If we have an {{unbound}} block, set the option so nested output can // be automatically unbound. var emberOptions = this.options.ember = (this.options.ember || {}); From 63f4abb7e4556cda8b952193f996454ab1fe610c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 29 Oct 2012 15:14:27 -0400 Subject: [PATCH 5/6] Replace Handlebars.getPath with Handebars.get --- packages/ember-handlebars/lib/helpers/unbound.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ember-handlebars/lib/helpers/unbound.js b/packages/ember-handlebars/lib/helpers/unbound.js index a4b76cc2aaf..8c2f8e2848e 100644 --- a/packages/ember-handlebars/lib/helpers/unbound.js +++ b/packages/ember-handlebars/lib/helpers/unbound.js @@ -77,7 +77,7 @@ Ember.Handlebars.registerHelper('unboundIf', function(property, options) { var context = (options.contexts && options.contexts[0]) || this; var normalized = Ember.Handlebars.normalizePath(context, property, options.data); - if (Ember.Handlebars.getPath(normalized.root,normalized.path,options)) + if (handlebarsGet(normalized.root,normalized.path,options)) return options.fn(context,property); else return options.inverse(context,property); @@ -109,7 +109,7 @@ Ember.Handlebars.registerHelper('unboundUnless', function(property, options) { var context = (options.contexts && options.contexts[0]) || this; var normalized = Ember.Handlebars.normalizePath(context, property, options.data); - if (Ember.Handlebars.getPath(normalized.root,normalized.path,options)) + if (handlebarsGet(normalized.root,normalized.path,options)) return options.inverse(context,property); else return options.fn(context,property); From a5197e3bbd77aad2972d4dc6cc02989ed793437c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 26 Nov 2012 11:42:30 -0500 Subject: [PATCH 6/6] Fixed broken specs. --- packages/ember-handlebars/tests/helpers/unbound_test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ember-handlebars/tests/helpers/unbound_test.js b/packages/ember-handlebars/tests/helpers/unbound_test.js index 8e278605bdc..13d6c90ff7f 100644 --- a/packages/ember-handlebars/tests/helpers/unbound_test.js +++ b/packages/ember-handlebars/tests/helpers/unbound_test.js @@ -110,9 +110,9 @@ test("unbound all output within a block", function() { occasion: 'bar mitzvah', notSpooky: false, werewolf: true, - template: Ember.Handlebars.compile("{{#unbound}}{{occasion}}: {{#unless notSpooky}}spooky! {{/unless}}{{#if werewolf}}scary!{{/if}}{{/unbound}}") + template: Ember.Handlebars.compile("{{#unbound}}{{view.occasion}}: {{#unless view.notSpooky}}spooky! {{/unless}}{{#if view.werewolf}}scary!{{/if}}{{/unbound}}") }); - appendView(view); + appendView(view); equal(view.$().text(), "bar mitzvah: spooky! scary!"); }); @@ -121,7 +121,7 @@ test("unbound blocks don't change", function() { occasion: 'bar mitzvah', notSpooky: false, werewolf: true, - template: Ember.Handlebars.compile("{{#unbound}}{{occasion}}: {{#unless notSpooky}}spooky! {{/unless}}{{#if werewolf}}scary!{{/if}}{{/unbound}}") + template: Ember.Handlebars.compile("{{#unbound}}{{view.occasion}}: {{#unless view.notSpooky}}spooky! {{/unless}}{{#if view.werewolf}}scary!{{/if}}{{/unbound}}") }); appendView(view); Ember.run(function () {