Artem Svirskyi
Artem Svirskyi

Reputation: 7839

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?

Upvotes: 46

Views: 18447

Answers (9)

user3297291
user3297291

Reputation: 23372

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>

Upvotes: 0

Simon_Weaver
Simon_Weaver

Reputation: 145880

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.

Upvotes: 14

Kenneth Moore
Kenneth Moore

Reputation: 1237

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

Upvotes: 1

Aleksey
Aleksey

Reputation: 1606

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 }

Upvotes: 59

Roy J
Roy J

Reputation: 43881

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

Upvotes: 2

mikus
mikus

Reputation: 3215

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.

Upvotes: 2

Alex
Alex

Reputation: 3444

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' : '');
});

Upvotes: 1

beauXjames
beauXjames

Reputation: 8418

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!

Upvotes: 4

Matt Burland
Matt Burland

Reputation: 45135

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/

Upvotes: 4

Related Questions