简体   繁体   中英

How to apply scope correctly so order does not matter

I am trying to write a unit test for a directive that matches a value to other input field. The problem is if I define the element to match before the element on which directive is applied it works fine, otherwise it fails.

It works fine when the template is

tpl = '<input name="verifyNewPassword" ng-model="verifyNewPassword" type="password"/>';
tpl += '<input name="newPassword" ng-model="newPassword" type="password" equals-to="userForm.verifyNewPassword"/>';

and it fails when template is

tpl = '<input name="newPassword" ng-model="newPassword" type="password" equals-to="userForm.verifyNewPassword"/>';
tpl+='<input name="verifyNewPassword" ng-model="verifyNewPassword" type="password"/>';

here is my directive

.directive('equalsTo', function() {
    return {
        require: 'ngModel',
        link: function(scope, elm, attrs, ctrl) {
            var sc = scope;
            scope.$watch(attrs.ngModel, function() {
                var eqCtrl = scope.$eval(attrs.equalsTo);
                console.log('Value1: ' + ctrl.$viewValue + ', Value2: ' + eqCtrl.$viewValue);
                if (ctrl.$viewValue===eqCtrl.$viewValue || (!!!ctrl.$viewValue && !!!eqCtrl.$viewValue)) {
                    ctrl.$setValidity('equalsTo', true);
                    eqCtrl.$setValidity('equalsTo', true);
                } else {
                    ctrl.$setValidity('equalsTo', false);
                    eqCtrl.$setValidity('equalsTo', false);
                }
            });
        }
    };
})

here is my test code:

describe('Unit: Testing Directives', function() {
    var elm, scope;

    beforeEach(function() {
        module('mctApp');

        inject(function($rootScope, $compile) {
            scope = $rootScope.$new();
        });
    });

    function compileDirective(tpl) {
        if(!tpl) {
            tpl = '<input name="newPassword" ng-model="newPassword" type="password" equals-to="userForm.verifyNewPassword"/>';
            tpl += '<input name="verifyNewPassword" ng-model="verifyNewPassword" type="password"/>';            
        }
        tpl = '<form name="userForm">' + tpl + '</form>';

        inject(function($compile) {
            var form = $compile(tpl)(scope);
        });

        scope.$digest();

    }

    it('must be valid form as both values are equal', function() {
        scope.newPassword = 'abcdef';
        scope.verifyNewPassword = 'abcdef';
        compileDirective();                 
        expect(scope.userForm.$valid).toBeTruthy();
    });
});

The test fails because when the watch initially fires, the $viewValue of the equals-to ngModelController is NaN, so the field validity is set to false and the form becomes invalid.

http://plnkr.co/edit/OZVmogR6GT2pIKUrHpuX?p=preview

As you're watching the object on scope that the inputs ngModel assigns to - which is already set to "abcdef" - the watch is only called once. If you watch the ngModel.$viewValue of the input you're comparing against instead, it guarantees that the initial state will always be correct, regardless of the order of the inputs in the DOM.

I'd also argue that it makes more sense to watch that value, as it's the one you're comparing.

.directive('equalsTo', function() {
    return {
        require: 'ngModel',
        link: function(scope, elm, attrs, ctrl) {
            var sc = scope;
            scope.$watch(attrs.equalsTo + '.$viewValue', function() {
                var eqCtrl = scope.$eval(attrs.equalsTo);
                console.log('Value1: ' + ctrl.$viewValue + ', Value2: ' + eqCtrl.$viewValue);
                if (ctrl.$viewValue===eqCtrl.$viewValue || (!!!ctrl.$viewValue && !!!eqCtrl.$viewValue)) {
                    ctrl.$setValidity('equalsTo', true);
                    eqCtrl.$setValidity('equalsTo', true);
                } else {
                    ctrl.$setValidity('equalsTo', false);
                    eqCtrl.$setValidity('equalsTo', false);
                }
            });
        }
    };
})

http://plnkr.co/edit/OZVmogR6GT2pIKUrHpuX?p=preview

Note

If you're using angular 1.3+ it might be worth looking at the new validator pipeline as a more elegant way to solve the same problem.

First off, there is a better UX solution for your model scenario than sync-validating both inputs. The first field (password) should be validated based just on your password format restrictions and only the second field (password confirmation) should be checked for equality with password. This helps the user remain sane while picking both a valid and re-type-able password. (In other words, password confirmation is the sole domain of the second input. If you mess up the confirmation, it doesn't suddenly invalidate the previous input that was, up to that point, valid.)

<input type="password" ng-model="password" required ng-pattern="/^(?=.*\w)(?=.*\W)/">
<input type="password" ng-model="passwordConfirmation" equal-to="password">

Not only will this approach help your UX, it will also help you avoid the need for direct interaction between unrelated elements (which is seldom a good idea in AngularJS).


Second, you're approaching the problem in a non-Angular fashion. Inputs are part of the view , which is just representation of model and means for the user to interact with it. So, instead of validating against another input, you should validate against the model. This is possible, now that you don't need the directive to mess with another input.

.directive('equalTo', ['$parse', function ($parse) {
    return {
        require: 'ngModel',
        compile: function (element, attrs) {
            var getOtherValue = $parse(attrs.equalTo);

            return function link ($scope, $element, $attrs, ngModelCtrl) {
                ngModelCtrl.$validators.equalTo = function (value) {
                    return (value === getOtherValue($scope));
                };
            };
        }
    };
}])

(use $parsers & $setValidity instead of $validators if using AngularJS version < 1.3 )

Testing the directive will be a breeze, since you'll just need to adjust the model.


As a sidenote, even if you decide you really want to validate both of the inputs at once, you'd do better to validate each against the corresponding model value (use my equalTo directive on both inputs) instead of forcing direct communication between sibling controllers.

I think you need to use $timeout while applying the scope. $timeout take cares of $digest process as it apply the scope once the $digest is done.

This is how I used to apply scope.

$timeout(function(){
    $scope.$apply()
}

In tests you can use $timeout.flush() to synchronously flush the queue of deferred functions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM