combine dynamic and static classes through css binding, knockout.js

In knockout.js we can use css binding for static classes

<div data-bind="css: {'translucent ': number() < 10}">static dynamic css classes</div>

and dynamic

<div data-bind="css: color">static dynamic css classes</div>

I've tried http://jsfiddle.net/tT9PK/1/ to combine it in something like

css: {color, translucent: number() < 10}

to get dynamic class color and static translucent at the same time, but I get an error. Is there a way to do that?

Answers:

Answer

You can add dynamic class by css property and then add static class by attr property

<div data-bind="attr: { 'class': color }, css: { 'translucent': number() < 10 }">
  static dynamic css classes
</div>

Be sure to add any predefined classes to this binding attr: { 'class': color }

Answer

I solved this problem a while back by just cloning the css binding as css2.

 ko.bindingHandlers['css2'] = ko.bindingHandlers.css;

Normally you can't use the same binding handler twice in a data-bind attribute, so this allowed me to do the following:

<div data-bind="css: color, css2: { 'translucent': number() < 10 }">static dynamic css classes</div>

I can't quite decide whether I still prefer this, or @Aleksey's answer, but this may be the only choice if you have multiple dynamic classes to add.

Answer

Correct...and to launch you even further, check out this modification.

http://jsfiddle.net/Fv27b/2/

Here, you'll see that not only are we combining the options, but we're creating our own binding entirely...which results in a much more portable extension of not just this view model, but any view model you may have in your project...so you'll only need to write this one once!

ko.bindingHandlers.colorAndTrans = {
    update: function(element, valAccessor) {
        var valdata = valAccessor();
        var cssString = valdata.color();
        if (valdata.transValue() < 10) cssString += " translucent";
        element.className = cssString;
    }
}

To invoke this, you just use it as a new data-bind property and can include as many (or as few) options as possible. Under this specific condition, I might have just provided $data, however if you're wanting a reusable option you need to be more specific as to what data types you need as parameters and not all view models may have the same properties.

data-bind="colorAndTrans: { color: color, transValue: number }"

Hope this does more than answer your question!

Answer

Your best bet is probably not to combine them. Instead use a computed property of your view model to combine them into a single property that you can bind dynamically. That way you can also avoid putting logic in your view with the number() < 10 binding, which is cleaner anyway.

Like this, for example:

viewModel.colorAndTrans = ko.computed(function () {
    var cssString = viewModel.color();
    if (viewModel.number() < 10) {
        cssString += " translucent"
    }
    return cssString;
});

See this working example: http://jsfiddle.net/tT9PK/4/

Answer

If you really get into complicated styling case, just accumulate all in the computed property. You can do it as Alex mentioned or a bit more readable:

vm.divStyle = ko.computed(function() {
        var styles = [];

        if (vm.isNested()) styles.push('nested');
        if (vm.isTabular()) styles.push('tabular');
        else styles.push('non-tabular');
        if (vm.color()) styles.push(vm.color());

        return styles.join(' ');
});

the main drawback is that you're moving a part of view definition into the viewmodel, that should be more independent. The alternative is to provide all the logic above as a plain js function call, and let knockout evaluate it.

Answer

Nice question, the problem seems to be the binding css isn't thought to mix the two kinds, color(): color() != '' doesn't work (would be nice).

I like @Simon_waver's answer approach, simple and practical.

Maybe at the time of the question wasn't supported (Idk) but with current knockout also combining the classes works: data-bind="css: computed"

viewModel.computed = ko.pureComputed(function() {
   return viewModel.color() + (viewModel.number() < 10 ? ' translucent' : '');
});
Answer

A couple more options:

Similar to the suggestions to use a computed, you can inline the expression:

<div data-bind="css: [color(), (number() < 10 ? 'translucent' : 'notTranslucent')].join(' ')">static dynamic css classes</div>

As an alternative to a custom binding handler that is specific to this case, you can make one that takes an array of mixed css specs and passes them to the original css handler:

<div data-bind="cssArray: [color, {translucent: number() < 10}]">static dynamic css classes</div>

The handler:

 ko.bindingHandlers.cssArray = {
    update: function (element, valueAccessor, allBindingsAccessor, data, context) {
        var arr = ko.unwrap(valueAccessor());
      for (var i=0; i<arr.length; ++i) {
        var wrapped = function () { return ko.unwrap(arr[i]) };
        ko.bindingHandlers.css.update(element, wrapped, allBindingsAccessor, data, context);
      }
    }
  }

Fiddle demo

Answer

There is a more elegant solution to this problem via computed property names (for FF>34, Chrome, Safari>7.1):

<div data-bind="css: { [color]: true,'translucent': number() < 10 }">
    static dynamic css classes
</div>

Whereas color is a property with a string value.

If the value of color is an observable then we need to clear the classname before that observable updates. If we do not do this then each change will add another class and not remove the previous one. This can easily be accomplished manually but I wrote an extender for those who are interested.

ko.extenders.css = function(target, value) {
  var beforeChange;
  var onChange;

  //add sub-observables to our observable
  target.show = ko.observable(true);

  beforeChange = function(oldValue){
    target.show(false);
  }
  onChange = function(newValue){
    target.show(true);
  }
  target.subscribe(beforeChange, null, "beforeChange");
  target.subscribe(onChange);
  return target;
};

With this extender, your JavaScript code would look like this:

function MyViewModel() {
    this.color = ko.observable("red").extend({ css: true });
    this.number = ko.observable(9)
};

And your markup would be this simple:

<div data-bind="css: { [color()]: color.show(),'translucent': number() < 10 }">
    static dynamic css classes
</div>

I have a code pen demonstrating this technique: http://codepen.io/USIUX/pen/WryGZQ

I have also submitted an issue with knockout in hopes that one day the custom extender will not be necessary: https://github.com/knockout/knockout/issues/1990

Answer

I'd create the css binding value in your viewmodel. You can define a computed that returns either an object or string.

Some examples, using ES2015:

const App = function() {
  this.number = ko.observable(12);
  this.color = ko.observable("red");
  
  this.cssConfigObj = ko.pureComputed(() => ({
    "italic": this.number() > 10,
    [this.color()]: true
  }));
  
  this.cssConfigStr = ko.pureComputed(() => 
    `${this.color()} ${this.number() > 10 ? "italic" : ""}`
  );
};

ko.applyBindings(new App());
.monospaced {
  font-family: monospace;
}

.italic {
  font-style: italic;
}

.red {
  color: red; 
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div
  class="monospaced"
  data-bind="css: cssConfigObj"
>Hello world</div>

<div
  class="monospaced"
  data-bind="css: cssConfigStr"
>Hello world</div>

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.