I have a bunch of amounts in my view model which are observables, and I want them stored as numbers because they are used in several calculations. However, when I bind them to textboxes in my view, I want them shown as a specially formatted string (1234.5678 => "1,234.57"). What is the best way to accomplish two-way binding in this situation, since I can't just use the value binding with my observable?
The user needs to be able to enter "1,234.56" or "1234.56" in the textbox which will store the number value 1234.56 in the observable, and if I change the number value (3450) via javascript, the textbox value needs to be updated to the new value, but formatted as a string ("3,450").
I appreciate the help!
I would implement something like the following:
JavaScript:
function ViewModel() {
var self = this;
self.dollarAmount = ko.observable();
self.formattedDollarAmount = ko.computed({
read: function() {
return applyFormat(self.dollarAmount()); // pseudocode
},
write: function(value) {
var parsedDollarAmount = parseFloat(value);
if(!isNaN(parsedDollarAmount)) {
self.dollarAmount(parsedDollarAmount);
}
else {
// recommend adding validation of some sort
// so getting here is less likely
alert("invalid dollar amount");
}
},
owner: self
});
}
HTML:
<input type="text" data-bind="value: formattedDollarAmount" />
See the documentation on computed observables for more info: http://knockoutjs.com/documentation/computedObservables.html
see :
http://jsfiddle.net/Ty8PG/14/
ko.bindingHandlers.numericValue = {
init : function(element, valueAccessor, allBindingsAccessor) {
var underlyingObservable = valueAccessor();
var interceptor = ko.computed({
read: underlyingObservable,
write: function(value) {
underlyingObservable(rawNumber(value));
}
});
ko.bindingHandlers.value.init(element, function() { return interceptor }, allBindingsAccessor);
},
update : function(element, valueAccessor, allBindingsAccessor) {
element.value = number_format(valueAccessor()(), 2);
}
};
Original POST :
https://groups.google.com/forum/#!topic/knockoutjs/gvhJt4iSOLk
Related
I'm starting with knockout and my computed observable seems to fire always when the viewmodel is instantiated and i don't know why.
I've reduced the problem to the absurd just for testing: the computed property just prints a message in the console and it is not binded to any element at the DOM. Here it is:
(function() {
function HomeViewModel() {
var self = this;
(...)
self.FullName = ko.computed(function () {
console.log("INSIDE");
});
(...)
};
ko.applyBindings(new HomeViewModel());
})();
How can it be avoided?
Update:
Here is the full code of the ViewModel just for your better understanding:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.FilterByName = ko.observable('');
self.FilterByLastName = ko.observable('');
self.FilteredTeachers = ko.observableArray([]);
self.FilteredStudents = ko.observableArray([]);
self.FilteredUsersComputed = ko.computed(function () {
var filteredTeachers = self.teachers().filter(function (user) {
return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
);
});
self.FilteredTeachers(filteredTeachers);
var filteredStudents = self.students().filter(function (user) {
return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
);
});
self.FilteredStudents(filteredStudents);
$("#LLAdminBodyMain").fadeIn();
}).extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.FilteredUsersComputed.subscribe(function () {
setTimeout(function () { $("#LLAdminBodyMain").fadeOut(); }, 200);
}, null, "beforeChange");
$.getJSON("/api/User/Teacher", function (data) {
self.teachers(data);
});
$.getJSON("/api/User/Student", function (data) {
self.students(data);
});
}
ko.applyBindings(new HomeViewModel());
})();
I need it to not be executed on load because on load the self.students and self.teachers arrays are not jet populated.
NOTE: Just want to highlight that in both codes (the absurd and full), the computed property is executed on loading (or when the ViewModel is first instantiated).
There are two main mistakes in your approach.
You have a separate observable for filtered users. That's not necessary. The ko.computed will fill that role, there is no need to store the computed results anywhere. (Computeds are cached, they store their own values internally. Calling a computed repeatedly does not re-calculate its value.)
You are interacting with the DOM from your view model. This should generally be avoided as it couples the viewmodel to the view. The viewmodel should be able operate without any knowledge of how it is rendered.
Minor points / improvement suggestions:
Don't rate-limit your filter result. Rate-limit the observable that contains the filter string.
Don't call your computed properties ...Computed - that's of no concern to your view, there is no reason to point it out. For all practical purposes inside your view, computeds and observables are exactly the same thing.
If teachers and students are the same thing, i.e. user objects to be displayed in the same list, why have them in two separate lists? Would it not make more sense to have a single list in your viewmodel, so you don't need to filter twice?
Observables are functions. This means
$.getJSON("...", function (data) { someObservable(data) });
can be shortened to
$.getJSON("...", someObservable);.
Here is a better viewmodel:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.filterByName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.filterByLastName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
function filterUsers(userList) {
var name = self.filterByName().toUpperCase(),
lastName = self.filterByLastName().toUpperCase(),
allUsers = userList();
if (!name && !lastName) return allUsers;
return allUsers.filter(function (user) {
return (!name || user.name.toUpperCase().includes(name)) &&
(!lastName || user.lastName.toUpperCase().includes(lastName));
});
}
self.filteredTeachers = ko.computed(function () {
return filterUsers(self.teachers);
});
self.filteredStudents = ko.computed(function () {
return filterUsers(self.students);
});
self.filteredUsers = ko.computed(function () {
return self.filteredTeachers().concat(self.filteredStudents());
// maybe sort the result?
});
$.getJSON("/api/User/Teacher", self.teachers);
$.getJSON("/api/User/Student", self.students);
}
With this it does not matter anymore that the computeds are calculated immediately. You can bind your view to filteredTeachers, filteredStudents or filteredUsers and the view will always reflect the state of affairs.
When it comes to making user interface elements react to viewmodel state changes, whether the reaction is "change HTML" or "fade in/fade out" makes no difference. It's not the viewmodel's job. It is always the task of bindings.
If there is no "stock" binding that does what you want, make a new one. This one is straight from the examples in the documentation:
// Here's a custom Knockout binding that makes elements shown/hidden via jQuery's fadeIn()/fadeOut() methods
// Could be stored in a separate utility library
ko.bindingHandlers.fadeVisible = {
init: function(element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function(element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
It fades in/out the bound element depending the bound value. It's practical that the empty array [] evaluates to false, so you can do this in the view:
<div data-bind="fadeVisible: filteredUsers">
<!-- show filteredUsers... --->
</div>
A custom binding that fades an element before and after the bound value changes would look like follows.
We subscribe to value during the binding's init phase.
There is no update phase in the binding, everything it needs to do is accomplished by the subscriptions.
When the DOM element goes away (for example, because a higher-up if or foreach binding triggers) then our binding cleans up the subscriptions, too.
Let's call it fadeDuringChange:
ko.bindingHandlers.fadeDuringChange = {
init: function(element, valueAccessor) {
var value = valueAccessor();
var beforeChangeSubscription = value.subscribe(function () {
$(element).delay(200).fadeOut();
}, null, "beforeChange");
var afterChangeSubscription = value.subscribe(function () {
$(element).fadeIn();
});
// dispose of subscriptions when the DOM node goes away
// see http://knockoutjs.com/documentation/custom-bindings-disposal.html
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
// see http://knockoutjs.com/documentation/observables.html#explicitly-subscribing-to-observables
beforeChangeSubscription.dispose();
afterChangeSubscription.dispose();
});
}
};
Usage is the same as above:
<div data-bind="fadeDuringChange: filteredUsers">
<!-- show filteredUsers... --->
</div>
I have this spinner.
When set it is ok, control shows the right value, no error message, but if I change it, every time I get this error message:
"TypeError: observable is not a function"
in this line:
observable($(element).spinner("value"));
This is a line in my customize binding, which goes like this:
ko.bindingHandlers.spinner = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().spinnerOptions || {};
$(element).spinner(options);
//handle the field changing
ko.utils.registerEventHandler(element, "spinchange", function () {
//var observable = valueAccessor();
var observable = ko.utils.unwrapObservable(valueAccessor());
observable($(element).spinner("value"));
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).spinner("destroy");
});
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
current = $(element).spinner("value");
if (value !== current) {
$(element).spinner("value", value);
}
}
};
If I swap the line that fails with the one that is commented out:
var observable = valueAccessor();
it works fine.
Why does the first one fail?
In a custom binding the valueAccessor is a function which returns of the value of the binding, so what you have on the "right side" in the binding.
So in the case of data-bind="spinner: yourValue you will get the content of the yourValue property.
If your yourValue is ko.observable then in var observable = valueAccessor(); the observable will contain your actual observable function from yourValue what you can set it now to any value with:
var observable = valueAccessor();
observable($(element).spinner("value"));
So this is the correct usage in this case because you need to reference and use your observable function and not its value.
However if you use ko.utils.unwrapObservable(valueAccessor()); then its is automatically unwraps your observable so you will end with a value not a function. So this call is roughly equivalent to write valueAccessor()();
So if your view model looks like this:
vm = {
yourValue: ko.observable(5);
}
When you write:
var observable = ko.utils.unwrapObservable(valueAccessor());
observable($(element).spinner("value"));
observable will contain the value 5 and not a function so you get the above mentioned exception.
I have observables in my KnockoutJS app and their values are being fetched from an array that's contained globally in the app, eg:
self.observable = ko.observable(App[0].Observable_Value);
For ease, let's say Observable_Value = 10.
This is working as you'd expect it to and the <input> that self.observable is binded to has the correct value in it by default; 10.
What I want to do now is to append the observable with a % in the <input> so the displayed output in the input field will be 10%.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string to prevent NaN errors later on in the app. The app relies heavily on numbers.
I've tried doing this as a computed function with read/write & parseFloat but to no success.
Any ideas?
I think that the best option in this case would be a custom binding handler.
I simply want to append the input value with a % because I need the observable to be readable as a number and not a string
That is precisely what a binding handler could do - preserve the original value of the observable, but change the way that it is displayed.
I've tried doing this as a computed function
Although a computed function could do the trick, it's usually unecessary unless you want to actually work with the return value from the computed. In this case, since you just want to change the visual display, you don't need a another variable representing the same value.
Here is a very basic one that just puts a % sign in front of the observable's value. See fiddle
ko.bindingHandlers.percent = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = ko.unwrap(valueAccessor());
$(element).text('%' + value);
}
};
You can add a read/write computable that will add the percent for display, then remove it when setting the underlying value (fiddle):
ko.observable.fn.formatAsPercent = function() {
var base = this;
return ko.computed({
read: function() {
return base() + "%";
},
write: function(newValue) {
base(parseInt(newValue.replace('%', '')));
}
});
};
function ViewModel() {
var self = this;
self.number = ko.observable(10); // actual number
self.percent= self.number.formatAsPercent(); // used for binding to show %
}
var my = { vm : new ViewModel() };
ko.applyBindings(my.vm);
I've got a working viewmodel with numerous variables.
I use autoNumeric (http://www.decorplanit.com/plugin/) for text formatting in textbox. I'd like to use the input field's observed data in a computed observable, but if the observable textfield with the formatting gets modified, the formatting also gets saved in the variable.
How can I only observe the value of the input field without the formatting?
I think the best way to this could be a getter/setter to the observable, and remove the formatting when the value is set. I couldn't find a solution in knockout's documentation to write get/set methods for ko.observable(), and ko.computed() can not store a value.
I don't want to use hidden fields, or extra variables.
Is this possible without it?
Solution, as seen on http://knockoutjs.com/documentation/extenders.html
ko.extenders.numeric = function(target, precision) {
//create a writeable computed observable to intercept writes to our observable
var result = ko.computed({
read: target, //always return the original observables value
write: function(newValue) {
var current = target(),
roundingMultiplier = Math.pow(10, precision),
newValueAsNum = isNaN(newValue) ? 0 : parseFloat(+newValue),
valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
//only write if it changed
if (valueToWrite !== current) {
target(valueToWrite);
} else {
//if the rounded value is the same, but a different value was written, force a notification for the current field
if (newValue !== current) {
target.notifySubscribers(valueToWrite);
}
}
}
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
And later on
function AppViewModel(one, two) {
this.myNumberOne = ko.observable(one).extend({ numeric: 0 });
this.myNumberTwo = ko.observable(two).extend({ numeric: 2 });
}
You can use ko.computed() for this. You can specify a write option, see Writeable computed observables
Example (taken from knockout documentation):
function MyViewModel() {
this.price = ko.observable(25.99);
this.formattedPrice = ko.computed({
read: function () {
return '$' + this.price().toFixed(2);
},
write: function (value) {
// Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
value = parseFloat(value.replace(/[^\.\d]/g, ""));
this.price(isNaN(value) ? 0 : value); // Write to underlying storage
},
owner: this
});
}
ko.applyBindings(new MyViewModel());
I think I can easily bind a date data with jquery ui calendar and knockout.js thanks to this answer.
Now I need to bind a date data as well as its time. Of course, I can use timepicker. But I am not sure how I can bind its data with knockout.js. I expected it'd be similar to datepicker so I made following script
ko.bindingHandlers.datetimepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datetimepickerOptions || {};
$(element).datetimepicker(options);
//handle the field changing
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
observable($(element).datetimepicker("getDate"));//****
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datetimepicker("destroy");
});
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
current = $(element).datetimepicker("getDate");
if (value - current !== 0) {
$(element).datetimepicker("setDate", value);
}
}
};
But when I ran the script, I get an error in line of //**** in javascript
TypeError: observable is not a function
But I can't find what I did wrong here.
This particular error is due to the observable = valueAccessor() line. You are assigning to observable the value of valueAccessor by adding the () to the end. In order to pass a value to observable in this way, you would need to write instead:
var observable = valueAccessor;
Otherwise, observable is not an 'observable function'.
I just found following code is working. As few open source code do, this addon is not very stable and will call change event with null observable sometimes. So I made the code to catch the exception and move on.
ko.bindingHandlers.datetimepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datetimepickerOptions || {};
$(element).datetimepicker(options);
//handle the field changing
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
try {
observable($(element).datetimepicker("getDate"));//****
}
catch(ex) {}
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datetimepicker("destroy");
});
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
current = $(element).datetimepicker("getDate");
if (value - current !== 0) {
$(element).datetimepicker("setDate", value);
}
}
};
Replace this line
var observable = valueAccessor();
With
var xxxx= valueAccessor();
Because you cannot use the observable, because it is reserved keyword in knockout.
Also, you may get error somewhere in future if you use observable as variable name.