Reputation: 7839
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
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
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
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
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
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);
}
}
}
Upvotes: 2
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
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
Reputation: 8418
Correct...and to launch you even further, check out this modification.
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
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