AngularJS directive that exposes view state

I have an AngularJS directive that I'm using to experiment with delayed form field validation, both on blur but also on idle (no input for some time). I have two approaches wired up; one is to add a class on the actual input field, the other is to set a state variable on the scope. Setting the class is ok for styling the field, but I'm more concerned with styling (ng-showing) the help messages, which is on an unrelated element. Plus the class could be added with ng-class, so the approach I want seems it should be a state variable property check.

My questions:

  1. Is there a way that I can easily expose an isActive state from the directive such that callers don't have to pass in an external property to act as the intermediary? This really isn't external state, it is all view oriented, and I just want to cleanly access it from the HTML.
  2. How are $error, $invalid, etc attached to and resolved on element? Can I build the same functionality in user code? i.e. element.state.active?

<form name="f">
        <div class="form-group" ng-class="{'has-error': f.e.$invalid}">
            <label>Enter Email Address</label>
            <input type="email" class="form-control" name="e" ng-model="user.email" required autofocus active="1500" is-active="state.active"/>
            <p class="help-block" ng-show="!state.active && f.e.$error.required">email is required</p>
            <p class="help-block" ng-show="!state.active && f.e.$error.email">enter a valid email address</p>
            <p class="help-block" ng-show="state.active">typing...</p>
            <p class="help-block" ng-show="!state.active && f.$valid">ok!</p>

        </div>
    </form>

And the associated directive...

app.directive('active', ['$parse', function ($parse) {
        var tid;
        return {
            restrict: 'A',
            require: '?ngModel',
            scope:
            {
                isActive: "=",
                ngModel: "="
            },

            link: function (scope, elem, attrs, ctrl) {
                elem.bind('blur', function()
                {
                    scope.isActive = false;
                    elem.removeClass("ng-active");
                });
                var timeout = attrs.active || 2000;
                ctrl.$parsers.unshift(function(value)
                {
                    scope.isActive = true;
                    console.log("adding class");
                    elem.addClass("ng-active");
                    if (tid) clearTimeout(tid);

                    tid = setTimeout(function()
                    {
                        console.log("timeout elapsed, removing class");
                        scope.$apply(function(){
                        scope.isActive = false;
                        });
                        elem.removeClass("ng-active")


                    }, timeout);
                    return value;
                })


            }
        };
    }]);

//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});

function TestCtrl($scope)
{
    $scope.testName = "Validation Test";
    $scope.user = {
        email: 'test@foo.com'
    };
    $scope.state = {
        active: false
    };

}

Here is a fiddle of this code running

Basically, it works, but I don't like that I have to pass in an external property for each input field, and I don't like the asymmetry with the validation checking. Is there a better way of doing this?

Answers


You could try: ngModelController.$setValidity

app.directive('active', ['$parse', function ($parse) {
        var tid;
        return {
            restrict: 'A',
            require: '?ngModel',
            scope:
            {
                ngModel: "="
            },

            link: function (scope, elem, attrs, ctrl) {
                elem.bind('blur', function()
                {
                    scope.$apply(function(){
                        ctrl.$setValidity("active",true);
                    });
                });
                var timeout = attrs.active || 2000;
                ctrl.$parsers.unshift(function(value)
                {
                    ctrl.$setValidity("active",false);
                    console.log("adding class");

                    if (tid) clearTimeout(tid);

                    tid = setTimeout(function()
                    {
                        ctrl.$setValidity("active",false);
                    }, timeout);
                    return value;
                })


            }
        };
    }]);

1) Setting the active like this: ctrl.$setValidity("active",true);.

2) Access from the html like this: f.e.$error.active.

3) Be aware that when we utilize this feature: ctrl.$setValidity("active",true); means there is no error with active => f.e.$error.active == true (kind of reverse your current logic)

DEMO

Another solution is just add the property directly to your ngModelController: ctrl.active = false; and access it in the html like this: f.e.active. But this solution is not good IMO because it may collide with future version of angular when angular decides to create a new property with the same name.

DEMO


To build on @Khanh TO's (second) answer, I've updated the fiddle to match my original a little more closely and remove some typos. I then use the exposed state to style both the help text and input field as appropriate using ng-class.

Go forth and fiddle!

For posterity, the relevant bits of HTML:

<form name="f">
    <div class="form-group" ng-class="{'has-error': !f.e.active && f.e.$invalid}">
        <label>Enter Email Address</label>
        <input type="email" ng-class="{'active' : f.e.active}" class="form-control" name="e" ng-model="user.email" required autofocus active="1500" />
        <p class="help-block" ng-show="!f.e.active && f.e.$error.required">email is required</p>
        <p class="help-block" ng-show="!f.e.active && f.e.$error.email">enter a valid email address</p>
        <p class="help-block" ng-show="f.e.active">typing...</p>
        <p class="help-block" ng-show="!f.e.active && f.$valid">ok!</p>

    </div>
</form>

and the JS:

app.directive('active', ['$parse', function ($parse) {
        var tid;
        return {
            restrict: 'A',
            require: '?ngModel',

            // warning - this adds an "active" property to ctrl; consider prefixing to avoid future collisions
            link: function (scope, elem, attrs, ctrl) {
                elem.bind('blur', function()
                {
                    ctrl.active = false;
                });
                var timeout = attrs.active || 2000;
                ctrl.$parsers.unshift(function(value)
                {
                    ctrl.active = true;
                    if (tid) clearTimeout(tid);

                    tid = setTimeout(function()
                    {
                        scope.$apply(function(){
                            ctrl.active = false;
                        });

                    }, timeout);
                    return value;
                })


            }
        };
    }]);

Need Your Help

How do you reverse a string in place in C or C++?

c++ c string reverse

How do you reverse a string in C or C++ without requiring a separate buffer to hold the reversed string?