I'm trying to create a number input box which will accept numbers only.
My initial value approach was to replace value and set it again to itself.
Subscribe approach
function vm(){
var self = this;
self.num = ko.observable();
self.num.subscribe(function(newValue){
var numReg = /^[0-9]$/;
var nonNumChar = /[^0-9]/g;
if(!numReg.test(newValue)){
self.num(newValue.toString().replace(nonNumChar, ''));
}
})
}
ko.applyBindings(new vm())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="textInput: num" />
Now this approach works but will add another cycle of subscribe event, so I tried to use a custom binding so that I can return updated value only. New to it, I tried something but not sure how to do it. Following is my attempt but its not working. Its not even updating the observable.
Custom Binding attempt
ko.bindingHandlers.numeric_value = {
update: function(element, valueAccessor, allBindingsAccessor) {
console.log(element, valueAccessor, allBindingsAccessor())
ko.bindingHandlers.value.update(element, function() {
var value = ko.utils.unwrapObservable(valueAccessor());
return value.replace(/[^0-9]/g, '')
});
},
};
function vm() {
this.num = ko.observable(0);
this.num.subscribe(function(n) {
console.log(n);
})
}
ko.applyBindings(new vm())
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div>
<input type="text" data-bind="number_value: num, valueUpdate:'keyup'">
<span data-bind="text: num"></span>
</div>
So my question is, Can we do this using custom bindings and is it better approach than subscribe one?
Edit 1:
As per @user3297291's answer, ko.extenders
looks more like a generic way for my subscribe approach. I'm looking for an approach (if possible in Knockout), which would clean value before it is set to observable.
I have taken reference from following articles:
- How to update/filter the underlying observable value using a custom binding?
- How can i update a observable in custom bindings?
Note: In the first example, they are using jQuery to set the value. I would like to avoid it and do it using knockout only
I´m on favor of use extender as user3297291's aswer.
Extenders are a flexible way to format or validate observables, and more reusable.
Here is my implementation for numeric extender
//Extender
ko.extenders.numeric = function(target, options) {
//create a writable computed observable to intercept writes to our observable
var result = ko.pureComputed({
read: target, //always return the original observables value
write: function(newValue) {
var newValueAsNum = options.decimals ? parseFloat(newValue) : parseInt(newValue);
var valueToWrite = isNaN(newValueAsNum) ? options.defaultValue : newValueAsNum;
target(valueToWrite);
}
}).extend({
notify: 'always'
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
//View Model
var vm = {
Product: ko.observable(),
Price: ko.observable().extend({
numeric: {
decimals: 2,
defaultValue: undefined
}
}),
Quantity: ko.observable().extend({
numeric: {
decimals: 0,
defaultValue: 0
}
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Edit
I get your point, what about and regular expression custom binding to make it more reusable?
Something like this.
function regExReplace(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var observable = valueAccessor();
var textToReplace = allBindingsAccessor().textToReplace || '';
var pattern = allBindingsAccessor().pattern || '';
var flags = allBindingsAccessor().flags;
var text = ko.utils.unwrapObservable(valueAccessor());
if (!text) return;
var textReplaced = text.replace(new RegExp(pattern, flags), textToReplace);
observable(textReplaced);
}
ko.bindingHandlers.regExReplace = {
init: regExReplace,
update: regExReplace
}
ko.applyBindings({
name: ko.observable(),
num: ko.observable()
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="textInput : name, regExReplace:name, pattern:'(^[^a-zA-Z]*)|(\\W)',flags:'g'" placeholder="Enter a valid name" />
<span data-bind="text : name"></span>
<br/>
<input class=" form-control " type="text " data-bind="textInput : num, regExReplace:num, pattern: '[^0-9]',flags: 'g' " placeholder="Enter a number " />
<span data-bind="text : num"></span>
I think you can divide the problem in to two parts:
- Making sure the user can only input numbers, or
- Making sure your viewmodel value is a number rather than a string.
If you only need part 1, I'd advice you to use default HTML(5) features:
<input type="number" step="1" />
<input type="text" pattern="\d*" />
If you want to make sure the user cannot enter any other than a number, and want to use the value in your viewmodel as well, I'd use an extender. By extending the observable, you can change its value before any subscriptions are fired.
The knockout docs provide an excelent example on their documentation page:
Note that when you use the extender, you don't need to worry about the pattern
or type
attribute anymore; knockout modifies the value instantly as soon as it's set.
ko.extenders.numeric = function(target, precision) {
//create a writable computed observable to intercept writes to our observable
var result = ko.pureComputed({
read: target, //always return the original observables value
write: function(newValue) {
var current = target(),
roundingMultiplier = Math.pow(10, precision),
newValueAsNum = isNaN(newValue) ? 0 : +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);
}
}
}
}).extend({
notify: 'always'
});
//initialize with current value to make sure it is rounded appropriately
result(target());
//return the new computed observable
return result;
};
var vm = {
changes: ko.observable(0),
myNumber: ko.observable(0).extend({
numeric: 1
})
};
vm.myNumber.subscribe(function() {
vm.changes(vm.changes() + 1);
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="text" data-bind="value: myNumber">
<div>2 times <span data-bind="text: myNumber"></span> is <span data-bind="text: myNumber() * 2"></span>.</div>
<div> Changes: <span data-bind="text: changes"></span></div>
Using Knockout Validation | LIVE PEN:
<input data-bind="value: Number">
ko.validation.init();
function VM() {
var self = this;
self.Number = ko.observable().extend({
required: true,
pattern: {
message: 'Invalid number.',
params: /\d$/
}
});
}
ko.applyBindings(window.v=new VM());
Following is a mimic of knockout
's textInput
binding, but with custom parsing. Note, I know, this has added few extra lines of duplicate code but I guess its worth.
I thought of creating my custom code, but reinventing the wheel will have lots of issues, hence appreciating Knockout teams effort and copying it.
Numeric Only - JSFiddle.
Characters Only - JSFiddle
I have updated code in following
- updateModel: To fetch only parsed value from element. This will prevent updating incorrect value.
- updateView: To check if user have entered incorrect value. If yes, replace previous value, else update previous value as current value and proceed.
Usability
I have tried to increase scope of this binding beyond this question. I have added a special data attributes (data-pattern
and data-flag
) to create regex will parse accordingly.