Number input box in Knockout JS - javascript

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.

Related

AngularJS currency filter on input field

I have the following input field
<input type="text" class="form-control pull-right" ng-model="ceremony.CeremonyFee | number:2">
it is showing up correctly but has been disabled. The error I am receiving is "[ngModel:nonassign] Expression 'ceremony.CeremonyFee | number:2' is non-assignable". I understand why it is in error, but do not know how to get this to work on an input field. Thanks.
input with ng-model is for inputting data, number filter is for displaying data. As filter values are not bindable, they are not compatible, as you can see. You have to decide what you want to do with that input.
Do you want it to be an input? User can input his own number and you only needs to validate? Use i.e. pattern attribute:
<input type="text" ng-model="ceremony.CeremonyFee" pattern="[0-9]+(.[0-9]{,2})?">
Do you want it to be an output? User does not need to input his own value? Do not use ng-model, use value instead:
<input type="text" value="{{ceremony.CeremonyFee | number:2}}" readonly>
UPDATE:
really I don't understand what you need, but, if you want just that users can insert only two digits you should use a simple html attributes, have a look on min, max, step...
Follows a pure js solution, but I don't suggest something like that!
angular.module('test', []).controller('TestCtrl', function($scope) {
var vm = $scope;
var testValue = 0;
Object.defineProperty(vm, 'testValue', {
get: function() { return testValue; },
set: function(val) {
val = Number(val);
if(angular.isNumber(val) && (val < 100 && val > 0)) {
console.log(val);
testValue = val;
}
}
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<section ng-app="test">
<div ng-controller="TestCtrl">
<input style="display:block; width: 100%; padding: 1em .5em;" type="number" ng-model="testValue" />
</div>
</section>
the ng-model directive requires a viewmodel assignable (or bindable) property, so, you cannot add a pipe...
angular.module('test', [])
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="test" ng-init="testValue = 0">
<label ng-bind="testValue | currency"></label>
<input style="display:block;" ng-model="testValue" type="number"/>
</div>
As an error states you have got an 'non-assignable' expression in your ng-model attribute.
You should use only ceremony.CeremonyFee.
| is used on ng-repeat to indicate what expression should be used as filter.
If you want to have that <input> populated with initial data in your controller/link you should give it an initial value ex.
$scope.ceremony = {
CeremonyFee: 'My first ceremony'
}
And every time your <input> element data will be changed CeremonyFee will be updated as well.
I found and used the solution found on this page.
http://jsfiddle.net/k7Lq0rns/1/
'use strict';
angular.module('induction').$inject = ['$scope'];
angular.module('induction').directive('format',['$filter', function ($filter) {
  return {
require: '?ngModel',
link: function (scope, elem, attrs, ctrl) {
if (!ctrl) return;
ctrl.$formatters.unshift(function (a) {
return $filter(attrs.format)(ctrl.$modelValue)
});
elem.bind('blur', function(event) {
var plainNumber = elem.val().replace(/[^\d|\-+|\.+]/g, '');
elem.val($filter(attrs.format)(plainNumber));
});
}
  };
}]);
relatively easy to apply it.

Convert value into lower case before knockout binding

Demo Here
I bound a label with knockoutjs. The value bound always should be in lower case. While it will remain in uppercase in js model. How to do this ?
Javascript
var model = {
name:ko.observable("Test")
}
ko.applyBindings(model);
HTML
<label data-bind="text:name">
you just need to use toLowerCase in the view
view :
<div class='liveExample'>
<p> name: <label data-bind='text: name().toLowerCase()'></label></p>
</div>
<b>Original Value:
<pre data-bind="text:ko.toJSON($data,null,2)"></pre>
sample working fiddle here
It's unclear what you want to do, in particular when the value is coming from the textarea, but you can probably do whatever it is using a writable computed:
model.lowerName = ko.computed({
read: function() {
return model.name().toLowerCase();
},
write: function(newValue) {
// ...save this however it is you want to save it...
}
});
HTML:
<input data-bind="value:lowerName">
Re your updated question: Your update completely changes the question. If you don't need updates from the element and are only showing what's in name, you have two options:
A read-only computed:
model.lowerName = ko.pureComputed(function() { return model.name().toLowerCase(); });
HTML:
<label data-bind="text:lowerName"></label>
Just do it in the binding:
<label data-bind="text:name().toLowerCase()"></label>

AngularJS form and null/empty values

I am working with a somewhat dynamic AngularJS form. In other words, I am able to add rows of input fields, etc. So my approach was to start with a $scope.formData empty object, to encapsulate all the properties that are bound to both static and dynamic HTML form elements.
The AngularJS code is as follows:
(function() {
var formApp = angular.module("formApp", []);
formApp.controller("FormCtrl", function ($scope, $timeout) {
$scope.formData = {};
$scope.formData.subscribers = [
{ name: null, email: null }
];
$scope.addSubscriber = function() {
$scope.formData.subscribers.push({ name: null, email: null });
};
});
})();
The HTML for the AngularJS form:
<body data-ng-app="formApp">
<div data-ng-controller="FormCtrl">
<p>
Name of Topic: <input type="text" data-ng-model="formData.title" placeholder="enter a title" />
</p>
Subscribers:
<button data-ng-click="addSubscriber()">Add subscriber</button>
<table>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
<tr data-ng-repeat="subscriber in formData.subscribers">
<td><input type="text" data-ng-model="subscriber.name" placeholder="enter name" /></td>
<td><input type="text" data-ng-model="subscriber.email" placeholder="enter email" /></td>
</tr>
</table>
<hr style="margin:1em 0;" />
<p>
<em>Debug info</em>: {{ formData }}
</p>
</div>
</body>
Note the Debug info section at the end, which displays the $scope.formData object and its contents. I have a couple of issues with the way I have implemented this form.
When the page first loads, there is no formData.title property in the $scope, but since it is bound to the Title input text field, when I start typing a value, the title property gets added to the $scope. However when I delete the value in the input text field, the formData.title property still exists in the $scope as an empty string. I suppose this is okay, but I really want to clean up empty or null values when submitting the form. I would like to do it on the client side if it is easy to do so, so the server side code does not have to clean up anything.
With the dynamic Subscribers section, I can keep adding as many rows as I want, but ultimately, I would like to filter out all empty subscriber objects on the client side.
Does AngularJS have any options for detecting and cleaning null/empty values in the $scope before further processing, such as a $http POST?
Note I have set up a jsFiddle for this example.
Simply use ngModelController $parsers and overwrite the default HTML input element.
With this implementation you can have control over the model value all the time.
So in your case you can set the model to null whenever the view value is empty String.
var inputDefinition = function () {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attr, ngModel) {
if (ngModel) {
var convertToModel = function (value) {
return value === '' ? null : value;
};
ngModel.$parsers.push(convertToModel);
}
}
};
/**
* Overwrite default input element.
*/
formApp.directive('input', inputDefinition);
Here is the updated JSFiddle: https://jsfiddle.net/9sjra07q/
I try and avoid using watchers for performance reasons. With that being said, this really isn't as much of an Angular question as it is a JavaScript question. Since you have total control over when the data is passed off to the service I would simply clean it up first. This is fairly simple since your data structure is so shallow.
https://jsfiddle.net/1ua6oj5e/9/
(function() {
var formApp = angular.module("formApp", []);
formApp.controller("FormCtrl", function ($scope, $timeout) {
// Remove junkiness
var _remove = function remove(item) {
if ($scope.formData.title === undefined || $scope.formData.title === '') {
delete $scope.formData.title;
}
};
$scope.formData = {};
$scope.formData.subscribers = [
{ name: null, email: null }
];
$scope.addSubscriber = function() {
$scope.formData.subscribers.push({ name: null, email: null });
};
// Submit to test functionality
$scope.submit = function() {
// Remove title if empty
_remove($scope.formData.title);
/* Remove name and email if empty.
* If your list of fields starts to get large you could always
* just nest another iterator to clean things up.
*/
angular.forEach($scope.formData.subscribers, function(sub) {
_remove(sub.name);
_remove(sub.email);
});
};
});
})();
function replacer(key, value) {
if (value == "" || value == null) {
return undefined;
}
return value;
}
var foo = {foundation: "", model: {year: 2015, price:null}, week: 45, transport: "car", month: 7};
foo = JSON.stringify(foo, replacer);
foo =JSON.parse(foo);
console.log(foo);
Added Watcher on formData,
$scope.$watch('formData',function(n,o){
if(typeof $scope.formData.title !== 'undefined' && $scope.formData.title === "" ){
delete $scope.formData.title;
}
},true);
Updated fiddle: https://jsfiddle.net/1ua6oj5e/6/
For all the dynamic fields you should use angular form validation, you should see this: https://docs.angularjs.org/guide/forms

Pass a function that returns the value of the ko.computed error during submit

I'm having trouble submitting a form with knockout js.
I receive the error "Pass a function that returns the value of the ko.computed."
The code is as follows:
(function(records,$,undefined){
records.models={
student:function(data){
var self=this;
self.id=ko.observable(data.id);
self.fname=ko.observable(data.fname);
self.lname=ko.observable(data.lname);
if(data.initial==='undefined'||data.initial===null){
self.initial=ko.observable("");
}else{
self.initial=ko.observable(data.initial);
}
self.fullname=ko.computed(function(){
return self.fname()+" "+" "+self.initial()+" "+self.lname();
});
},
students_model:function(){
var self=this;
self.selectedStudent=ko.observable();
self.students=ko.observableArray([]);
getStudents();
self.save=function(){
var form=$("#student-form");
$.ajax({
type:"POST",
url:"/Student/create",
data:ko.toJSON(form[0]), //This line here is the exact point of failue
success:function(response){
records.general.handleSuccess(response);
if(response.status){
getStudents();
}
}
});
return false;
};
function getStudents(){
$.getJSON("/Student/data",function(result){
var mapped=$.map(result,function(item){
return new records.models.student(item);});
self.students(mapped);
});
}
}
};
return records;
}(window.records=window.records||{},jQuery));
HTML
#using (Ajax.BeginForm("Create", "Student",
new AjaxOptions
{
HttpMethod = "Post"
},
new { #class = "student-form", name = "student-form", id = "student-form" }))
{
<input type="text" data-bind="value:$root.fname" id="student.fname" name="student.fname" />
<input type="text" data-bind="value:$root.lname" id="student.lname" name="student.lname"/>
<input type="text" data-bind="value:$root.initial" id="student.initial" name="student.initial"/>
<input type="text" data-bind="value:$root.dob" id="dob" name="dob" />
<button data-bind="click:save">Save</button>
}
<script type="text/javascript">
ko.applyBindings(new records.models.students_model());
</script>
What am I doing wrong here? I'm aware of this question here:Pass a function that returns the value of the ko.computed
But it seems like that individual had a different problem. My code fails when starting in the save method. Specifically the line:
data:ko.toJSON(form[0])
ko.toJSON is expecting you to pass it your viewModel, but you're passing it an element from the DOM, thus the error.
You need to pass a javascript object (a viewmodel or part of your viewmodel) to ko.toJSON. For example, if you wanted to send up the array of students, you could do this:
ko.toJSON(self.students);
I see that your form has some inputs bound to $root.fname, $root.lname, $root.initial, and $root.dob, but I'm not sure where those exist in your viewmodel, so I can't tell you exactly what to pass. But I can give you an example of one way could could solve this.
If you have a viewmodel that looks like this:
var data = ...;
var vm = {
newStudent : {
fname : ko.observable(data.fname),
lname: ko.observable(data.lname),
initial: ko.observable(data.initial ?? ""),
dob: ko.observable(data.dob)
}
}
and then you bound this to your dom by calling
ko.applyBindings(vm);
You could then call ko.toJSON like this:
...
data:ko.toJSON(vm.newStudent),
...

How can I get Knockout JS to data-bind on keypress instead of lost-focus?

This example of knockout js works so when you edit a field and press TAB, the viewmodel data and hence the text below the fields is updated.
How can I change this code so that the viewmodel data is updated every keypress?
<!doctype html>
<html>
<title>knockout js</title>
<head>
<script type="text/javascript" src="js/knockout-1.1.1.debug.js"></script>
<script type="text/javascript">
window.onload= function() {
var viewModel = {
firstName : ko.observable("Jim"),
lastName : ko.observable("Smith")
};
viewModel.fullName = ko.dependentObservable(function () {
return viewModel.firstName() + " " + viewModel.lastName();
});
ko.applyBindings(viewModel);
}
</script>
</head>
<body>
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
</body>
</html>
<body>
<p>First name: <input data-bind="value: firstName, valueUpdate: 'afterkeydown'" /></p>
<p>Last name: <input data-bind="value: lastName, valueUpdate: 'afterkeydown'" /></p>
<h2>Hello, <span data-bind="text: fullName"> </span>!</h2>
</body>
From the documentation
Additional parameters
valueUpdate
If your binding also includes a parameter called valueUpdate, this
defines which browser event KO should use to detect changes. The
following string values are the most commonly useful choices:
"change" (default) - updates your view model when the user
moves the focus to a different control, or in the case of
elements, immediately after any change
"keyup" - updates your view model when the user releases a key
"keypress" - updates your view model when the user has typed a
key. Unlike keyup, this updates repeatedly while the user holds a key
down
"afterkeydown" - updates your view model as soon as the user
begins typing a character. This works by catching the browser’s
keydown event and handling the event asynchronously.
Of these options, "afterkeydown" is the best choice if you want to
keep your view model updated in real-time.
In version 3.2 you can simply use textinput binding.:
<input data-bind="textInput: userName" />
It does two important things:
make immediate updates
handles browser differences for cut, drag, autocomplete ...
So no need for additional modules, custom controls and other things.
If you want it to do updates on afterkeydown "by default," you could inject the valueUpdate binding in the value binding handler. Just supply a new allBindingsAccessor for the handler to use that includes afterkeydown.
(function () {
var valueHandler = ko.bindingHandlers.value;
var getInjectValueUpdate = function (allBindingsAccessor) {
var AFTERKEYDOWN = 'afterkeydown';
return function () {
var allBindings = ko.utils.extend({}, allBindingsAccessor()),
valueUpdate = allBindings.valueUpdate;
if (valueUpdate === undefined) {
return ko.utils.extend(allBindings, { valueUpdate: AFTERKEYDOWN });
} else if (typeof valueUpdate === 'string' && valueUpdate !== AFTERKEYDOWN) {
return ko.utils.extend(allBindings, { valueUpdate: [valueUpdate, AFTERKEYDOWN] });
} else if (typeof valueUpdate === 'array' && ko.utils.arrayIndexOf(valueUpdate, AFTERKEYDOWN) === -1) {
valueUpdate = ko.utils.arrayPushAll([AFTERKEYDOWN], valueUpdate);
return ko.utils.extend(allBindings, {valueUpdate: valueUpdate});
}
return allBindings;
};
};
ko.bindingHandlers.value = {
// only needed for init
'init': function (element, valueAccessor, allBindingsAccessor) {
allBindingsAccessor = getInjectValueUpdate(allBindingsAccessor);
return valueHandler.init(element, valueAccessor, allBindingsAccessor);
},
'update': valueHandler.update
};
} ());
If you're not comfortable "overriding" the value binding, you could give the overriding custom binding a different name and use that binding handler.
ko.bindingHandlers.realtimeValue = { 'init':..., 'update':... };
demo
A solution like this would be suitable for Knockout version 2.x. The Knockout team has fleshed out a more complete binding for text-like inputs through the textInput binding in Knockout version 3 and up. It was designed to handle all text input methods for text inputs and textarea. It will even handle real time updating which effectively renders this approach obsolete.
Jeff Mercado's answer is fantastic, but unfortunately broken with Knockout 3.
But I found the answer suggested by the ko devs while working through Knockout 3 changes. See the bottom comments at https://github.com/knockout/knockout/pull/932. Their code:
//automatically add valueUpdate="afterkeydown" on every value binding
(function () {
var getInjectValueUpdate = function (allBindings) {
return {
has: function (bindingKey) {
return (bindingKey == 'valueUpdate') || allBindings.has(bindingKey);
},
get: function (bindingKey) {
var binding = allBindings.get(bindingKey);
if (bindingKey == 'valueUpdate') {
binding = binding ? [].concat(binding, 'afterkeydown') : 'afterkeydown';
}
return binding;
}
};
};
var valueInitHandler = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function (element, valueAccessor, allBindings, viewModel, bindingContext) {
return valueInitHandler(element, valueAccessor, getInjectValueUpdate(allBindings), viewModel, bindingContext);
};
}());
http://jsfiddle.net/mbest/GKJnt/
Edit
Ko 3.2.0 now has a more complete solution with the new "textInput" binding. See SalvidorDali's answer

Categories

Resources