javascriptangularjsangularjs-directiveangularjs-filterangularjs-model

AngularJS input mask with validation


I have a directive which has a mask and validation for an input control. It's basically a time input where the user can input something like 45 (45 mins) or 2.5 (2.5 hours => 150 minutes). The input is a type=number when editing but a type=text when displaying the result using a filter.

(Only increments of 15 mins are allowed, and the filter is applied on the blur event)

All works fine EXCEPT that the initial value, when I click into the textbox, gets lost. In the focus event, the scope.minutes value is undefined, where this should be the 90 value set in the controller. I can't figure out why.

angular.module('demoApp', [])
	.controller('MainController', MainController)
  .filter("hoursMinutes", function () {
            return function (mins) {
                if (mins == 0) return "0 mins";
                var hours = ((mins - (mins % 60)) / 60);
                var minutes = (mins % 60);
                return (hours > 0 ? hours + " hr" + (hours === 1 ? "" : "s") : "") + (minutes > 0 ? " " + minutes + " min" + (minutes === 1 ? "" : "s") : "");
            }
        })
  .directive("ixTimeEntry", function ($filter) {
            return {
                restrict: "A",
                require: 'ngModel',
                scope: {
                    minutes: "=",
                    filter: "@",
                    inputFormat: "@"
                },
                link: function (scope, element, attr, ngModel) {
                    
                    // could set a max attribute, so mins can't be more than 1440 (a day)?

                    var inputFormat = scope.inputFormat || "minutes";
                    var filter = scope.filter || "hoursMinutes";

                    // for DOM -> model validation
                    ngModel.$parsers.unshift(function (value) {
                        var result = validate(value);
                        ngModel.$setValidity('ixTimeEntry', result.valid);
                        return result.value;
                    });

                    // for model -> DOM validation
                    ngModel.$formatters.unshift(function (value) {
                        ngModel.$setValidity('ixTimeEntry', true);
                        return $filter(filter)(value);
                    });

                    function validate(input) {
                        var result = { valid: false, value: input };
                        if (input === undefined) return result;
                        input = +input;
                        if (input === 0) return result;
                        if (input < 0) return result;

                        if (inputFormat === "minutes") {
                            // entering as minutes:
                            // if 15, 30, 45, 60, 75, 90, etc       => treat as minutes
                            // if .25, .5, .75, 1, 1.25, ...12      => treat as hours
                            // else (e.g. 13, 14, 16, 17, 18, etc)  => invalid 
                            if (input % 15 === 0) result = { valid: true, value: input };
                            if (input % .25 === 0 && input <= 12) result = { valid: true, value: input * 60 };
                        } else if (inputFormat === "hours") {
                            // entering as hours:
                            // if .25, .5, .75, 1, 1.25, etc        => treat as hours
                            // else                                 => invalid
                            if (input % .25 === 0) result = { valid: true, value: input * 60 };
                        } else {
                            throw "Invalid inputFormat in timeEntry";
                        }

                        return result;
                    }

                    function addMask(text) {
                        return $filter(filter)(text);
                    }

                    function removeMask(text) {
                        if (inputFormat === "hours")
                            return +text / 60;
                        return text;
                    }

                    element.val(addMask(scope.minutes));

                    element.bind("blur", function () {
                        element.attr("type", "text");
                        scope.$apply(function () {
                            var value = validate(element.val()).value;
                            scope.minutes = value;
                            element.val(addMask(value));
                        });
                    });

                    element.bind("focus", function () {
                        element.attr("type", "number");
                        scope.$apply(function () {
                            element.val(removeMask(scope.minutes));
                        });
                    });
                }
            };
        });;


function MainController() {
	var vm = this;
  vm.minutes1 = 90;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app="demoApp" ng-controller="MainController as vm">
<form id="timeForm" name="timeForm" >
<input type="text" class="form-control" name="time" placeholder="time" ix-time-entry input-format="minutes"  minutes="vm.minutes" filter="hoursMinutes" ng-required="true" ng-model="vm.minutes1" />
</form>
<br>
Model: {{vm.minutes1}}<br>
Valid: {{timeForm.time.$valid}}<br>
</div>


Solution

  • You only set vm.minutes1 = 90; in your MainController. It works if you set vm.minutes = 90; as well.

    angular.module('demoApp', [])
    	.controller('MainController', MainController)
      .filter("hoursMinutes", function () {
                return function (mins) {
                    if (mins == 0) return "0 mins";
                    var hours = ((mins - (mins % 60)) / 60);
                    var minutes = (mins % 60);
                    return (hours > 0 ? hours + " hr" + (hours === 1 ? "" : "s") : "") + (minutes > 0 ? " " + minutes + " min" + (minutes === 1 ? "" : "s") : "");
                }
            })
      .directive("ixTimeEntry", function ($filter) {
                return {
                    restrict: "A",
                    require: 'ngModel',
                    scope: {
                        minutes: "=",
                        filter: "@",
                        inputFormat: "@"
                    },
                    link: function (scope, element, attr, ngModel) {
                        
                        // could set a max attribute, so mins can't be more than 1440 (a day)?
    
                        var inputFormat = scope.inputFormat || "minutes";
                        var filter = scope.filter || "hoursMinutes";
    
                        // for DOM -> model validation
                        ngModel.$parsers.unshift(function (value) {
                            var result = validate(value);
                            ngModel.$setValidity('ixTimeEntry', result.valid);
                            return result.value;
                        });
    
                        // for model -> DOM validation
                        ngModel.$formatters.unshift(function (value) {
                            ngModel.$setValidity('ixTimeEntry', true);
                            return $filter(filter)(value);
                        });
    
                        function validate(input) {
                            var result = { valid: false, value: input };
                            if (input === undefined) return result;
                            input = +input;
                            if (input === 0) return result;
                            if (input < 0) return result;
    
                            if (inputFormat === "minutes") {
                                // entering as minutes:
                                // if 15, 30, 45, 60, 75, 90, etc       => treat as minutes
                                // if .25, .5, .75, 1, 1.25, ...12      => treat as hours
                                // else (e.g. 13, 14, 16, 17, 18, etc)  => invalid 
                                if (input % 15 === 0) result = { valid: true, value: input };
                                if (input % .25 === 0 && input <= 12) result = { valid: true, value: input * 60 };
                            } else if (inputFormat === "hours") {
                                // entering as hours:
                                // if .25, .5, .75, 1, 1.25, etc        => treat as hours
                                // else                                 => invalid
                                if (input % .25 === 0) result = { valid: true, value: input * 60 };
                            } else {
                                throw "Invalid inputFormat in timeEntry";
                            }
    
                            return result;
                        }
    
                        function addMask(text) {
                            return $filter(filter)(text);
                        }
    
                        function removeMask(text) {
                            if (inputFormat === "hours")
                                return +text / 60;
                            return text;
                        }
    
                        element.val(addMask(scope.minutes));
    
                        element.bind("blur", function () {
                            element.attr("type", "text");
                            scope.$apply(function () {
                                var value = validate(element.val()).value;
                                scope.minutes = value;
                                element.val(addMask(value));
                            });
                        });
    
                        element.bind("focus", function () {
                            element.attr("type", "number");
                            scope.$apply(function () {
                                element.val(removeMask(scope.minutes));
                            });
                        });
                    }
                };
            });;
    
    
    function MainController() {
    	var vm = this;
      vm.minutes = 90;
      vm.minutes1 = 90;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    
    <div ng-app="demoApp" ng-controller="MainController as vm">
    <form id="timeForm" name="timeForm" >
    <input type="text" class="form-control" name="time" placeholder="time" ix-time-entry input-format="minutes"  minutes="vm.minutes" filter="hoursMinutes" ng-required="true" ng-model="vm.minutes1" />
    </form>
    <br>
    Model: {{vm.minutes1}}<br>
    Valid: {{timeForm.time.$valid}}<br>
    </div>