Skip to content

Commit

Permalink
Add support for ternary operator in classNames
Browse files Browse the repository at this point in the history
This commit extends bindings to class names by adding support for falsy class names using the ternary operator as following:

Ember.View.create({
  classNameBindings: ['isEnabled?enabled:disabled']
  isEnabled: true
});

This will add the class 'enabled' when isEnabled is true. If isEnabled is false the class 'disabled' is added instead.
  • Loading branch information
pangratz committed May 14, 2012
1 parent 4ef1568 commit c9cf4ab
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 73 deletions.
34 changes: 2 additions & 32 deletions packages/ember-handlebars/lib/helpers/binding.js
Expand Up @@ -342,37 +342,7 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
// determine which class string to return, based on whether it is
// a Boolean or not.
var classStringForProperty = function(property) {
var split = property.split(':'),
className = split[1];

property = split[0];

var val = property !== '' ? getPath(context, property, options) : true;

// If the value is truthy and we're using the colon syntax,
// we should return the className directly
if (!!val && className) {
return className;

// If value is a Boolean and true, return the dasherized property
// name.
} else if (val === true) {
// Normalize property path to be suitable for use
// as a class name. For exaple, content.foo.barBaz
// becomes bar-baz.
var parts = property.split('.');
return Ember.String.dasherize(parts[parts.length-1]);

// If the value is not false, undefined, or null, return the current
// value of the property.
} else if (val !== false && val !== undefined && val !== null) {
return val;

// Nothing to display. Return null so that the old class is removed
// but no new class is added.
} else {
return null;
}
return Ember.View.classStringForProperty(context, property);
};

// For each property passed, loop through and setup
Expand Down Expand Up @@ -420,7 +390,7 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
Ember.run.once(observer);
};

var property = binding.split(':')[0];
var property = binding.split(/\?|:/)[0];
if (property !== '') {
Ember.addObserver(context, property, invoker);
}
Expand Down
34 changes: 32 additions & 2 deletions packages/ember-handlebars/tests/handlebars_test.js
Expand Up @@ -1385,6 +1385,30 @@ test("should be able to bind class attribute via a truthy property with {{bindAt
equal(view.$('.is-truthy').length, 0, "removes class name if bound property is set to something non-truthy");
});

test("should be able to bind class attribute using ternary operator in {{bindAttr}}", function() {
var template = Ember.Handlebars.compile('<img {{bindAttr class="content.isDisabled?disabled:enabled"}} />');
var content = Ember.Object.create({
isDisabled: true
});

view = Ember.View.create({
template: template,
content: content
});

appendView();

ok(view.$('img').hasClass('disabled'), 'disabled class is rendered');
ok(!view.$('img').hasClass('enabled'), 'enabled class is not rendered');

Ember.run(function() {
set(content, 'isDisabled', false);
});

ok(!view.$('img').hasClass('disabled'), 'disabled class is not rendered');
ok(view.$('img').hasClass('enabled'), 'enabled class is rendered');
});

test("should not allow XSS injection via {{bindAttr}} with class", function() {
view = Ember.View.create({
template: Ember.Handlebars.compile('<img {{bindAttr class="foo"}}>'),
Expand Down Expand Up @@ -1425,11 +1449,12 @@ test("should be able to bind boolean element attributes using {{bindAttr}}", fun
});

test("should be able to add multiple classes using {{bindAttr class}}", function() {
var template = Ember.Handlebars.compile('<div {{bindAttr class="content.isAwesomeSauce content.isAlsoCool content.isAmazing:amazing :is-super-duper"}}></div>');
var template = Ember.Handlebars.compile('<div {{bindAttr class="content.isAwesomeSauce content.isAlsoCool content.isAmazing:amazing :is-super-duper content.isEnabled?enabled:disabled"}}></div>');
var content = Ember.Object.create({
isAwesomeSauce: true,
isAlsoCool: true,
isAmazing: true
isAmazing: true,
isEnabled: true
});

view = Ember.View.create({
Expand All @@ -1443,15 +1468,20 @@ test("should be able to add multiple classes using {{bindAttr class}}", function
ok(view.$('div').hasClass('is-also-cool'), "dasherizes second property and sets classname");
ok(view.$('div').hasClass('amazing'), "uses alias for third property and sets classname");
ok(view.$('div').hasClass('is-super-duper'), "static class is present");
ok(view.$('div').hasClass('enabled'), "truthy class in ternary classname definition is rendered");
ok(!view.$('div').hasClass('disabled'), "falsy class in ternary classname definition is not rendered");

Ember.run(function() {
set(content, 'isAwesomeSauce', false);
set(content, 'isAmazing', false);
set(content, 'isEnabled', false);
});

ok(!view.$('div').hasClass('is-awesome-sauce'), "removes dasherized class when property is set to false");
ok(!view.$('div').hasClass('amazing'), "removes aliased class when property is set to false");
ok(view.$('div').hasClass('is-super-duper'), "static class is still present");
ok(!view.$('div').hasClass('enabled'), "truthy class in ternary classname definition is not rendered");
ok(view.$('div').hasClass('disabled'), "falsy class in ternary classname definition is rendered");
});

test("should be able to bindAttr to 'this' in an {{#each}} block", function() {
Expand Down
106 changes: 70 additions & 36 deletions packages/ember-views/lib/views/view.js
Expand Up @@ -915,7 +915,7 @@ Ember.View = Ember.Object.extend(Ember.Evented,
}

// Extract just the property name from bindings like 'foo:bar'
property = binding.split(':')[0];
property = binding.split(/\?|:/)[0];
addObserver(this, property, observer);
}, this);
},
Expand Down Expand Up @@ -965,41 +965,7 @@ Ember.View = Ember.Object.extend(Ember.Evented,
passing `isUrgent` to this method will return `"is-urgent"`.
*/
_classStringForProperty: function(property) {
var split = property.split(':'),
className = split[1];

property = split[0];

// TODO: Remove this `false` when the `getPath` globals support is removed
var val = Ember.getPath(this, property, false);
if (val === undefined && Ember.isGlobalPath(property)) {
val = Ember.getPath(window, property);
}

// If the value is truthy and we're using the colon syntax,
// we should return the className directly
if (!!val && className) {
return className;

// If value is a Boolean and true, return the dasherized property
// name.
} else if (val === true) {
// Normalize property path to be suitable for use
// as a class name. For exaple, content.foo.barBaz
// becomes bar-baz.
var parts = property.split('.');
return Ember.String.dasherize(parts[parts.length-1]);

// If the value is not false, undefined, or null, return the current
// value of the property.
} else if (val !== false && val !== undefined && val !== null) {
return val;

// Nothing to display. Return null so that the old class is removed
// but no new class is added.
} else {
return null;
}
return Ember.View.classStringForProperty(this, property);
},

// ..........................................................
Expand Down Expand Up @@ -1559,6 +1525,16 @@ Ember.View = Ember.Object.extend(Ember.Evented,
isUrgent: true
});
If you want to add a class name for a property which evaluates to true and
and a different class name if it evaluates to false, you can pass a binding
like this:
// Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false
Ember.View.create({
classNameBindings: ['isEnabled?enabled:disabled']
isEnabled: true
});
This list of properties is inherited from the view's superclasses as well.
@type Array
Expand Down Expand Up @@ -1949,6 +1925,64 @@ Ember.View.reopen({
domManager: DOMManager
});

Ember.View.reopenClass({

/**
Given a property name, returns a dasherized version of that
property name if the property evaluates to a non-falsy value.
For example, if the view has property `isUrgent` that evaluates to true,
passing `isUrgent` to this method will return `"is-urgent"`.
*/
classStringForProperty: function(context, property) {
var split = property.split(/\?|:/),
className = split[1],
falsyClassName;

// check if the property is defined as prop:trueClass:falseClass
if (split.length === 3) {
falsyClassName = split[2];
}

property = split[0];

// TODO: Remove this `false` when the `getPath` globals support is removed
var val = Ember.getPath(context, property, false);
if (val === undefined && Ember.isGlobalPath(property)) {
val = Ember.getPath(window, property);
}

// If the value is truthy and we're using the colon syntax,
// we should return the className directly
if (!!val && className) {
return className;

// If value is a Boolean and true, return the dasherized property
// name.
} else if (val === true) {
// Normalize property path to be suitable for use
// as a class name. For exaple, content.foo.barBaz
// becomes bar-baz.
var parts = property.split('.');
return Ember.String.dasherize(parts[parts.length-1]);

// If the value is false and a falsyClassName is specified, return it
} else if (val === false && falsyClassName) {
return falsyClassName;

// If the value is not false, undefined, or null, return the current
// value of the property.
} else if (val !== false && val !== undefined && val !== null) {
return val;

// Nothing to display. Return null so that the old class is removed
// but no new class is added.
} else {
return null;
}
}
});

// Create a global view hash.
Ember.View.views = {};

Expand Down
16 changes: 13 additions & 3 deletions packages/ember-views/tests/views/view/class_name_bindings_test.js
Expand Up @@ -11,13 +11,15 @@ module("Ember.View - Class Name Bindings");
test("should apply bound class names to the element", function() {
var view = Ember.View.create({
classNameBindings: ['priority', 'isUrgent', 'isClassified:classified',
'canIgnore', 'messages.count', 'messages.resent:is-resent', 'isNumber:is-number'],
'canIgnore', 'messages.count', 'messages.resent:is-resent', 'isNumber:is-number',
'isEnabled?enabled:disabled'],

priority: 'high',
isUrgent: true,
isClassified: true,
canIgnore: false,
isNumber: 5,
isNumber: 5,
isEnabled: true,

messages: {
count: 'five-messages',
Expand All @@ -33,17 +35,21 @@ test("should apply bound class names to the element", function() {
ok(view.$().hasClass('is-resent'), "supports customing class name for paths");
ok(view.$().hasClass('is-number'), "supports colon syntax with truthy properties");
ok(!view.$().hasClass('can-ignore'), "does not add false Boolean values as class");
ok(view.$().hasClass('enabled'), "supports customizing class name for Boolean values with negation");
ok(!view.$().hasClass('disabled'), "does not add class name for negated binding");
});

test("should add, remove, or change class names if changed after element is created", function() {
var view = Ember.View.create({
classNameBindings: ['priority', 'isUrgent', 'isClassified:classified',
'canIgnore', 'messages.count', 'messages.resent:is-resent'],
'canIgnore', 'messages.count', 'messages.resent:is-resent',
'isEnabled?enabled:disabled'],

priority: 'high',
isUrgent: true,
isClassified: true,
canIgnore: false,
isEnabled: true,

messages: Ember.Object.create({
count: 'five-messages',
Expand All @@ -56,6 +62,7 @@ test("should add, remove, or change class names if changed after element is crea
set(view, 'priority', 'orange');
set(view, 'isUrgent', false);
set(view, 'canIgnore', true);
set(view, 'isEnabled', false);
setPath(view, 'messages.count', 'six-messages');
setPath(view, 'messages.resent', true );

Expand All @@ -69,6 +76,9 @@ test("should add, remove, or change class names if changed after element is crea
ok(!view.$().hasClass('five-messages'), "removes old value when path changes");

ok(view.$().hasClass('is-resent'), "adds customized class name when path changes");

ok(!view.$().hasClass('enabled'), "updates class name for negated binding");
ok(view.$().hasClass('disabled'), "adds negated class name for negated binding");
});

test("classNames should not be duplicated on rerender", function(){
Expand Down

0 comments on commit c9cf4ab

Please sign in to comment.