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.
1 Answer 1
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
-
\$\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– dan coleman2014年08月03日 12:54:43 +00:00Commented Aug 3, 2014 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– Pete2014年08月03日 13:01:24 +00:00Commented Aug 3, 2014 at 13:01
thisform...
, too easy to confuse withthis.form...
\$\endgroup\$$asyncValidators
- odetocode.com/blogs/scott/archive/2014/10/16/… \$\endgroup\$