Skip to content
This repository

Support :: syntax in classNameBindings of Ember.View #732

Merged
merged 13 commits into from over 1 year ago

10 participants

Clemens Müller Don't Add Me To Your Organization a.k.a The Travis Bot Dan Gebhardt Peter Wagenet Devin Torres Karim Nassar Erik Bryn Sylvain MINA Albert-Jan Nijburg sherwinyu
Clemens Müller

This commit extends bindings to class names by adding support for falsy class names by using the double colon* syntax 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.

*Inspired by recent semicolon discussion

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged b4c784b4 into 3c02542).

Dan Gebhardt
dgeb commented April 24, 2012

Seems like a useful concept. What do you think of the syntax isEnabled?enabled:disabled to show that it's acting as a ternary statement? I suppose isEnabled?enabled would also need to be supported for consistency.

Clemens Müller

@dgeb Good point! I like your syntax better.

Peter Wagenet
Owner

@pangratz This also needs to be added to the Handlebars helpers as well. However, we should make sure this is something we do want to add before you go to the trouble to add it there also.

Clemens Müller

@wagenet thanks for the hint.

Clemens Müller

Just a note: there should be a single method which returns the class name for a property and this should be used in both places to DRY: binding.js and view.js

Peter Wagenet
Owner

@pangratz Would you like to DRY this up?

Clemens Müller

I'll give it a DRY. Uhh, Oscar for bad pun, here I come..

Devin Torres

isFoo?foo:bar also has the unique advantage of being the JS ternary operator.

Devin Torres

Clever.

Karim Nassar

I think the more flexible solution would be to allow attr/class bindings to a non-boolean computed property, with the return value of the property being used as the attribute value. Something like:

Ember.View.create({
  classNameBindings: ['elementClasses::'],
  isEnabled: true,
  elementClasses: Ember.computed(function() {
    if (this.get('isEnabled') === true) {
       return "enabled";
    } else if (this.get('isEnabled') === false) {
       return "disabled";
    } else {
       return "";
    }
  }).property('isEnabled')
});
Dan Gebhardt
dgeb commented April 26, 2012

@knassar - Class names can currently be bound to computed properties. Here's an example I modified: http://jsfiddle.net/dgeb/hscQV/

I think the advantage of @pangratz's ternary solution is its brevity. Plus, it can be used from either the view class or the handlebars template (although #593 still needs to be sorted out).

Karim Nassar

Huh. I could have sworn I tried that and it didn't work. Thanks @dgeb

Well, in that case, I like the ternary operator version: property?trueClass:falseClass

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 994dd8be into 4ef1568).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 9c600f24 into 4ef1568).

Clemens Müller

I rebased onto master and squashed the commits.

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged c9cf4ab into 4ef1568).

Dan Gebhardt
dgeb commented May 17, 2012

Since this has a large amount of overlap with code I'm working on to finally wrap up #593, it would be nice to get this accepted sooner than later (of course, assuming it's acceptable :). Otherwise, either @pangratz or myself will end up doing a bunch of refactoring afterward.

Erik Bryn
Owner
ebryn commented June 02, 2012

I like this, but I think we should colons.

Clemens Müller

I've rebased on to the latest master. The current implementation uses the ternary opertator syntax path?true-class:false-class. But changing it to double-colon syntax should be an easy change. So what do you think? Any comments on the implementation?

Clemens Müller

It looks like I wasn't rebasing on to the latest master :pensive:

I'm on it ...

Dan Gebhardt
dgeb commented July 13, 2012

@pangratz please take a look at how class bindings are now evaluated in the {{view}} helper. This will require some changes, regardless of which format (:: vs ?:) is chosen. I still like the ternary format, but I may be in the minority here.

Clemens Müller

thanks @dgeb for the hint

Erik Bryn
Owner
ebryn commented July 13, 2012

If you make it use colon syntax, I will merge immediately :)

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged b7aa61c into 3d8a3b3).

Clemens Müller Change ternary syntax to double colon sytax
The syntax to define class names for truthy and falsy values changed from isEnabled?enabled:disabled to isEnabled:enabled:disabled
6309f24
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 6309f24 into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 12225b1 into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 89d6319 into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged b0d4845 into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 0e41ab5b into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 96840c6 into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 0d384ae into 3d8a3b3).

Clemens Müller pangratz referenced this pull request from a commit July 15, 2012
Commit has since been removed from the repository and is no longer available.
Clemens Müller

Can I get some feedback on this?

Dan Gebhardt
dgeb commented July 18, 2012

Seems good to me, @pangratz. I appreciate the refactoring you've done to consolidate property parsing and class name evaluation.

Clemens Müller

Thanks @dgeb !

I was thinking about another thing: sometimes people ask for a binding where a class is bound if the value of the property is false. Previously you had to create a computed property or a Ember.Binding.not() for that:

Ember.View.extend({
   isNotEnabledBinding: Ember.Binding.not('isEnabled'),
   classNameBindings: ['isNotEnabled:not-enabled']
});

When this gets merged, it would be possible via:

Ember.View.extend({
   classNameBindings: ['isEnabled::not-enabled']
});

Is this something which should be supported or should I add an Ember.assert for the "true class" to not being empty?

Dan Gebhardt
dgeb commented July 18, 2012

@pangratz I discussed this very question with @ebryn a few days ago and we agreed it's convenient to allow the true class to be empty so that isEnabled::not-enabled works (although I find the syntax a bit weird, especially coming from Ruby). So I'm glad this works - thanks for checking!

@ebryn Any more concerns or are you ready to merge?

Erik Bryn
Owner
ebryn commented July 18, 2012

@pangratz My intent was that isEnabled::not-enabled would work.

Devin Torres

Double colon is confusing, but I don't have a better suggestion.

Clemens Müller

Thanks for the feedback!

I'll add a test case covering the empty true class ...

added some commits July 18, 2012
Clemens Müller Add case where trueClass in classNameBindings is empty
So if a binding like

Ember.View.extend({
  classNameBindings: ['isEnabled::disabled']
});

adds no class if `isEnabled` is `true` and adds the class disabled when `isEnabled` is `false`
077e59c
Clemens Müller Refactor Ember.View#_classStringForValue ae0dd6a
Clemens Müller

@ebryn I've added a test case for the empty true class case.

I'll do a rebase on master if this is good to go ...

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged ae0dd6a into 3d8a3b3).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 7df746d into 3d8a3b3).

Erik Bryn ebryn merged commit 0a4ece1 into from July 18, 2012
Erik Bryn ebryn closed this July 18, 2012
Erik Bryn
Owner
ebryn commented July 18, 2012

Thanks @pangratz for adding this.

Clemens Müller

Wuhuuu, nice! :metal:

Thanks for your feedback!

Sylvain MINA
sly7-7 commented July 18, 2012

Very nice, thx all of you for this feature.

Clemens Müller pangratz referenced this pull request from a commit in pangratz/website July 15, 2012
Clemens Müller Add documentation for double colon syntax in class name bindings
This add documentation for emberjs/ember.js#732
0912b35
Albert-Jan Nijburg

Awesome! thanks!

sherwinyu

I'm just curious, what was the reason for preferring the double colon syntax over the ternary test?trueClass:falseClass syntax?

Clemens Müller pangratz deleted the branch November 14, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 13 unique commits by 1 author.

Jul 14, 2012
Clemens Müller Add tests for ternary operator in class bindings a75451c
Clemens Müller Implement helper methods in Ember.View
The helper methods are
 - _parsePropertyPath
 - _classStringForValue
 - _classStringForPath
411321a
Clemens Müller Add tests for Ember.View helpers e596ab0
Clemens Müller Use previously introduced Ember.View helpers in Handlebar helpers b7aa61c
Clemens Müller Change ternary syntax to double colon sytax
The syntax to define class names for truthy and falsy values changed from isEnabled?enabled:disabled to isEnabled:enabled:disabled
6309f24
Clemens Müller Cleanup Ember.View#_classStringForProperty 12225b1
Clemens Müller Add documentation for double colon syntax in classNameBindings 89d6319
Clemens Müller Add some documentation to Ember.View helpers b0d4845
Clemens Müller Add more documentation 96840c6
Jul 15, 2012
Clemens Müller Fix typo 0d384ae
Jul 18, 2012
Clemens Müller Add case where trueClass in classNameBindings is empty
So if a binding like

Ember.View.extend({
  classNameBindings: ['isEnabled::disabled']
});

adds no class if `isEnabled` is `true` and adds the class disabled when `isEnabled` is `false`
077e59c
Clemens Müller Refactor Ember.View#_classStringForValue ae0dd6a
Clemens Müller Add documentation for the case where the "true class" is empty 7df746d
This page is out of date. Refresh to see the latest.
39  packages/ember-handlebars/lib/helpers/binding.js
@@ -360,8 +360,9 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
360 360
   // Helper method to retrieve the property from the context and
361 361
   // determine which class string to return, based on whether it is
362 362
   // a Boolean or not.
363  
-  var classStringForPath = function(root, path, className, options) {
364  
-    var val;
  363
+  var classStringForPath = function(root, parsedPath, options) {
  364
+    var val,
  365
+        path = parsedPath.path;
365 366
 
366 367
     if (path === 'this') {
367 368
       val = root;
@@ -371,30 +372,7 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
371 372
       val = getPath(root, path, options);
372 373
     }
373 374
 
374  
-    // If the value is truthy and we're using the colon syntax,
375  
-    // we should return the className directly
376  
-    if (!!val && className) {
377  
-      return className;
378  
-
379  
-    // If value is a Boolean and true, return the dasherized property
380  
-    // name.
381  
-    } else if (val === true) {
382  
-      // Normalize property path to be suitable for use
383  
-      // as a class name. For example, content.foo.barBaz
384  
-      // becomes bar-baz.
385  
-      var parts = path.split('.');
386  
-      return Ember.String.dasherize(parts[parts.length-1]);
387  
-
388  
-    // If the value is not false, undefined, or null, return the current
389  
-    // value of the property.
390  
-    } else if (val !== false && val !== undefined && val !== null) {
391  
-      return val;
392  
-
393  
-    // Nothing to display. Return null so that the old class is removed
394  
-    // but no new class is added.
395  
-    } else {
396  
-      return null;
397  
-    }
  375
+    return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName);
398 376
   };
399 377
 
400 378
   // For each property passed, loop through and setup
@@ -408,9 +386,8 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
408 386
 
409 387
     var observer, invoker;
410 388
 
411  
-    var split = binding.split(':'),
412  
-        path = split[0],
413  
-        className = split[1],
  389
+    var parsedPath = Ember.View._parsePropertyPath(binding),
  390
+        path = parsedPath.path,
414 391
         pathRoot = context,
415 392
         normalized;
416 393
 
@@ -426,7 +403,7 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
426 403
     /** @private */
427 404
     observer = function() {
428 405
       // Get the current value of the property
429  
-      newClass = classStringForPath(pathRoot, path, className, options);
  406
+      newClass = classStringForPath(pathRoot, parsedPath, options);
430 407
       elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$();
431 408
 
432 409
       // If we can't find the element anymore, a parent template has been
@@ -461,7 +438,7 @@ EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId,
461 438
 
462 439
     // We've already setup the observer; now we just need to figure out the
463 440
     // correct behavior right now on the first pass through.
464  
-    value = classStringForPath(pathRoot, path, className, options);
  441
+    value = classStringForPath(pathRoot, parsedPath, options);
465 442
 
466 443
     if (value) {
467 444
       ret.push(value);
43  packages/ember-handlebars/lib/helpers/view.js
@@ -75,40 +75,19 @@ EmberHandlebars.ViewHelper = Ember.Object.create({
75 75
 
76 76
     // Evaluate the context of class name bindings:
77 77
     if (extensions.classNameBindings) {
78  
-      var full,
79  
-          parts;
80  
-
81 78
       for (var b in extensions.classNameBindings) {
82  
-        full = extensions.classNameBindings[b];
  79
+        var full = extensions.classNameBindings[b];
83 80
         if (typeof full === 'string') {
84  
-          if (full.indexOf(':') > 0) {
85  
-            // When a classNameBinding contains a colon anywhere after the first character,
86  
-            // then the part preceding the colon is a binding path that needs to be
87  
-            // contextualized.
88  
-            //
89  
-            // For example:
90  
-            //   classNameBinding="isGreen:green"
91  
-            //
92  
-            // Will be converted to:
93  
-            //   classNameBinding="bindingContext.isGreen:green"
94  
-
95  
-            parts = full.split(':');
96  
-            path = this.contextualizeBindingPath(parts[0], data);
97  
-            if (path) { extensions.classNameBindings[b] = path + ':' + parts[1]; }
98  
-
99  
-          } else if (full.indexOf(':') === -1 ) {
100  
-            // When a classNameBinding doesn't contain any colons, then the entire binding
101  
-            // needs to be contextualized.
102  
-            //
103  
-            // For example:
104  
-            //   classNameBinding="myClass"
105  
-            //
106  
-            // Will be converted to:
107  
-            //   classNameBinding="bindingContext.myClass"
108  
-
109  
-            path = this.contextualizeBindingPath(full, data);
110  
-            if (path) { extensions.classNameBindings[b] = path; }
111  
-          }
  81
+          // Contextualize the path of classNameBinding so this:
  82
+          //
  83
+          //     classNameBinding="isGreen:green"
  84
+          //
  85
+          // is converted to this:
  86
+          //
  87
+          //     classNameBinding="bindingContext.isGreen:green"
  88
+          var parsedPath = Ember.View._parsePropertyPath(full);
  89
+          path = this.contextualizeBindingPath(parsedPath.path, data);
  90
+          if (path) { extensions.classNameBindings[b] = path + parsedPath.classNames; }
112 91
         }
113 92
       }
114 93
     }
70  packages/ember-handlebars/tests/handlebars_test.js
@@ -1149,12 +1149,13 @@ test("{{view}} should evaluate class bindings set to global paths", function() {
1149 1149
     window.App = Ember.Application.create({
1150 1150
       isApp:       true,
1151 1151
       isGreat:     true,
1152  
-      directClass: "app-direct"
  1152
+      directClass: "app-direct",
  1153
+      isEnabled:   true
1153 1154
     });
1154 1155
   });
1155 1156
 
1156 1157
   view = Ember.View.create({
1157  
-    template: Ember.Handlebars.compile('{{view Ember.TextField class="unbound" classBinding="App.isGreat:great App.directClass App.isApp"}}')
  1158
+    template: Ember.Handlebars.compile('{{view Ember.TextField class="unbound" classBinding="App.isGreat:great App.directClass App.isApp App.isEnabled:enabled:disabled"}}')
1158 1159
   });
1159 1160
 
1160 1161
   appendView();
@@ -1163,12 +1164,17 @@ test("{{view}} should evaluate class bindings set to global paths", function() {
1163 1164
   ok(view.$('input').hasClass('great'),       "evaluates classes bound to global paths");
1164 1165
   ok(view.$('input').hasClass('app-direct'),  "evaluates classes bound directly to global paths");
1165 1166
   ok(view.$('input').hasClass('is-app'),      "evaluates classes bound directly to booleans in global paths - dasherizes and sets class when true");
  1167
+  ok(view.$('input').hasClass('enabled'),     "evaluates ternary operator in classBindings");
  1168
+  ok(!view.$('input').hasClass('disabled'),   "evaluates ternary operator in classBindings");
1166 1169
 
1167 1170
   Ember.run(function() {
1168 1171
     App.set('isApp', false);
  1172
+    App.set('isEnabled', false);
1169 1173
   });
1170 1174
 
1171 1175
   ok(!view.$('input').hasClass('is-app'),     "evaluates classes bound directly to booleans in global paths - removes class when false");
  1176
+  ok(!view.$('input').hasClass('enabled'),    "evaluates ternary operator in classBindings");
  1177
+  ok(view.$('input').hasClass('disabled'),    "evaluates ternary operator in classBindings");
1172 1178
 
1173 1179
   Ember.run(function() {
1174 1180
     window.App.destroy();
@@ -1180,7 +1186,8 @@ test("{{view}} should evaluate class bindings set in the current context", funct
1180 1186
     isView:      true,
1181 1187
     isEditable:  true,
1182 1188
     directClass: "view-direct",
1183  
-    template: Ember.Handlebars.compile('{{view Ember.TextField class="unbound" classBinding="isEditable:editable directClass isView"}}')
  1189
+    isEnabled: true,
  1190
+    template: Ember.Handlebars.compile('{{view Ember.TextField class="unbound" classBinding="isEditable:editable directClass isView isEnabled:enabled:disabled"}}')
1184 1191
   });
1185 1192
 
1186 1193
   appendView();
@@ -1189,30 +1196,49 @@ test("{{view}} should evaluate class bindings set in the current context", funct
1189 1196
   ok(view.$('input').hasClass('editable'),    "evaluates classes bound in the current context");
1190 1197
   ok(view.$('input').hasClass('view-direct'), "evaluates classes bound directly in the current context");
1191 1198
   ok(view.$('input').hasClass('is-view'),     "evaluates classes bound directly to booleans in the current context - dasherizes and sets class when true");
  1199
+  ok(view.$('input').hasClass('enabled'),     "evaluates ternary operator in classBindings");
  1200
+  ok(!view.$('input').hasClass('disabled'),   "evaluates ternary operator in classBindings");
1192 1201
 
1193 1202
   Ember.run(function() {
1194 1203
     view.set('isView', false);
  1204
+    view.set('isEnabled', false);
1195 1205
   });
1196 1206
 
1197 1207
   ok(!view.$('input').hasClass('is-view'),    "evaluates classes bound directly to booleans in the current context - removes class when false");
  1208
+  ok(!view.$('input').hasClass('enabled'),    "evaluates ternary operator in classBindings");
  1209
+  ok(view.$('input').hasClass('disabled'),    "evaluates ternary operator in classBindings");
1198 1210
 });
1199 1211
 
1200 1212
 test("{{view}} should evaluate class bindings set with either classBinding or classNameBindings", function() {
1201 1213
   Ember.run(function() {
1202 1214
     window.App = Ember.Application.create({
1203  
-      isGreat: true
  1215
+      isGreat: true,
  1216
+      isEnabled: true
1204 1217
     });
1205 1218
   });
1206 1219
 
1207 1220
   view = Ember.View.create({
1208  
-    template: Ember.Handlebars.compile('{{view Ember.TextField class="unbound" classBinding="App.isGreat:great" classNameBindings="App.isGreat:really-great"}}')
  1221
+    template: Ember.Handlebars.compile('{{view Ember.TextField class="unbound" classBinding="App.isGreat:great App.isEnabled:enabled:disabled" classNameBindings="App.isGreat:really-great App.isEnabled:really-enabled:really-disabled"}}')
1209 1222
   });
1210 1223
 
1211 1224
   appendView();
1212 1225
 
1213  
-  ok(view.$('input').hasClass('unbound'),      "sets unbound classes directly");
1214  
-  ok(view.$('input').hasClass('great'),        "evaluates classBinding");
1215  
-  ok(view.$('input').hasClass('really-great'), "evaluates classNameBinding");
  1226
+  ok(view.$('input').hasClass('unbound'),          "sets unbound classes directly");
  1227
+  ok(view.$('input').hasClass('great'),            "evaluates classBinding");
  1228
+  ok(view.$('input').hasClass('really-great'),     "evaluates classNameBinding");
  1229
+  ok(view.$('input').hasClass('enabled'),          "evaluates ternary operator in classBindings");
  1230
+  ok(view.$('input').hasClass('really-enabled'),   "evaluates ternary operator in classBindings");
  1231
+  ok(!view.$('input').hasClass('disabled'),        "evaluates ternary operator in classBindings");
  1232
+  ok(!view.$('input').hasClass('really-disabled'), "evaluates ternary operator in classBindings");
  1233
+
  1234
+  Ember.run(function() {
  1235
+    App.set('isEnabled', false);
  1236
+  });
  1237
+
  1238
+  ok(!view.$('input').hasClass('enabled'),        "evaluates ternary operator in classBindings");
  1239
+  ok(!view.$('input').hasClass('really-enabled'), "evaluates ternary operator in classBindings");
  1240
+  ok(view.$('input').hasClass('disabled'),        "evaluates ternary operator in classBindings");
  1241
+  ok(view.$('input').hasClass('really-disabled'), "evaluates ternary operator in classBindings");
1216 1242
 
1217 1243
   Ember.run(function() {
1218 1244
     window.App.destroy();
@@ -1545,11 +1571,10 @@ test("should not allow XSS injection via {{bindAttr}} with class", function() {
1545 1571
   equal(view.$('img').attr('class'), '" onmouseover="alert(\'I am in your classes hacking your app\');');
1546 1572
 });
1547 1573
 
1548  
-test("should be able to bind boolean element attributes using {{bindAttr}}", function() {
1549  
-  var template = Ember.Handlebars.compile('<input type="checkbox" {{bindAttr disabled="content.isDisabled" checked="content.isChecked"}} />');
  1574
+test("should be able to bind class attribute using ternary operator in {{bindAttr}}", function() {
  1575
+  var template = Ember.Handlebars.compile('<img {{bindAttr class="content.isDisabled:disabled:enabled"}} />');
1550 1576
   var content = Ember.Object.create({
1551  
-    isDisabled: false,
1552  
-    isChecked: true
  1577
+    isDisabled: true
1553 1578
   });
1554 1579
 
1555 1580
   view = Ember.View.create({
@@ -1559,24 +1584,24 @@ test("should be able to bind boolean element attributes using {{bindAttr}}", fun
1559 1584
 
1560 1585
   appendView();
1561 1586
 
1562  
-  ok(!view.$('input').attr('disabled'), 'attribute does not exist upon initial render');
1563  
-  ok(view.$('input').attr('checked'), 'attribute is present upon initial render');
  1587
+  ok(view.$('img').hasClass('disabled'), 'disabled class is rendered');
  1588
+  ok(!view.$('img').hasClass('enabled'), 'enabled class is not rendered');
1564 1589
 
1565 1590
   Ember.run(function() {
1566  
-    set(content, 'isDisabled', true);
1567  
-    set(content, 'isChecked', false);
  1591
+    set(content, 'isDisabled', false);
1568 1592
   });
1569 1593
 
1570  
-  ok(view.$('input').attr('disabled'), 'attribute exists after update');
1571  
-  ok(!view.$('input').attr('checked'), 'attribute is not present after update');
  1594
+  ok(!view.$('img').hasClass('disabled'), 'disabled class is not rendered');
  1595
+  ok(view.$('img').hasClass('enabled'), 'enabled class is rendered');
1572 1596
 });
1573 1597
 
1574 1598
 test("should be able to add multiple classes using {{bindAttr class}}", function() {
1575  
-  var template = Ember.Handlebars.compile('<div {{bindAttr class="content.isAwesomeSauce content.isAlsoCool content.isAmazing:amazing :is-super-duper"}}></div>');
  1599
+  var template = Ember.Handlebars.compile('<div {{bindAttr class="content.isAwesomeSauce content.isAlsoCool content.isAmazing:amazing :is-super-duper content.isEnabled:enabled:disabled"}}></div>');
1576 1600
   var content = Ember.Object.create({
1577 1601
     isAwesomeSauce: true,
1578 1602
     isAlsoCool: true,
1579  
-    isAmazing: true
  1603
+    isAmazing: true,
  1604
+    isEnabled: true
1580 1605
   });
1581 1606
 
1582 1607
   view = Ember.View.create({
@@ -1590,15 +1615,20 @@ test("should be able to add multiple classes using {{bindAttr class}}", function
1590 1615
   ok(view.$('div').hasClass('is-also-cool'), "dasherizes second property and sets classname");
1591 1616
   ok(view.$('div').hasClass('amazing'), "uses alias for third property and sets classname");
1592 1617
   ok(view.$('div').hasClass('is-super-duper'), "static class is present");
  1618
+  ok(view.$('div').hasClass('enabled'), "truthy class in ternary classname definition is rendered");
  1619
+  ok(!view.$('div').hasClass('disabled'), "falsy class in ternary classname definition is not rendered");
1593 1620
 
1594 1621
   Ember.run(function() {
1595 1622
     set(content, 'isAwesomeSauce', false);
1596 1623
     set(content, 'isAmazing', false);
  1624
+    set(content, 'isEnabled', false);
1597 1625
   });
1598 1626
 
1599 1627
   ok(!view.$('div').hasClass('is-awesome-sauce'), "removes dasherized class when property is set to false");
1600 1628
   ok(!view.$('div').hasClass('amazing'), "removes aliased class when property is set to false");
1601 1629
   ok(view.$('div').hasClass('is-super-duper'), "static class is still present");
  1630
+  ok(!view.$('div').hasClass('enabled'), "truthy class in ternary classname definition is not rendered");
  1631
+  ok(view.$('div').hasClass('disabled'), "falsy class in ternary classname definition is rendered");
1602 1632
 });
1603 1633
 
1604 1634
 test("should be able to bind classes to globals with {{bindAttr class}}", function() {
170  packages/ember-views/lib/views/view.js
@@ -151,6 +151,43 @@ var invokeForState = {
151 151
 
152 152
       <div id="ember1" class="ember-view empty"></div>
153 153
 
  154
+
  155
+  If you want to add a class name for a property which evaluates to true and
  156
+  and a different class name if it evaluates to false, you can pass a binding
  157
+  like this:
  158
+
  159
+    // Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false
  160
+    Ember.View.create({
  161
+      classNameBindings: ['isEnabled:enabled:disabled']
  162
+      isEnabled: true
  163
+    });
  164
+
  165
+  Will result in view instances with an HTML representation of:
  166
+
  167
+      <div id="ember1" class="ember-view enabled"></div>
  168
+
  169
+  When isEnabled is `false`, the resulting HTML reprensentation looks like this:
  170
+
  171
+      <div id="ember1" class="ember-view disabled"></div>
  172
+
  173
+  This syntax offers the convenience to add a class if a property is `false`:
  174
+
  175
+    // Applies no class when isEnabled is true and class 'disabled' when isEnabled is false
  176
+    Ember.View.create({
  177
+      classNameBindings: ['isEnabled::disabled']
  178
+      isEnabled: true
  179
+    });
  180
+
  181
+  Will result in view instances with an HTML representation of:
  182
+
  183
+    <div id="ember1" class="ember-view"></div>
  184
+
  185
+  When the `isEnabled` property on the view is set to `false`, it will result
  186
+  in view instances with an HTML representation of:
  187
+
  188
+    <div id="ember1" class="ember-view disabled"></div>
  189
+
  190
+
154 191
   Updates to the the value of a class name binding will result in automatic update 
155 192
   of the  HTML `class` attribute in the view's rendered HTML representation.
156 193
   If the value becomes  `false` or `undefined` the class name will be removed.
@@ -926,7 +963,7 @@ Ember.View = Ember.Object.extend(Ember.Evented,
926 963
       // Variable in which the old class value is saved. The observer function
927 964
       // closes over this variable, so it knows which string to remove when
928 965
       // the property changes.
929  
-      var oldClass, property;
  966
+      var oldClass;
930 967
 
931 968
       // Set up an observer on the context. If the property changes, toggle the
932 969
       // class name.
@@ -968,8 +1005,8 @@ Ember.View = Ember.Object.extend(Ember.Evented,
968 1005
       }
969 1006
 
970 1007
       // Extract just the property name from bindings like 'foo:bar'
971  
-      property = binding.split(':')[0];
972  
-      addObserver(this, property, observer);
  1008
+      var parsedPath = Ember.View._parsePropertyPath(binding);
  1009
+      addObserver(this, parsedPath.path, observer);
973 1010
     }, this);
974 1011
   },
975 1012
 
@@ -1018,41 +1055,16 @@ Ember.View = Ember.Object.extend(Ember.Evented,
1018 1055
     passing `isUrgent` to this method will return `"is-urgent"`.
1019 1056
   */
1020 1057
   _classStringForProperty: function(property) {
1021  
-    var split = property.split(':'),
1022  
-        className = split[1];
1023  
-
1024  
-    property = split[0];
  1058
+    var parsedPath = Ember.View._parsePropertyPath(property);
  1059
+    var path = parsedPath.path;
1025 1060
 
1026 1061
     // TODO: Remove this `false` when the `getPath` globals support is removed
1027  
-    var val = Ember.getPath(this, property, false);
1028  
-    if (val === undefined && Ember.isGlobalPath(property)) {
1029  
-      val = Ember.getPath(window, property);
  1062
+    var val = getPath(this, path, false);
  1063
+    if (val === undefined && Ember.isGlobalPath(path)) {
  1064
+      val = getPath(window, path);
1030 1065
     }
1031 1066
 
1032  
-    // If the value is truthy and we're using the colon syntax,
1033  
-    // we should return the className directly
1034  
-    if (!!val && className) {
1035  
-      return className;
1036  
-
1037  
-    // If value is a Boolean and true, return the dasherized property
1038  
-    // name.
1039  
-    } else if (val === true) {
1040  
-      // Normalize property path to be suitable for use
1041  
-      // as a class name. For exaple, content.foo.barBaz
1042  
-      // becomes bar-baz.
1043  
-      var parts = property.split('.');
1044  
-      return Ember.String.dasherize(parts[parts.length-1]);
1045  
-
1046  
-    // If the value is not false, undefined, or null, return the current
1047  
-    // value of the property.
1048  
-    } else if (val !== false && val !== undefined && val !== null) {
1049  
-      return val;
1050  
-
1051  
-    // Nothing to display. Return null so that the old class is removed
1052  
-    // but no new class is added.
1053  
-    } else {
1054  
-      return null;
1055  
-    }
  1067
+    return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName);
1056 1068
   },
1057 1069
 
1058 1070
   // ..........................................................
@@ -2003,6 +2015,96 @@ Ember.View.reopen({
2003 2015
   domManager: DOMManager
2004 2016
 });
2005 2017
 
  2018
+Ember.View.reopenClass({
  2019
+
  2020
+  /**
  2021
+    @private
  2022
+
  2023
+    Parse a path and return an object which holds the parsed properties.
  2024
+
  2025
+    For example a path like "content.isEnabled:enabled:disabled" wil return the
  2026
+    following object:
  2027
+
  2028
+        {
  2029
+          path: "content.isEnabled",
  2030
+          className: "enabled",
  2031
+          falsyClassName: "disabled",
  2032
+          classNames: ":enabled:disabled"
  2033
+        }
  2034
+
  2035
+  */
  2036
+  _parsePropertyPath: function(path) {
  2037
+    var split = path.split(/:/),
  2038
+        propertyPath = split[0],
  2039
+        classNames = "",
  2040
+        className,
  2041
+        falsyClassName;
  2042
+
  2043
+    // check if the property is defined as prop:class or prop:trueClass:falseClass
  2044
+    if (split.length > 1) {
  2045
+      className = split[1];
  2046
+      if (split.length === 3) { falsyClassName = split[2]; }
  2047
+
  2048
+      classNames = ':' + className;
  2049
+      if (falsyClassName) { classNames += ":" + falsyClassName; }
  2050
+    }
  2051
+
  2052
+    return {
  2053
+      path: propertyPath,
  2054
+      classNames: classNames,
  2055
+      className: (className === '') ? undefined : className,
  2056
+      falsyClassName: falsyClassName
  2057
+    };
  2058
+  },
  2059
+
  2060
+  /**
  2061
+    @private
  2062
+
  2063
+    Get the class name for a given value, based on the path, optional className
  2064
+    and optional falsyClassName.
  2065
+
  2066
+    - if the value is truthy and a className is defined, the className is returned
  2067
+    - if the value is true, the dasherized last part of the supplied path is returned
  2068
+    - if the value is false and a falsyClassName is supplied, the falsyClassName is returned
  2069
+    - if the value is truthy, the value is returned
  2070
+    - if none of the above rules apply, null is returned
  2071
+
  2072
+  */
  2073
+  _classStringForValue: function(path, val, className, falsyClassName) {
  2074
+    // If the value is truthy and we're using the colon syntax,
  2075
+    // we should return the className directly
  2076
+    if (!!val && className) {
  2077
+      return className;
  2078
+
  2079
+    // If value is a Boolean and true, return the dasherized property
  2080
+    // name.
  2081
+    } else if (val === true) {
  2082
+      // catch syntax like isEnabled::not-enabled
  2083
+      if (val === true && !className && falsyClassName) { return null; }
  2084
+
  2085
+      // Normalize property path to be suitable for use
  2086
+      // as a class name. For exaple, content.foo.barBaz
  2087
+      // becomes bar-baz.
  2088
+      var parts = path.split('.');
  2089
+      return Ember.String.dasherize(parts[parts.length-1]);
  2090
+
  2091
+    // If the value is false and a falsyClassName is specified, return it
  2092
+    } else if (val === false && falsyClassName) {
  2093
+      return falsyClassName;
  2094
+
  2095
+    // If the value is not false, undefined, or null, return the current
  2096
+    // value of the property.
  2097
+    } else if (val !== false && val !== undefined && val !== null) {
  2098
+      return val;
  2099
+
  2100
+    // Nothing to display. Return null so that the old class is removed
  2101
+    // but no new class is added.
  2102
+    } else {
  2103
+      return null;
  2104
+    }
  2105
+  }
  2106
+});
  2107
+
2006 2108
 // Create a global view hash.
2007 2109
 Ember.View.views = {};
2008 2110
 
31  packages/ember-views/tests/views/view/class_name_bindings_test.js
@@ -11,13 +11,15 @@ module("Ember.View - Class Name Bindings");
11 11
 test("should apply bound class names to the element", function() {
12 12
   var view = Ember.View.create({
13 13
     classNameBindings: ['priority', 'isUrgent', 'isClassified:classified',
14  
-                        'canIgnore', 'messages.count', 'messages.resent:is-resent', 'isNumber:is-number'],
  14
+                        'canIgnore', 'messages.count', 'messages.resent:is-resent', 'isNumber:is-number',
  15
+                        'isEnabled:enabled:disabled'],
15 16
 
16 17
     priority: 'high',
17 18
     isUrgent: true,
18 19
     isClassified: true,
19 20
     canIgnore: false,
20  
-		isNumber: 5,
  21
+    isNumber: 5,
  22
+    isEnabled: true,
21 23
 
22 24
     messages: {
23 25
       count: 'five-messages',
@@ -36,17 +38,21 @@ test("should apply bound class names to the element", function() {
36 38
   ok(view.$().hasClass('is-resent'), "supports customing class name for paths");
37 39
   ok(view.$().hasClass('is-number'), "supports colon syntax with truthy properties");
38 40
   ok(!view.$().hasClass('can-ignore'), "does not add false Boolean values as class");
  41
+  ok(view.$().hasClass('enabled'), "supports customizing class name for Boolean values with negation");
  42
+  ok(!view.$().hasClass('disabled'), "does not add class name for negated binding");
39 43
 });
40 44
 
41 45
 test("should add, remove, or change class names if changed after element is created", function() {
42 46
   var view = Ember.View.create({
43 47
     classNameBindings: ['priority', 'isUrgent', 'isClassified:classified',
44  
-                        'canIgnore', 'messages.count', 'messages.resent:is-resent'],
  48
+                        'canIgnore', 'messages.count', 'messages.resent:is-resent',
  49
+                        'isEnabled:enabled:disabled'],
45 50
 
46 51
     priority: 'high',
47 52
     isUrgent: true,
48 53
     isClassified: true,
49 54
     canIgnore: false,
  55
+    isEnabled: true,
50 56
 
51 57
     messages: Ember.Object.create({
52 58
       count: 'five-messages',
@@ -59,6 +65,7 @@ test("should add, remove, or change class names if changed after element is crea
59 65
     set(view, 'priority', 'orange');
60 66
     set(view, 'isUrgent', false);
61 67
     set(view, 'canIgnore', true);
  68
+    set(view, 'isEnabled', false);
62 69
     setPath(view, 'messages.count', 'six-messages');
63 70
     setPath(view, 'messages.resent', true );
64 71
   });
@@ -73,6 +80,24 @@ test("should add, remove, or change class names if changed after element is crea
73 80
   ok(!view.$().hasClass('five-messages'), "removes old value when path changes");
74 81
 
75 82
   ok(view.$().hasClass('is-resent'), "adds customized class name when path changes");
  83
+
  84
+  ok(!view.$().hasClass('enabled'), "updates class name for negated binding");
  85
+  ok(view.$().hasClass('disabled'), "adds negated class name for negated binding");
  86
+});
  87
+
  88
+test(":: class name syntax works with an empty true class", function() {
  89
+  var view = Ember.View.create({
  90
+    isEnabled: false,
  91
+    classNameBindings: ['isEnabled::not-enabled']
  92
+  });
  93
+
  94
+  Ember.run(function(){ view.createElement(); });
  95
+
  96
+  equal(view.$().attr('class'), 'ember-view not-enabled', "false class is rendered when property is false");
  97
+
  98
+  Ember.run(function(){ view.set('isEnabled', true); });
  99
+
  100
+  equal(view.$().attr('class'), 'ember-view', "no class is added when property is true and the class is empty");
76 101
 });
77 102
 
78 103
 test("classNames should not be duplicated on rerender", function(){
41  packages/ember-views/tests/views/view/class_string_for_value_test.js
... ...
@@ -0,0 +1,41 @@
  1
+module("Ember.View - _classStringForValue");
  2
+
  3
+var cSFV = Ember.View._classStringForValue;
  4
+
  5
+test("returns dasherized version of last path part if value is true", function() {
  6
+  equal(cSFV("propertyName", true), "property-name", "class is dasherized");
  7
+  equal(cSFV("content.propertyName", true), "property-name", "class is dasherized");
  8
+});
  9
+
  10
+test("returns className if value is true and className is specified", function() {
  11
+  equal(cSFV("propertyName", true, "truthyClass"), "truthyClass", "returns className if given");
  12
+  equal(cSFV("content.propertyName", true, "truthyClass"), "truthyClass", "returns className if given");
  13
+});
  14
+
  15
+test("returns falsyClassName if value is false and falsyClassName is specified", function() {
  16
+  equal(cSFV("propertyName", false, "truthyClass", "falsyClass"), "falsyClass", "returns falsyClassName if given");
  17
+  equal(cSFV("content.propertyName", false, "truthyClass", "falsyClass"), "falsyClass", "returns falsyClassName if given");
  18
+});
  19
+
  20
+test("returns null if value is false and falsyClassName is not specified", function() {
  21
+  equal(cSFV("propertyName", false, "truthyClass"), null, "returns null if falsyClassName is not specified");
  22
+  equal(cSFV("content.propertyName", false, "truthyClass"), null, "returns null if falsyClassName is not specified");
  23
+});
  24
+
  25
+test("returns null if value is false", function() {
  26
+  equal(cSFV("propertyName", false), null, "returns null if value is false");
  27
+  equal(cSFV("content.propertyName", false), null, "returns null if value is false");
  28
+});
  29
+
  30
+test("returns null if value is true and className is not specified and falsyClassName is specified", function() {
  31
+  equal(cSFV("propertyName", true, undefined, "falsyClassName"), null, "returns null if value is true");
  32
+  equal(cSFV("content.propertyName", true, undefined, "falsyClassName"), null, "returns null if value is true");
  33
+});
  34
+
  35
+test("returns the value if the value is truthy", function() {
  36
+  equal(cSFV("propertyName", "myString"), "myString", "returns value if the value is truthy");
  37
+  equal(cSFV("content.propertyName", "myString"), "myString", "returns value if the value is truthy");
  38
+
  39
+  equal(cSFV("propertyName", "123"), 123, "returns value if the value is truthy");
  40
+  equal(cSFV("content.propertyName", 123), 123, "returns value if the value is truthy");
  41
+});
46  packages/ember-views/tests/views/view/parse_property_path_test.js
... ...
@@ -0,0 +1,46 @@
  1
+module("Ember.View - _parsePropertyPath");
  2
+
  3
+test("it works with a simple property path", function() {
  4
+  var parsed = Ember.View._parsePropertyPath("simpleProperty");
  5
+
  6
+  equal(parsed.path, "simpleProperty", "path is parsed correctly");
  7
+  equal(parsed.className, undefined, "there is no className");
  8
+  equal(parsed.falsyClassName, undefined, "there is no falsyClassName");
  9
+  equal(parsed.classNames, "", "there is no classNames");
  10
+});
  11
+
  12
+test("it works with a more complex property path", function() {
  13
+  var parsed = Ember.View._parsePropertyPath("content.simpleProperty");
  14
+
  15
+  equal(parsed.path, "content.simpleProperty", "path is parsed correctly");
  16
+  equal(parsed.className, undefined, "there is no className");
  17
+  equal(parsed.falsyClassName, undefined, "there is no falsyClassName");
  18
+  equal(parsed.classNames, "", "there is no classNames");
  19
+});
  20
+
  21
+test("className is extracted", function() {
  22
+  var parsed = Ember.View._parsePropertyPath("content.simpleProperty:class");
  23
+
  24
+  equal(parsed.path, "content.simpleProperty", "path is parsed correctly");
  25
+  equal(parsed.className, "class", "className is extracted");
  26
+  equal(parsed.falsyClassName, undefined, "there is no falsyClassName");
  27
+  equal(parsed.classNames, ":class", "there is a classNames");
  28
+});
  29
+
  30
+test("falsyClassName is extracted", function() {
  31
+  var parsed = Ember.View._parsePropertyPath("content.simpleProperty:class:falsyClass");
  32
+
  33
+  equal(parsed.path, "content.simpleProperty", "path is parsed correctly");
  34
+  equal(parsed.className, "class", "className is extracted");
  35
+  equal(parsed.falsyClassName, "falsyClass", "falsyClassName is extracted");
  36
+  equal(parsed.classNames, ":class:falsyClass", "there is a classNames");
  37
+});
  38
+
  39
+test("it works with an empty true class", function() {
  40
+  var parsed = Ember.View._parsePropertyPath("content.simpleProperty::falsyClass");
  41
+
  42
+  equal(parsed.path, "content.simpleProperty", "path is parsed correctly");
  43
+  equal(parsed.className, undefined, "className is undefined");
  44
+  equal(parsed.falsyClassName, "falsyClass", "falsyClassName is extracted");
  45
+  equal(parsed.classNames, "::falsyClass", "there is a classNames");
  46
+});
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.