diff --git a/packages/ember-handlebars/lib/ext.js b/packages/ember-handlebars/lib/ext.js index 1886a56cd8b..7309be0f20a 100644 --- a/packages/ember-handlebars/lib/ext.js +++ b/packages/ember-handlebars/lib/ext.js @@ -85,6 +85,41 @@ 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) { + + 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 || {}); + if (block.mustache.id.string === "unbound") { + var originalValue = emberOptions.insideUnboundBlock; + emberOptions.insideUnboundBlock = true; + var result = Handlebars.Compiler.prototype.block.call(this, block); + emberOptions.insideUnboundBlock = originalValue; + return result; + } + + // Substitute unboundIf/unboundUnless for if/unless + 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"; + } + + return Handlebars.Compiler.prototype.block.call(this, block); +}; + /** @private @@ -100,7 +135,10 @@ 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 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 diff --git a/packages/ember-handlebars/lib/helpers/unbound.js b/packages/ember-handlebars/lib/helpers/unbound.js index 4c3632ce67d..8c2f8e2848e 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 (handlebarsGet(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 (handlebarsGet(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..13d6c90ff7f --- /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}}{{view.occasion}}: {{#unless view.notSpooky}}spooky! {{/unless}}{{#if view.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}}{{view.occasion}}: {{#unless view.notSpooky}}spooky! {{/unless}}{{#if view.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!"); +}); +