4
\$\begingroup\$

I looked at a bunch of AngularJS directives checking for uniqueness of a username and decided to try for the simplest implementation. It does what I want but may not really be 'idiomatic' Angular.

Here is the form element:

<input type="text"
    name="username"
    ng-model="form.username"
    unique-username=""
    required
/>
<span class="hide-while-in-focus" ng-show="thisform.username.$error.unique">Username taken!</span>

Here's the directive:

.directive('uniqueUsername', function($http) {
      return {
           restrict: 'A',
           require: 'ngModel',
           link: function (scope, element, attrs, ngModel) {
                element.bind('blur', function (e) {
                     ngModel.$setValidity('unique', true);

                     $http.get("/api/checkUnique/" + element.val()).success(function(data) {
                          if (data) {
                              ngModel.$setValidity('unique', false);
                          }
                     });
                });
           }
      };
})

And the ExpressJS call:

if (data) {
      console.log("found " + data.username);
      return res.send(data.username);
}
else {
      console.log("not found");
      return res.send(404);
}

I would appreciate any feedback on why this is good or bad and if possible a revision that uses $scope.watch on the model.

\$\endgroup\$
2
\$\begingroup\$

Here's my two cents. These are the changes that I would make:

  • Separate the API calls into a service.
  • Separate the event triggers using the model-options in the view.
  • Handle the monitoring using a watch. (Expensive, but it seems to be the "angular" way.)
  • Isolate your code into functions/files, then configure in application.js.

Helper Functions

Normally, I use !!value to test if a value is not undefined or null, but in this case I want to do things properly and make the code readable. Alternatively, if you use library like lodash or underscore, you can just call !_.isEmpty(value)

function notEmpty(value) {
    return (angular.isDefined(value) && !angular.isNull(value))
}

View

Using ngModelOptions will allow one to fine tune the view without having to manipulate the directive. One can then test different events without having to make changes in the directive.

<input type="text"
    name="username"
    ng-model="form.username"
    ng-model-options="{ updateOn: 'blur' }"
    unique-username=""
    required
/>
<span class="hide-while-in-focus" ng-show="thisform.username.$error.unique">Username taken!</span>

Service

Using a service is another large advantage. Currently, the directive requires $http and thus becomes bound to the api. Using a service, one can create a mock service to test the directive independently of the api.

Another advantage is that the logic for the uniqueness test is now isolated from the view. This code can then be reused in a controller, another directive, or even another service.

function UserService($q, $http) {

    this.isUnique = function(username) {
        if (notEmpty(username)) {
            var uri = '/api/checkUnique/' + username;
            return $http.get(uri);
        }
        return $q.reject("Invalid User name");
    }

}

Directive

userService is must now be injected into the directive instead of $http . Since the uniqueness test is done when the model changes, one can also create a watch on the model rather than binding it to an event.

In the future, you can use the controller.$parsers.unshift command instead of a scope.$watch. Unfortunately, $parsers doesn't allow for responses from a $promise. See issue #6416.

function UniqueUsernameDirective(userService) {
      return {
           restrict: 'A',
           require: 'ngModel',
           link: function (scope, element, attrs, ngModel) {
               scope.$watch(attrs.ngModel, function(value) {
                   userService.isUnique(value)
                              .then(function(data) {
                                   ngModel.$setValidity('unique', notEmpty(data));
                              })
                              .catch(function() {
                                   ngModel.$setValidity('unique', false);
                              });
               });
           }
      };
}

Application.js

This just keeps your application.js file nice an clean.

angular.module('app', [])
       .service('userService', ['$q', '$http', UserService])
       .directive('uniqueUsername', ['userService', UniqueUsernameDirective]);

Demo

I made a demo using a mock service to demonstrate this functionality. jsFiddle

\$\endgroup\$
  • \$\begingroup\$ Looks good thanks. I am doing service injections elsewhere in my app but wanted to strip this directive down to bare bones and get feedback. First question I have, is the service call being fired on every character entry or backspace within the form field? \$\endgroup\$ – dan coleman Aug 3 '14 at 12:54
  • \$\begingroup\$ It only executes on blur due to the ng-model-options. The interesting feature is to use debounce and then you can have it update during a specific time interval. If you're going bare bones, then your code is fine, but any production code i would recommend decoupling to aide in maintainability. \$\endgroup\$ – Pete Aug 3 '14 at 13:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.