AngularJS - input number spinner, not working correctly when number is inputted manually

Solution 1:

That's because input type="text" will return a string as a model value, and as was mentioned by @AlekseySolovey - you'll have to cast it to a Number (since "10" + 1 will give you "101" as a result). As you are using numericOnly directive as well, it seems to be the right place to do the conversion, since you'll wave to do it only in the one place. Here is an example:

(function () {
    "use strict";

    var app = angular
        .module("app", []);

    app.controller('AppController', ['$scope', function ($scope) {
        var vm = this;

        vm.testNumber = 10;
    }]);

    app.directive('numericOnly', function () {
        return {
            require: 'ngModel',
            link: function (scope, element, attrs, modelCtrl) {

                modelCtrl.$parsers.push(function (inputValue) {
                    if (angular.isNumber(inputValue)) {
                        return inputValue;
                    }

                    var transformedInput = inputValue ? Number(inputValue.replace(/[^\d.-]/g, '')) : null;

                    if (transformedInput != inputValue) {
                        modelCtrl.$setViewValue(transformedInput);
                        modelCtrl.$render();
                    }

                    return transformedInput;
                });
            }
        };
    });

    app.directive('numberSpin', [function () {

        return {
            restrict: 'E',
            scope: {
                "ngModel": '='
            },
            template: '<div>' +
            '<input numeric-only data-ng-model="ngModel" ng-pattern="onlyNumbers" type="text">' +
            '<a class="ns-plus"  data-ng-click="plus()">+</a>' +
            '<a class="ns-minus"data-ng-click="minus()">-</a> </div>',
            link: function (scope, elem, attrs) {

                scope.onlyNumbers = /^\d+$/;

                scope.plus = function () {
                    scope.ngModel = scope.ngModel + 1;
                };

                scope.minus = function () {
                    scope.ngModel = scope.ngModel - 1;
                };

            }
        }

    }])


}());
number-spin div {
  position: relative;
  width: 126px;
  
}
number-spin input {
  height: 32px;
  width: 100%;
  text-align: right;
  padding-right: 20px;
  box-sizing: border-box;
  font-size: 16px;
}

number-spin .ns-plus {
  position: absolute;
  text-align: center;
  line-height: 16px;
  top: 0;
  right: 0;
  height: 16px;
  display: block;
  border-left: 1px solid #ccc;
  border-bottom: 1px solid #ccc;
  width: 16px;
}

number-spin .ns-minus {
  position: absolute;
  text-align: center;
  display: block;
  line-height: 16px;
  height: 16px;
  border-left: 1px solid #ccc;
  bottom: 0;
  right: 0;
  width: 16px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
<div ng-app="app" style="padding: 16px; "data-ng-controller="AppController as vm">


<number-spin data-ng-model="vm.testNumber"></number-spin>

</div>

UPDATE: A bit simpler code that allows passing min/max into the directive:

(function () {
    "use strict";

    var app = angular
        .module("app", []);

    app.controller('AppController', ['$scope', function ($scope) {
        var vm = this;

        vm.testNumber = 10;
    }]);

    app.directive('numberSpin', [function () {

        return {
            restrict: 'E',
            scope: {
                "ngModel": '=',
                "min": '<',
                "max": '<',
                "step": '<'
            },
            template: '<div>' +
            '<input data-ng-model="ngModel" type="number" ng-attr-min="{{min}}" ng-attr-max="{{max}}">' +
            '<a class="btn ns-plus" data-ng-click="plus()">+</a>' +
            '<a class="btn ns-minus"data-ng-click="minus()">-</a> </div>',
            link: function (scope, elem, attrs) {

                scope.plus = function () {
                    if (scope.ngModel >= scope.max) return;
                    scope.ngModel += (scope.step || 1);
                    checkModel()
                };

                scope.minus = function () {
                    if (scope.ngModel <= scope.min) return;
                    scope.ngModel -= (scope.step || 1);
                    checkModel();
                };

                function checkModel() {
                    if (!scope.ngModel) scope.ngModel = scope.min || 0;
                }

            }
        }

    }])


}());
number-spin div {
    position: relative;
    width: 126px;

}

number-spin input {
    height: 32px;
    width: 100%;
    text-align: right;
    padding-right: 20px;
    box-sizing: border-box;
    font-size: 16px;
}

number-spin .btn {
    position: absolute;
    text-align: center;
    line-height: 16px;
    display: block;
    height: 16px;
    right: 0;
    border-left: 1px solid #ccc;
    width: 16px;
    cursor: pointer;
    user-select: none;
}

number-spin .ns-plus {
    top: 0;
    border-bottom: 1px solid #ccc;
    cursor: pointer;
    user-select: none;
}

number-spin .ns-minus {
    bottom: 0;
}

number-spin input[type=number]::-webkit-inner-spin-button,
number-spin input[type=number]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
<div ng-app="app" style="padding: 16px; "data-ng-controller="AppController as vm">


<number-spin data-ng-model="vm.testNumber" min="0" max="15"></number-spin>

</div>