Number input box in Knockout JS

2020-02-01 03:30发布

问题:

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

回答1:

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>



回答2:

I think you can divide the problem in to two parts:

  1. Making sure the user can only input numbers, or
  2. 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>



回答3:

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());


回答4:

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.