I know variations of this have been asked, but my requirement is that instead of formatting/rounding the value that's entered, I need to not even allow the user to enter more than one number after the decimal point. I have something that works with a decimalFormatter binding handler, but it's clunky - i.e., it allows you to enter a value like '20.' and you can't erase all the values due to the way I have the regular expression written. I realize I could modify that to accept optional digits for the whole entry, but what I really want is for it to revert to 0.0 if the user erases the value. And, allow for them to enter '20.' then tab out (onblur) and have it reformat the value to 20.0 so it looks more complete. Currently, if the user enters '20.' then saves the form, it does store a whole value of 20, and when you reopen/retrieve the value from the database, it shows 20.0 so it looks fine when reloading.
I thought about adding an onblur event but we don't want to do that in overriding what has been databound by knockout. We want to keep everything bound by the ko view model, so that's not an option. I've also seen some suggest a computed observable (for example this SO post) but my binding already does that. On second thought, would the knockout event binding be the way to go here???
html:
<input style="width:38px" data-bind="decimalFormatter: percentage"/>
javascript:
ko.bindingHandlers.decimalFormatter = {
init: function (element, valueAccessor) {
var initialValue;
//$(element).on('keydown', function (event) {
$(element).keydown(function (event) {
initialValue = $(element).val();
// Allow: backspace, delete, tab, escape, and enter
if (event.keyCode == 46 || event.keyCode == 8 || event.keyCode == 9 || event.keyCode == 27 || event.keyCode == 13 ||
// Allow: Ctrl combinations
(event.ctrlKey === true) ||
// Allow decimal/period
(event.keyCode === 110) || (event.keyCode === 190) ||
// Allow: home, end, left, right
(event.keyCode >= 35 && event.keyCode <= 39)) {
// let it happen, don't do anything
return;
}
else {
// Ensure that it is a number and stop the keypress
if (event.shiftKey || (event.keyCode < 48 || event.keyCode > 57) && (event.keyCode < 96 || event.keyCode > 105)) {
event.preventDefault();
}
}
});
$(element).keyup(function (event) {
if (!$(element).val().match(/^\d+\.?\d?$/)) {
event.preventDefault();
$(element).val(initialValue);
}
else
return;
});
var observable = valueAccessor();
var interceptor = ko.computed({
read: function () { return formatWithComma(observable(), 1); },
write: function (newValue) {
observable(reverseFormat(newValue, 1));
}
});
if (element.tagName == 'INPUT')
ko.applyBindingsToNode(element, { value: interceptor });
else
ko.applyBindingsToNode(element, { text: interceptor });
},
update: function (element, valueAccessor) {
}
}
// Formatting Functions
function formatWithComma(x, precision, seperator) {
var options = {
precision: precision || 2,
seperator: seperator || '.'
}
var formatted = parseFloat(x, 10).toFixed(options.precision);
var regex = new RegExp('^(\\d+)[^\\d](\\d{' + options.precision + '})$');
formatted = formatted.replace(regex, '$1' + options.seperator + '$2');
return formatted;
}
function reverseFormat(x, precision, seperator) {
var options = {
precision: precision || 2,
seperator: seperator || '.'
}
var regex = new RegExp('^(\\d+)[^\\d](\\d+)$');
var formatted = x.replace(regex, '$1.$2');
return parseFloat(formatted);
}
var viewModel = function () {
var self = this;
self.percentage = ko.observable(20.0);
};
var vm = new viewModel();
ko.applyBindings(vm);
JSFiddle