Skip to content
New issue

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

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for the block form of {{#unbound}} #1457

Closed
wants to merge 8 commits into from
40 changes: 39 additions & 1 deletion packages/ember-handlebars/lib/ext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
95 changes: 92 additions & 3 deletions packages/ember-handlebars/lib/helpers/unbound.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,101 @@ var handlebarsGet = Ember.Handlebars.get;
<div>{{unbound somePropertyThatDoesntChange}}</div>
```

If you call `unbound` with a block, it will unbind all the outputs in the
block:

``` handlebars
<tr>
{{#unbound}}
<td>{{firstName}}</td>
<td>{{lastName}}</td>
<td>{{age}}</td>
{{/unbound}}
</tr>
```

@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
<div>
{{unboundIf somePropertyThatDoesntChange}}
Hi! I won't go away even if the expression becomes false!
{{/unboundIf}}
</div>
```
@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
<div>
{{unboundUnless somePropertyThatDoesntChange}}
Hi! I won't go away even if the expression becomes true!
{{/unboundUnless}}
</div>
```
@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);
});

134 changes: 134 additions & 0 deletions packages/ember-handlebars/tests/helpers/unbound_test.js
Original file line number Diff line number Diff line change
@@ -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!");
});