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!");
+});
+