I want to achieve dynamic class biding, which should assign and reassign correct class based on variable, putted in this field. The issue I got, when I induce function setRed(), and then setWhite(), both color classes are binded, and of course 1st CSS class is considered.
I've got an element which binding looks like that:
<div data-bind='dxNumberBox: dxCustomCalc'></div>
So far I made elementAttr class ko.observable();
self.dxCustomCalc = {
displayExpr: 'Name',
keyExpr: 'Id',
value: self.customCalc,
//onValueChanged: function (e) {
// self.childFormName(e.component.option('selectedItem'));
//},
disabled: ko.computed(function () {
return self.readOnly;
}),
elementAttr: {
/* class: "is-valid-nok"*/ /* - red*/
/*class: "is-valid-onlyok" */ /* -white */
/*class: "is-required-empty",*/ /* - yellow */
class: ko.observable(),
}
};
And went through element:
function setRed() {
self.dxCustomCalc.elementAttr.class("is-valid-nok");
console.log("color changed to red")
}
function setWhite(){
self.dxCustomCalc.elementAttr.class("is-valid-onlyok");
console.log("color changed to white")
}
Functions are executed based on value in field. For example, If value matches, function setRed() is fired. Then, if the value changes and the condition is met, function setWhite() is fired.
The result I got, after executing both functions on subscription to element is:
<div data-bind="dxNumberBox: dxCustomCalc" class="dx-numberbox is-valid-nok is-valid-onlyok">
The result I want to achieve, after executing both functions is:
<div data-bind="dxNumberBox: dxCustomCalc" class="dx-numberbox is-valid-onlyok">
I'd use the class binding to set a CSS class based on an observable.
You could do use it directly
<div data-bind="dxNumberBox: dxCustomCalc, class: dxCustomCalc.cssClass">
or you could apply the class binding as part of your dxCustomCalc custom binding, using ko.applyBindingsToNode():
ko.bindingHandlers.dxNumberBox = {
init: function(elem, valueAccessor, allBindings, viewModel) {
const value = ko.unwrap(valueAccessor());
ko.applyBindingsToNode(elem, {class: value.cssClass}, viewModel);
}
};
function DxNumberBox() {
this.dxCustomCalc = {
cssClass: ko.observable("is-required-empty")
};
this.setRed = () => this.dxCustomCalc.cssClass("is-valid-nok");
this.setWhite = () => this.dxCustomCalc.cssClass("is-valid-onlyok");
this.setEmpty = () => this.dxCustomCalc.cssClass("is-required-empty");
}
const vm = new DxNumberBox();
ko.applyBindings(vm);
.is-valid-nok {
background-color: red;
}
.is-valid-onlyok {
background-color: white;
}
.is-required-empty {
border: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.js"></script>
<button data-bind="click: setRed">Red</button>
<button data-bind="click: setWhite">White</button>
<button data-bind="click: setEmpty">Empty</button>
<div data-bind="dxNumberBox: dxCustomCalc">
Profit Information
</div>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
Functions are executed based on value in field.
Having extra methods like .setRed() is clunky and unnecessary. Turn cssClass into a computed observable that calculates a class name based on the state of the viewmodel, e.g.
cssClass: ko.pureComputed(() => {
var value = self.value().trim();
if (value == '') return 'is-required-empty';
if (value < 0) return 'is-valid-nok';
return 'is-valid-onlyok';
});
Related
I'm using knockout.js for binding values to view.
When modal is shown i initialize formatter. Here is sample:
<input type="text" id="propertyName" class="form-control" name="name" required="" data-bind="value: Name">
$("#exampleFormModal").on("shown.bs.modal", function () {
self.InitFormatter();
});
self.InitFormatter = function () {
$('#propertyName').formatter({
'pattern': '{{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}}',
'persistent': true
});
}
The problem is that there is empty values in value: Name
Using knockout with a library that does any kind of DOM manipulation - including value updates on elements - requires a custom binding handler, so that knockout can a) initialize that library properly and b) pass any updates between viewmodel and view.
Writing a custom binding handler for formatter.js is tricky, because formatter.js takes very tight control of all value-related events (keyboard, paste) that happen on an input element - without exposing any events of its own.
In other words, it's easy to set up, but it's hard to be notified when a value changes. But that is exactly what's necessary to keep the viewmodel up-to-date.
To be able to do it anyway, we must hook into one of the internal functions of formatter - the _processKey method. This method is called whenever the value of an input changes, so it's the perfect spot to set up a little "snitch" that tells knockout when the value changes.
Disclaimer This is a hack. It will break whenever the formatter.js internals change. With the current version 0.1.5 however, it seems to work rather well.
This way we can bind our view like this:
<input data-bind="formatter: {
value: someObservable,
pattern: '{{9999}}-{{9999}},
persistent: true
}">
and knockout can fill in the input value whenever someObservable changes, and thanks to the hook into _processKey it also can update someObservable whenever the input value changes.
The full implementation of the binding handler follows (it has no jQuery dependency):
// ko-formatter.js
/* global ko, Formatter */
ko.bindingHandlers.formatter = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor()) || {},
instance = new Formatter(element, ko.toJS(options)),
_processKey = Formatter.prototype._processKey,
valueSubs, patternSubs, patternsSubs;
if (ko.isWritableObservable(options.value)) {
// capture initial element value
options.value(element.value);
// shadow the internal _processKey method so we see value changes
instance._processKey = function () {
_processKey.apply(this, arguments);
options.value(element.value);
};
// catch the 'cut' event that formatter.js originally ignores
ko.utils.registerEventHandler(element, 'input', function () {
options.value(element.value);
});
// subscribe to options.value to achieve two-way binding
valueSubs = options.value.subscribe(function (newValue) {
// back out if observable and element values are equal
if (newValue === element.value) return;
// otherwise reset element and "type in" new observable value
element.value = '';
_processKey.call(instance, newValue, false, true);
// write formatted value back into observable
if (element.value !== newValue) options.value(element.value);
});
}
// support updating "pattern" option through knockout
if (ko.isObservable(options.pattern)) {
patternSubs = options.pattern.subscribe(function (newPattern) {
instance.resetPattern(newPattern);
});
}
// support updating "patterns" option through knockout
if (ko.isObservable(options.patterns)) {
patternsSubs = options.patterns.subscribe(function (newPatterns) {
instance.opts.patterns = newPatterns;
instance.resetPattern();
});
}
// clean up after ourselves
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (valueSubs) valueSubs.dispose();
if (patternSubs) patternSubs.dispose();
if (patternsSubs) patternsSubs.dispose();
});
}
// this binding has no "update" part, it's not necessary
};
This also supports making the pattern observable, so you can change the pattern for an input field dynamically.
Live demo (expand to run):
// ko-formatter.js
/* global ko, Formatter */
ko.bindingHandlers.formatter = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor()) || {},
instance = new Formatter(element, ko.toJS(options)),
_processKey = Formatter.prototype._processKey,
valueSubs, patternSubs, patternsSubs;
if (ko.isWritableObservable(options.value)) {
// capture initial element value
options.value(element.value);
// shadow the internal _processKey method so we see value changes
instance._processKey = function () {
_processKey.apply(this, arguments);
options.value(element.value);
};
// catch the 'cut' event that formatter.js originally ignores
ko.utils.registerEventHandler(element, 'input', function () {
options.value(element.value);
});
// subscribe to options.value to achieve two-way binding
valueSubs = options.value.subscribe(function (newValue) {
// back out if observable and element values are equal
if (newValue === element.value) return;
// otherwise reset element and "type" new observable value
element.value = '';
_processKey.call(instance, newValue, false, true);
// write formatted value back into observable
if (element.value !== newValue) options.value(element.value);
});
}
// support updating "pattern" option through knockout
if (ko.isObservable(options.pattern)) {
patternSubs = options.pattern.subscribe(function (newPattern) {
instance.resetPattern(newPattern);
});
}
// support updating "patterns" option through knockout
if (ko.isObservable(options.patterns)) {
patternsSubs = options.patterns.subscribe(function (newPatterns) {
instance.opts.patterns = newPatterns;
instance.resetPattern();
});
}
// clean up after ourselves
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (valueSubs) valueSubs.dispose();
if (patternSubs) patternSubs.dispose();
if (patternsSubs) patternsSubs.dispose();
});
}
// this binding has no "update" part, it's not necessary
};
// viewmodel implementation
ko.applyBindings({
inputPattern: ko.observable('{{9999}}-{{9999}}-{{9999}}-{{9999}}'),
inputValue: ko.observable(),
setValidValue: function () {
var dummy = this.inputPattern().replace(/\{\{([a9*]+)\}\}/g, function ($0, $1) {
return $1.replace(/\*/g, "x");
});
this.inputValue(dummy);
},
setInvalidValue: function () {
this.inputValue('invalid value');
}
});
input {
width: 20em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/formatter.js/0.1.5/formatter.min.js"></script>
View:<br>
<input data-bind="formatter: {
value: inputValue,
pattern: inputPattern,
persistent: true
}">
<input data-bind="value: inputPattern"><br>
<button data-bind="click: setValidValue">Set valid value</button>
<button data-bind="click: setInvalidValue">Set invalid value</button>
<hr>
Viewmodel:<br>
<pre data-bind="text: ko.toJSON($root, null ,2)"></pre>
I'm making my first custom binding. I would like to be able to specify what text that appears on an element based on a resource file. Something like this:
var exampleResource = {
hello: 'world'
};
ko.bindingHandlers.resource = {
init: function (element, valueAccessor) {
var value = valueAccessor();
ko.bindingHandlers.text.update(element, function() {
return exampleResource[value] || '';
});
}
};
<span data-bind="resource: 'hello'"></span>
Should I use ko.bindingHandlers.text as above?
Since the resource variable isn't observable, is there any point of adding the update callback for the binding? If I understand it correctly it will only get called if an observable is passed as the value?
You'd need an update if you want to support the input for your binding handler to be dynamic. In your example you don't do that, but you could. Here's an example:
var exampleResource = {
hello: 'world',
goodbye: 'drowl'
};
ko.bindingHandlers.resource = {
update: function (element, valueAccessor) {
var key = ko.utils.unwrapObservable(valueAccessor());
ko.bindingHandlers.text.update(element, function() {
return exampleResource[key] || key;
});
}
};
ko.applyBindings({ myObs: ko.observable('goodbye') });
span { font-weight: bold; color: red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
Static: <span data-bind="resource: 'hello'"></span>
<hr>
Dynamic: <span data-bind="resource: myObs"></span>
- based on: <select data-bind="value: myObs, options: ['hello', 'goodbye']"></select>
If you don't need this dynamicness you could stick to your old solution. However, in that case I'd question the added value of KnockoutJS for resources in general :-)
I want to add animation when working with observable not observableArray which is already available in knockout documents.
Consider a <div data-bind="text: fullname"/> which fullname is an observable. I want the content of <div> element animate and slides down when the value of fullname changes. Something like:
<div data-bind="text: fullname, transition: slideDown"/>
Also, I'm using Durandal if it helps.
Question: How can I assign transition animations to observables?
The beforeRemove and afterAdd are just wrappers around the subscribe function.
ko.bindingHandlers['transition'] = {
init: function(element, valueAccessor, allBindings) {
var transition = valueAccessor(),
target = allBindings.get('text'),
subscriptions = [
target.subscribe(target, onChange),
target.subscribe(target, onBeforeChange, 'beforeChange')
];
function onChange() {
// handle transition when target updates
}
function onBeforeChange() {
// handle transition BEFORE target updates
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
subscription[0].dispose();
subscription[1].dispose();
});
}
};
You can create a custom binding handler which does the animation for you:
ko.bindingHandlers.textSlide = {
init: function(element, valueAccessor) {
$(element).text(ko.unwrap(valueAccessor()));
},
update: function(element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
if (value != $(element).text()) {
$(element).slideUp(function() {
$(this).text(value).slideDown();
});
}
}
};
See http://jsfiddle.net/ro0u9Lmb/
I'm building a mobile app which requires a bunch of open/close tab. I'm trying to find a way to use bindingHandlers to reduce the amount of code. But I seem to miss something. Here's my fiddle.
http://jsfiddle.net/noppanit/4zRrZ/
And this is what I have
<a href="javascript:void(0)" data-bind="click: expandCommentsRatings">Rating
<div style="display:none" data-bind="visible: productCommentsRatingsVisiblity">
<div class="rating" style="width: 85%">3.5 Stars Rating</div>
</div>
</a>
<br/>
<a href="javascript:void(0)" data-bind="click: expandsReviews">Reviews
<div style="display:none" data-bind="visible: productReviewsVisiblity">
<div class="reviews">Reviews</div>
</div>
</a>
var Model = function () {
var productCommentsRatingsVisiblity = ko.observable(false);
var productReviewsVisiblity = ko.observable(false);
var expandCommentsRatings = function (item, event) {
productCommentsRatingsVisiblity(!productCommentsRatingsVisiblity());
if (productCommentsRatingsVisiblity() === false) {
$(event.target).removeClass('expanded');
} else {
$(event.target).addClass('expanded');
}
};
var expandsReviews = function (item, event) {
productReviewsVisiblity(!productReviewsVisiblity());
if (productReviewsVisiblity() === false) {
$(event.target).removeClass('expanded');
} else {
$(event.target).addClass('expanded');
}
};
return {
productCommentsRatingsVisiblity: productCommentsRatingsVisiblity,
productReviewsVisiblity: productReviewsVisiblity,
expandCommentsRatings: expandCommentsRatings,
expandsReviews: expandsReviews
}
};
ko.applyBindings(Model());
How do I reduce the duplication so I can reuse this code to other ViewModel as well. The reason I'm struggling is because I don't know how to pass productCommentsRatingsVisiblity or productReviewsVisiblity to allBindings dynamically. You need to know the name in order to get it.
Thanks.
Sorry for the late reply on this, but I have a solution using bindingHandlers.
The fiddle is here: http://jsfiddle.net/u3m7m/1/
I followed a strategy of creating a toggle bindingHandler which adds the specified class if it's not present on the element, or removes the class if it is. The only state needed to make this happen is the class list on the element, meaning you can delete all those state tracking observables from the model. In fact, this was the model I used:
var Model = function () {
// stuff
};
ko.applyBindings(Model());
The toggle bindingHandler looks like this:
ko.bindingHandlers['toggle'] = {
init: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor()),
clickHandler = function (e) {
if (!e) {
e = window.event;
}
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
}
var classes = (this.className||'').split(' '),
index = classes.indexOf(value);
if (index >= 0) {
classes.splice(index, 1);
} else {
classes.push(value);
}
element.className = classes.join(' ');
};
element.onclick = clickHandler;
if (element.captureEvents) {
element.captureEvents(Event.CLICK);
}
}
};
Which is hopefully not too complicated, the weird looking stuff with the e object is from here: http://www.quirksmode.org/js/introevents.html
Because I'm using the strategy of using classes only, I had to add to your CSS:
.expandable > div
{
display: none;
}
.expandable.expanded > div
{
display: block;
}
The state tracking is now removed from the html, and the data-bind is modified to use the toggle bindingHandler:
<a class="expandable" href="javascript:void(0)" data-bind="toggle: 'expanded'">Rating
<div>
<div class="rating" style="width: 85%">3.5 Stars Rating</div>
</div>
</a>
<br/>
<a class="expandable" href="javascript:void(0)" data-bind="toggle: 'expanded'">Reviews
<div>
<div class="reviews">Reviews</div>
</div>
</a>
Hopefully this is of some help to you.
I'm not sure this would help you,
I've reconstruct and optimize your code based on what you need.
This might give you some idea. You don't need custom binding handler to implement this.
here the working jsFiddle: http://jsfiddle.net/farizazmi/6E4Wz/2/
so, what do you need is to include property to control visibility of the item:
var data = [
{
'name' : 'test1',
'rateIsExpanded' : ko.observable(false),
'rating': 3.5,
'review': 'blabla1',
'reviewIsExpanded': ko.observable(false)
},
{
'name' : 'test2',
'rateIsExpanded' : ko.observable(false),
'rating': 1.5,
'review': 'blabla2',
'reviewIsExpanded': ko.observable(false)
}
];
and create a function will use to change state of visibility each data:
var Model = function () {
var self = this;
self.data = ko.observableArray(data);
self.expandRate = function(item)
{
console.log(ko.toJSON(item));
item.rateIsExpanded( ! item.rateIsExpanded() );
};
self.expandReview = function(item)
{
item.reviewIsExpanded( ! item.reviewIsExpanded() );
};
};
ko.applyBindings(Model());
You can do this simply by using an observableArray to hold your menu system, with properties for:
itemName - to hold top level menu items
expanded - to control the expansion of a submenu with child items
subMenu - to hold child items
On top of this, you need a simple function to toggle the visibility of each sub-menu when the parent is clicked. Then you can utilise the knockout visible attribute in your data-binding, which would be bound to the expanded property.
Here's a working JSFiddle and below is the code used:
JS view model:
var Model = function () {
var self = this;
self.tabs = ko.observableArray([
{ itemName: "Ratings",
expanded: ko.observable(false),
subMenu: ["option 1","option 2"]},
{ itemName: "Review",
expanded: ko.observable(false),
subMenu: ["option 1","option 2"]}
]);
self.toggleExpanded = function (item) {
item.expanded(!item.expanded());
}
};
ko.applyBindings(Model());
HTML Mark Up:
<ul data-bind="foreach: tabs">
<li><span data-bind="text: itemName, click: toggleExpanded"></span>:
<ul data-bind="foreach: subMenu">
<li data-bind="text: $data, visible: $parent.expanded">
</li>
</ul>
</li>
</ul>
Is there anyway to trigger an update of the title attribute of my element in this JS Fiddle:
http://jsfiddle.net/YPXYJ/9/
Note that the tooltip in the data-bind attribute of the element is part of the knockout-bootstrap.js library
<label data-bind="text: copyOtherPartyHelpText()"></label>
<br />
<br />
<i class="icon-question-sign" data-bind="tooltip: { title: copyOtherPartyHelpText(), placement: 'top', trigger: 'hover' }"></i>
<br />
<br />
<a style="cursor: pointer;" data-bind="click:changeHelpText">Click HERE To Change Label Text</a>
function MyViewModel() {
this._copyOtherPartyHelpText = ko.observable();
this.readOnlyView = ko.observable(true);
this.copyOtherPartyHelpText = ko.computed({
read: function () {
var value = this._copyOtherPartyHelpText();
if (value) {
return value;
}
if (this.readOnlyView()) {
value = 'Currently Disabled';
} else {
value = 'Match/agree to this term.';
}
//this makes things even worse, it is an initialization workaround
//_copyOtherPartyHelpText(value);
return value;
},
write: function (value) {
this._copyOtherPartyHelpText(value);
},
owner: this
});
this.changeHelpText = function(){
this.copyOtherPartyHelpText('help text updated but not tooltip');
}
}
ko.applyBindings(new MyViewModel());
The console/browser error log will tell you:
Uncaught ReferenceError: copyOtherPartyHelpText is not defined
You'd have to reference your function calls with this. or the inner function will go looking for window.copyOtherPartyHelpText instead.
I'd recommend using a local variable named self (as they often do in the knockoutjs documentation and tutorials) in your view model, so you can always reference its properties safely and easily from inside, as demonstrated in your modified JSFiddle: http://jsfiddle.net/YPXYJ/3/
function MyViewModel() {
var self = this;
// More code here...
this.changeHelpText = function(){
alert('changeHelpText called');
self.copyOtherPartyHelpText('help text and UI updated');
}
}
EDIT2:
Inside the tooltip binding for the title, you don't call the value accessor but instead reference to the observable function like so:
old:
<i class="icon-question-sign" data-bind="tooltip: { title: copyOtherPartyHelpText(), placement: 'top', trigger: 'hover' }"></i>
new:
<i class="icon-question-sign" data-bind="tooltip: { title: copyOtherPartyHelpText, placement: 'top', trigger: 'hover' }"></i>
See: http://jsfiddle.net/YPXYJ/11/
You needed the "this." when reffering to 'this._copyOtherPartyHelpText()' and 'this.copyOtherPartyHelpText()'
here you go http://jsfiddle.net/FtMdZ/2/
ko.observable();
this.readOnlyView = ko.observable(true);
this.copyOtherPartyHelpText = ko.computed({
read: function () {
var value = this._copyOtherPartyHelpText();
if (value) {
return value;
}
if (this.readOnlyView()) {
value = 'Currently Disabled';
} else {
value = 'Match/agree to this term.';
}
//this makes things even worse, it is an initialization workaround
//_copyOtherPartyHelpText(value);
return value;
},
write: function (value) {
this._copyOtherPartyHelpText(value);
},
owner: this
});
this.changeHelpText = function(){
alert('changeHelpText called');
this.copyOtherPartyHelpText('help text and UI updated');
}
}
ko.applyBindings(new MyViewModel());