Reputation: 40032
I have a form that looks like this:
<form name="myForm" ng-submit="saveDeployment()">
<input type="hidden" value="{{item.CloneUrl}}" name="cloneurl" />
<input type="hidden" value="{{Username}}" name="username" />
<input type="radio" name="deploymenttype" ng-model="item.deploymentType" value="azure" checked="checked">Azure
<br />
<input type="radio" name="deploymenttype" ng-model="item.deploymentType" value="ftp">FTP
<div id="azure" ng-show="item.deploymentType=='azure'">
<label for="azurerepo">Azure Git Repo</label>
<input type="text" name="azurerepo" ng-model="item.azurerepo" ng-class="{error: myForm.azurerepo.$invalid}" ng-required="item.deploymentType=='azure'" />
</div>
<div id="ftp" ng-show="item.deploymentType=='ftp'">
<label for="ftpserver">FTP Server</label>
<input type="text" name="ftpserver" ng-model="item.ftpserver" ng-class="{error: myForm.ftpserver.$invalid}" ng-required="item.deploymentType=='ftp'" />
<label for="ftppath">FTP Path</label>
<input type="text" name="ftppath" ng-model="item.ftppath" ng-class="{error: myForm.ftppath.$invalid}" ng-required="item.deploymentType=='ftp'" />
<label for="ftpusername">FTP Username</label>
<input type="text" name="ftpusername" ng-model="item.ftpusername" ng-class="{error: myForm.ftpusername.$invalid}" ng-required="item.deploymentType=='ftp'"/>
<label for="ftppassword">FTP Password</label>
<input type="password" name="ftppassword" ng-model="item.ftppassword" ng-class="{error: myForm.ftppassword.$invalid}" ng-required="item.deploymentType=='ftp'"/>
</div>
<input type="submit" value="Save" ng-disabled="myForm.$invalid"/>
</form>
Its setup so that the required fields and Save button are all working once data is entered. However, part of my validation will be, "Is the user already registered?"
where I will use the data entered to hit the server via POST using $http.
Should I put that logic in the saveDeployment()
function or is there a better place to put it?
*UPDATE:*
I've implemented the below which is applied as an attribute on a element but it calls the server/database on every key press which I don't like:
app.directive('repoAvailable', function ($http, $timeout) { // available
return {
require: 'ngModel',
link: function (scope, elem, attr, ctrl) {
console.log(ctrl);
ctrl.$parsers.push(function (viewValue) {
// set it to true here, otherwise it will not
// clear out when previous validators fail.
ctrl.$setValidity('repoAvailable', true);
if (ctrl.$valid) {
// set it to false here, because if we need to check
// the validity of the email, it's invalid until the
// AJAX responds.
ctrl.$setValidity('checkingRepo', false);
// now do your thing, chicken wing.
if (viewValue !== "" && typeof viewValue !== "undefined") {
$http.post('http://localhost:12008/alreadyregistered',viewValue) //set to 'Test.json' for it to return true.
.success(function (data, status, headers, config) {
ctrl.$setValidity('repoAvailable', true);
ctrl.$setValidity('checkingRepo', true);
})
.error(function (data, status, headers, config) {
ctrl.$setValidity('repoAvailable', false);
ctrl.$setValidity('checkingRepo', true);
});
} else {
ctrl.$setValidity('repoAvailable', false);
ctrl.$setValidity('checkingRepo', true);
}
}
return viewValue;
});
}
};
});
Upvotes: 3
Views: 3348
Reputation: 43745
Validating a form input field using an asynchronous $http
ajax call is a common need, but I haven't found any implementations that were complete, reusable, and easy to use so I tried my best to make one.
This functionality is especially useful for checking if a username, email, or other field/column is unique, but there are plenty of other use cases where a value must be validated with an ajax call (as in your example).
My solution has the following features:
$scope
that makes the $http
call or any kind of validation (synchronous or asynchronous)$scope
that allows the check to be bypassed based on the value or ngModel
state.$http
call result is used (in case multiple are fired and return out of order).My directive is pmkr-validate-custom (GitHub)
. It can be used for any asynchronous validation. I've tested it in several versions as far back as 1.1.5.
Here is a sample usage with Twitter Bootstrap in which I check if a username is unique.
<form name="the_form" class="form-group has-feedback">
<div ng-class="{'has-success':userNameUnique.valid, 'has-warning':userNameUnique.invalid}">
<label for="user_name">Username</label>
<input
name="user_name"
ng-model="user.userName"
pmkr-validate-custom="{name:'unique', fn:checkUserNameUnique, gate:gateUserNameUnique, wait:500, props:'userNameUnique'}"
pmkr-pristine-original=""
class="form-control"
>
<span ng-show="userNameUnique.valid" class="glyphicon glyphicon-ok form-control-feedback"></span>
<span ng-show="userNameUnique.invalid" class="glyphicon glyphicon-warning-sign form-control-feedback"></span>
<i ng-show="userNameUnique.pending" class="glyphicon glyphicon-refresh fa-spin form-control-feedback"></i>
<p ng-show="userNameUnique.valid" class="alert alert-success">"{{userNameUnique.checkedValue}}" is availiable.</p>
<p ng-show="userNameUnique.invalid" class="alert alert-warning">"{{userNameUnique.checkedValue}}" is not availiable.</p>
<button
ng-disabled="the_form.$invalid || the_form.user_name.$pristine || userNameUnique.pending"
class="btn btn-default"
>Submit</button>
</div>
</form>
Sample controller:
// Note that this ought to be in a service and referenced to $scope. This is just for demonstration.
$scope.checkUserNameUnique = function(value) {
return $http.get(validationUrl+value).then(function(resp) {
// use resp to determine if value valid
return isValid; // true or false
});
}
// The directive is gated off when this function returns true.
$scope.gateUserNameUnique = function(value, $ngModel) {
return !value || $ngModel.$pristine;
};
If I make any improvements, they will be up-to-date on GitHub, but I am also going to put the code here for this directive and its dependencies (may not be updated). I welcome suggestions or issues though GitHub issues!
angular.module('pmkr.validateCustom', [
'pmkr.debounce'
])
.directive('pmkrValidateCustom', [
'$q',
'pmkr.debounce',
function($q, debounce) {
var directive = {
restrict: 'A',
require: 'ngModel',
// set priority so that other directives can change ngModel state ($pristine, etc) before gate function
priority: 1,
link: function($scope, $element, $attrs, $ngModel) {
var opts = $scope.$eval($attrs.pmkrValidateCustom);
// this reference is used as a convenience for $scope[opts.props]
var props = {
pending : false,
validating : false,
checkedValue : null,
valid : null,
invalid : null
};
// if opts.props is set, assign props to $scope
opts.props && ($scope[opts.props] = props);
// debounce validation function
var debouncedFn = debounce(validate, opts.wait);
var latestFn = debounce.latest(debouncedFn);
// initially valid
$ngModel.$setValidity(opts.name, true);
// track gated state
var gate;
$scope.$watch(function() {
return $ngModel.$viewValue;
}, valueChange);
// set model validity and props based on gated state
function setValidity(isValid) {
$ngModel.$setValidity(opts.name, isValid);
if (gate) {
props.valid = props.invalid = null;
} else {
props.valid = !(props.invalid = !isValid);
}
}
function validate(val) {
if (gate) { return; }
props.validating = true;
return opts.fn(val);
}
function valueChange(val) {
if (opts.gate && (gate = opts.gate(val, $ngModel))) {
props.pending = props.validating = false;
setValidity(true);
return;
}
props.pending = true;
props.valid = props.invalid = null;
latestFn(val).then(function(isValid) {
if (gate) { return; }
props.checkedValue = val;
setValidity(isValid);
props.pending = props.validating = false;
});
}
} // link
}; // directive
return directive;
}
])
;
angular.module('pmkr.debounce', [])
.factory('pmkr.debounce', [
'$timeout',
'$q',
function($timeout, $q) {
var service = function() {
return debounceFactory.apply(this, arguments);
};
service.immediate = function() {
return debounceImmediateFactory.apply(this, arguments);
};
service.latest = function() {
return debounceLatestFactory.apply(this, arguments);
};
function debounceFactory(fn, wait) {
var timeoutPromise;
function debounced() {
var deferred = $q.defer();
var context = this;
var args = arguments;
$timeout.cancel(timeoutPromise);
timeoutPromise = $timeout(function() {
deferred.resolve(fn.apply(context, args));
}, wait);
return deferred.promise;
}
return debounced;
}
function debounceImmediateFactory(fn, wait) {
var timeoutPromise;
function debounced() {
var deferred = $q.defer();
var context = this;
var args = arguments;
if (!timeoutPromise) {
deferred.resolve(fn.apply(context, args));
// return here?
}
$timeout.cancel(timeoutPromise);
timeoutPromise = $timeout(function() {
timeoutPromise = null;
}, wait);
return deferred.promise;
}
return debounced;
}
function debounceLatestFactory(fn) {
var latestArgs;
function debounced() {
var args = latestArgs = JSON.stringify(arguments);
var deferred = $q.defer();
fn.apply(this, arguments).then(function(res) {
if (latestArgs === args) {
deferred.resolve(res);
}
}, function(res) {
if (latestArgs === args) {
deferred.reject(res);
}
});
return deferred.promise;
}
return debounced;
}
return service;
}
])
;
angular.module('pmkr.pristineOriginal', [])
.directive('pmkrPristineOriginal', [
function() {
var directive = {
restrict : 'A',
require : 'ngModel',
link: function($scope, $element, $atts, $ngModel) {
var pristineVal = null;
$scope.$watch(function() {
return $ngModel.$viewValue;
}, function(val) {
// set pristineVal to newVal the first time this function runs
if (pristineVal === null) {
pristineVal = $ngModel.$isEmpty(val) ? '' : val.toString();
}
// newVal is the original value - set input to pristine state
if (pristineVal === val) {
$ngModel.$setPristine();
}
});
}
};
return directive;
}
])
;
Upvotes: 1
Reputation: 40032
My solution was taken from Kosmetika's idea.
I used the angular-ui
project and set an onBlur
callback that was on the controller which called the web service via $http
.
This set a controller/model property to true or false.
I then had a <span>
use ng-show
to watch the controller/model property so when the web service returned it would show the user information
Upvotes: 0
Reputation: 21304
You don't need to make $http request in directive, better place for it is controller.
You can specify method inside controller - $scope.saveDeployment = function () { // here you make and handle your error on request ... };
you'll save error to scope and then create a directive that will watch $scope.yourResponseObject
and set validity based on it.
Also if you need something like request and error on input field blur instead, you need to create a simple directive with elem.bind('blur', ...)
where you call $scope.saveDeployment
with callback to handle validity.
Take a look on the examples, there might be something similar - https://github.com/angular/angular.js/wiki/JsFiddle-Examples
Upvotes: 1