AngularJS ng-model value is lost after custom validation directive is triggered - angularjs

I created a custom validation directive and used it in a form. It can be triggered with no problem, but after the validation is triggered, I found that the model value is just lost. Say I have
ng-model="project.key"
and after validation, project.key doesn't exist in the scope anymore. I think somehow I understood AngularJS wrong and did something wrong.
Code speaks.
Here is my html page:
<div class="container">
...
<div class="form-group"
ng-class="{'has-error': form.key.$invalid && form.key.$dirty}">
<label for="key" class="col-sm-2 control-label">Key</label>
<div class="col-sm-10">
<input type="text" class="form-control text-uppercase" name="key"
ng-model="project.key" ng-model-options="{ debounce: 700 }"
placeholder="unique key used in url"
my-uniquekey="vcs.stream.isProjectKeyValid" required />
<div ng-messages="form.key.$error" ng-if="form.key.$dirty"
class="help-block">
<div ng-message="required">Project key is required.</div>
<div ng-message="loading">Checking if key is valid...</div>
<div ng-message="keyTaken">Project key already in use, please
use another one.</div>
</div>
</div>
</div>
<div class="col-sm-offset-5 col-sm-10">
<br> Cancel
<button ng-click="save()" ng-disabled="form.$invalid"
class="btn btn-primary">Save</button>
<button ng-click="destroy()" ng-show="project.$key"
class="btn btn-danger">Delete</button>
</div>
</form>
And here's my directive:
.directive('myUniquekey', function($http) {
return {
restrict : 'A',
require : 'ngModel',
link : function(scope, elem, attrs, ctrl) {
var requestTypeValue = attrs.myUniquekey;
ctrl.$parsers.unshift(function(viewValue) {
// if (viewValue == undefined || viewValue == null
// || viewValue == "") {
// ctrl.$setValidity('required', false);
// } else {
// ctrl.$setValidity('required', true);
// }
setAsLoading(true);
setAsValid(false);
$http.get('/prism-cmti/2.1', {
params : {
requestType : requestTypeValue,
projectKey : viewValue.toUpperCase()
}
}).success(function(data) {
var isValid = data.isValid;
if (isValid) {
setAsLoading(false);
setAsValid(true);
} else {
setAsLoading(false);
setAsValid(false);
}
});
return viewValue;
});
function setAsLoading(bool) {
ctrl.$setValidity('loading', !bool);
}
function setAsValid(bool) {
ctrl.$setValidity('keyTaken', bool);
}
}
};
});
Here's the controller for the form page:
angular.module('psm3App').controller(
'ProjectCreateCtrl',
[ '$scope', '$http', '$routeParams', '$location',
function($scope, $http, $routeParams, $location) {
$scope.save = function() {
$http.post('/prism-cmti/2.1', {requestType:'vcs.stream.addProject', project:$scope.project})
.success(function(data) {
$location.path("/");
});
};
}]);
Before this bug, somehow I need to handle the required validation in my custom validation directive too, if I don't do it, required validation would go wrong. Now I think of it, maybe the root cause of these two problems is the same: the model value is gone after my directive link function is triggered.
I'm using Angular1.3 Beta 18 BTW.
Any help is appreciated. Thanks in advance.
Update:
Followed #ClarkPan's answer, I updated my code to return viewValue in ctrl.$parsers.unshift() immediately, which makes required validation works well now, so I don't need lines below any more.
// if (viewValue == undefined || viewValue == null
// || viewValue == "") {
// ctrl.$setValidity('required', false);
// } else {
// ctrl.$setValidity('required', true);
// }
But the {{project.key}} still didn't get updated.
Then I tried to comment out these two lines here:
setAsLoading(true);
setAsValid(false);
Model value {{project.key}} got updated. I know that if any validation fails, the model value will be cleared, but I thought
function(data) {
var isValid = data.isValid;
if (isValid) {
setAsLoading(false);
setAsValid(true);
} else {
setAsLoading(false);
setAsValid(false);
}
}
in $http.get(...).success() should be executed in $digest cycle, which means the model value should be updated.
What is wrong?

This is happening because angular does not apply any change to the scope and $modelValue if there is any invalid flag set in the model. When you start the validation process, you are setting the 'keyTaken' validity flag to false. That is telling to angular not apply the value to the model. When the ajax response arrives and you set the 'keyTaken' validity flag to true, the $modelValue was already set to undefined and the property 'key' was gone. Try to keep all validity flags set to true during the ajax request. You must avoid the calls to setAsLoading(true) and setAsValid(false) before the ajax call and keep all validity flags set to true. Only after the ajax response set the validity flag.

NOTE:This answer below only applies if you're using angular versions prior to 1.3 (before they introduced the $validators concept).
From my reading of your myUniqueKey directive, you want to validate the projectkey asynchronously. If that is the case, that would be your problem. ngModel's $parser/$formatter system doesn't expect asynchronous calls.
The anonymous function you used in the $parsers array does not return a value, as $http is an asynchronous method that returns a method. You'll want to return the viewValue immediately from that method.
Then in the .success callback of your $http call, you can ten set the validity and loading status. I don't recommend you try to change the viewValue (unless that is not your purpose in returning either undefined or viewValue) at this point as it will probably trigger another run of the $parsers.
So:
ctrl.$parsers.unshift(function(viewValue){
//...omitted for clarity
$http.get(
//...
).success(function(data){
setAsLoading(false);
setAsValid(data.isValid);
});
//...
return viewValue;
});

If the value is not valid, by default the model will not be updated (as explained in the accepted answer), but you can make the model to be updated in any case by using allowInvalid in ng-model-options
For the input field in the question:
<input type="text" class="form-control text-uppercase" name="key"
ng-model="project.key" ng-model-options="{ debounce: 700,
allowInvalid: true }"
placeholder="unique key used in url"
my-uniquekey="vcs.stream.isProjectKeyValid" required />

Related

Reset form as pristine if user changed input values to their initial value

Angular has $dirty and $pristine properties on FormController that we can use to detect whether user has interacted with the form or not. Both of these properties are more or less the opposite face of the same coin.
I would like to implement the simplest way to actually detect when form is still in its pristine state despite user interacting with it. The form may have any inputs. If user for instance first changes something but then changes it back to initial value, I would like my form to be $pristine again.
This may not be so apparent with text inputs, but I'm having a list of checkboxes where user can toggle some of those but then changes their mind... I would only like the user to be able to save actual changes. The problem is that whenever user interacts with the list the form becomes dirty, regardless whether user re-toggled the same checkbox making the whole form back to what it initially was.
One possible way would be I could have default values saved with each checkbox and add ngChange to each of them which would check all of them each time and call $setPristine if all of them have initial values.
But I guess there're better, simpler more clever ways of doing the same. Maybe (ab)using validators or even something more ingenious?
Question
What would be the simplest way to detect forms being pristine after being interacted with?
It can be done by using a directive within the ngModel built-in directive and watch the model value and make changes to pristine when needed. It's less expensive than watching the entire form but still seems an overkill and I'm not sure about the performance in a large form.
Note: The following snippet is not the newest version of this solution, check on UPDATE 1 for a newest and optimized solution.
angular.module('app', [])
.directive('ngModel', function() {
return {
restrict: 'A',
require: ['ngModel', '^?form'],
priority: 1000, // just to make sure it will run after the built-in
link: function(scope, elem, attr, ctrls) {
var
rawValue,
ngModelCtrl = ctrls[0],
ngFormCtrl = ctrls[1],
isFormValue = function(value) {
return typeof value === 'object' && value.hasOwnProperty('$modelValue');
};
scope.$watch(attr.ngModel, function(value) {
// store the raw model value
// on initial state
if (rawValue === undefined) {
rawValue = value;
return;
}
if (value == rawValue) {
// set model pristine
ngModelCtrl.$setPristine();
// don't need to check if form is not defined
if (!ngFormCtrl) return;
// check for other named models in case are all pristine
// sets the form to pristine as well
for (key in ngFormCtrl) {
var value = ngFormCtrl[key];
if (isFormValue(value) && !value.$pristine) return;
}
// if haven't returned yet, means that all model are pristine
// so then, sets the form to pristine as well
ngFormCtrl.$setPristine();
}
});
}
};
})
.controller('myController', function($rootScope, $timeout) {
var $ctrl = this;
$ctrl.model = {
name: 'lenny',
age: 23
};
$timeout(function() {
console.log('watchers: ' + $rootScope.$$watchersCount)
}, 1000);
});
angular.element(document).ready(function() {
angular.bootstrap(document, ['app']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.js"></script>
<div ng-controller="myController as $ctrl">
<form name="$ctrl.myForm" novalidate>
<label>Name :
<input name="test1" ng-model="$ctrl.model.name">
</label>
<label>age :
<input name="test2" ng-model="$ctrl.model.age">
</label>
<label>Pristine: {{ $ctrl.myForm.$pristine }}</label>
<div><pre>
</pre>
</div>
</form>
</div>
UPDATE 1
Changed the watching system to watch once and get rid of the extra watchers.Now the changes comes from the change listeners of ngModelController and the watcher is unbinded on the first model set . As can be noticed by a console log, the numbers of watchers on root was always doubling the number of watchers, by doing this the number of watchers remains the same.
angular.module('app', [])
.directive('ngModel', function() {
return {
restrict: 'A',
require: ['ngModel', '^?form'],
priority: 1000,
link: function(scope, elem, attr, ctrls) {
var
rawValue,
ngModelCtrl = ctrls[0],
ngFormCtrl = ctrls[1],
isFormValue = function(value) {
return typeof value === 'object' && value.hasOwnProperty('$modelValue');
};
var unbindWatcher = scope.$watch(attr.ngModel, function(value) {
// set raw value
rawValue = value;
// add a change listenner
ngModelCtrl.$viewChangeListeners.push(function() {
if (rawValue === undefined) {
//rawValue = ngModelCtrl.$lastCommit;
}
if (ngModelCtrl.$modelValue == rawValue) {
// set model pristine
ngModelCtrl.$setPristine();
// check for other named models in case are all pristine
// sets the form to pristine as well
for (key in ngFormCtrl) {
var value = ngFormCtrl[key];
if (isFormValue(value) && !value.$pristine) return;
}
ngFormCtrl.$setPristine();
}
});
// unbind the watcher at the first change
unbindWatcher();
});
}
};
})
.controller('myController', function($rootScope, $timeout) {
var $ctrl = this;
$ctrl.model = {
name: 'lenny',
age: 23
};
$timeout(function() {
console.log('watchers: ' + $rootScope.$$watchersCount)
}, 1000);
});
angular.element(document).ready(function() {
angular.bootstrap(document, ['app']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.js"></script>
<div ng-controller="myController as $ctrl">
<form name="$ctrl.myForm" novalidate>
<label>Name :
<input name="test1" ng-model="$ctrl.model.name">
</label>
<label>age :
<input name="test2" ng-model="$ctrl.model.age">
</label>
<label>Pristine: {{ $ctrl.myForm.$pristine }}</label>
<div><pre>
</pre>
</div>
</form>
</div>
When your controller initialises:
constructor() {
super(arguments);
this._copy = angular.copy(this._formModel);
}
Then you can place a watch on the model.
this._$scope.$watch('this._formModel', (new, old) => {
if (_.eq(this._copy, this._formModel)) {
formObject.$setPristine();
}
});
If the copy is the same as the model, it's still pristine.
Edit: 2nd option is to add ngChange to each input to call a method on your controller, and then do the same procedure as above. This still relies on your copying the original (blank) model in the constructor.
<input ng-change="vm.noticeInputChange(t)" id="some_element" class="some_class" />
Then in the controller:
noticeInputChange() {
if (_.eq(this._copy, this._formModel)) {
formObject.$setPristine();
}
}
That should do the same, but as has been pointed out, the $watch might become quite expensive depending on the size of your form. Also, as someone here pointed out, the _.eq() is a lodash method

How to manually change the $error values for input tags - Angularjs

I am submitting a form via the angular $http and I want to give an error if the user bypassed the angularjs validation. I want to use the same error tag as if they didn't bypass the validation, userForm.name.$invalid && !userForm.name.$pristine how can I manually set this value ?
HTML
<body>
<form ng-controller="UserController" ng-model="userForm" name="userForm" ng-submit="createUser()">
<legend>Create User</legend>
<label>Name</label>
<input type="text" id="name" name="name" ng-model="user.name" placeholder="User Name" required>
<!-- HERE IS WHERE THE ERROR SHOWS -->
<p ng-show="userForm.name.$invalid && !userForm.name.$pristine"
<button class="btn btn-primary">Register</button>
</form>
</body>
ANGULAR JS
$http({
method : 'POST',
url : '/create',
data : user,
headers : {
'Content-Type' : 'application/x-www-form-urlencoded'
}
})
.success(function(data) {
// I want to do something like this
name.$invalid = true;
name.$pristine = false;
});
I want to do something like what is in the success function. Therefore it will show the error message.
If you have access to scope in http success callback, you can do this to set the validity or mark it as dirty.
scope.userForm.name.$setDirty();
OR
scope.userForm.name.$setValidity('serverError', false); // creating a new field in $error and makes form field invalid.
To set the validity or pristine values of the form, you must use the function provided by the form.FormController. You can set the form to pristine or dirty but you cannot set a form directly to valid to not. You must set a particular model to invalid which will trigger the form value to invalid which will trigger the form to be invalid (https://docs.angularjs.org/api/ng/type/form.FormController).
//UserController
//$scope.userName is your object which has it's own controller using Angular Forms.
app.controller("UserController", function($scope){
$http({
method : 'POST',
url : '/create',
data : user,
headers : {
'Content-Type' : 'application/x-www-form-urlencoded'
}
})
.success(function(data) {
// I want to do something like this
$scope.userForm.$setDirty(); //Sets $pristine to false; Alternatively, you could call $setPristine() to set $pristine to true
$scope.userForm.name.$setValidity("length",false); //In your case, the "length" validation is likely something different or will be generic. This enables you to have name fail validation based multiple rules (perhaps length and unique)
});
});
If you want to check a specific field for validity, you can use a custom directive:
app.directive('customValidation', function(dataService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue){
//first, assume the value is valid, until proven otherwise
ctrl.$setValidity('customValidation', true);
//check against the API
if(viewValue.length > 0 && !ctrl.$error.customValidation) {
dataService.checkValidity(viewValue).then(function(response){
//API logic here
var results = response.data;
if(results.isNotValid)
ctrl.$setValidity('customValidation', false);
}).catch(function(response){
//some error occurred
ctrl.$setValidity('customValidation', false);
});
}
});
}
};
});
To use it:
<input custom-validation ng-model-options="{updateOn: 'default blur', debounce: { 'default': 700, 'blur': 0 }}" />

Prevent async unique validator to validate same value only when editing?

I've got an AngularJS form that has a custom validator to check with the server backend whether the input value is unique or not. The unique validation is done by the mob-async-validate-unique directive in the example below.
The form looks somewhat like this:
<form class="form-floating" novalidate="novalidate" ng-submit="saveItem(item)">
<div class="form-group filled">
<label class="control-label">Key</label>
<input type="text" class="form-control" ng-model="item.Key" mob-async-validate-unique >
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary">Save</button>
</div>
</form>
I want to use the same form for both adding as well as editing the model that I put on $scope.
Everything works great except for the fact that the unique validator fires on the edit operation even when the value is the same as the original value and then validates unique as false because the value already exists. The validator marks the field invalid in both the following cases:
Change the field value and then edit it back to the original value
Submit the form without changing anything
What is the best way to achieve this? The naive way I can think of is that I'll have to store the original value on a $scope.originalValue variable, and then add an attribute on the unique validated element to name this variable. Within the validator I'll read this value from the $scope and compare it with the current value to make the validator accept it when the two values are the same. I'm going ahead and implementing this.
I use the unique validator in a generic way in several places (yes, there are several more attributes in use on the <input> element that I've not included in the code sample, for simplicity and legibility) and need the validator to function completely on it's own and ideally want to keep the controller $scope out of the picture so that I can use the custom async validator anyway / anywhere I want.
Depending on your used version of angularjs, there is absolutely no need for writing a custom async validator.
Angular has a built-in way of doing that. Check https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
When you use the $asyncValidator as described in the docs, it does only validate against you API if all other validations succeed.
** EDIT **
Regarding your problem with async validation on editing an existing entry in your database, i suggest the following.
var originalData = {};
if(editMode) {
originalData = data.from.your.API;
}
$scope.formData = angular.copy(orignalData);
// in your async validator
if(value && value !== orginalData(key)) {
//do async validation
} else if(value == originalData(key)) {
return true; //field is valid
}
here is my directive to solve problem
validate if user press key
if model value same as initialValue. Async validation will not be applied
export default function ($http, API_URL, $q) {
"ngInject";
return {
require: 'ngModel',
restrict: 'A',
scope: {
asyncFieldValidator: '#',
initialValue: '#'
},
link: ($scope, element, attrs, ngModel) => {
const apiUrl = `${API_URL}${$scope.asyncFieldValidator}`;
element.on('keyup', e => {
ngModel.$asyncValidators.uniq = (modelValue, viewValue) => {
const userInput = modelValue || viewValue;
const checkInitial = $scope.initialValue === userInput;
return !checkInitial
? $http.get(apiUrl + userInput)
.then(res => res.status === 204 ? true : $q.reject())
: $q.resolve(`value is same`)
}
});
}
}
}

how to show validation messages when form is submitted in angular js

I have form in which there are couple of text fields and an email field.
I want to validate all fields for required / mandatory validation. I want to validate email field for email format validation. I want to carry out validation only when I click on two buttons and show the validation messages at top of the page. I know that I can check for email and required validations on field and using ng-show and a flag I can show messages. However I want to check each field's value in a directive and then set the flag to true which will make the message appear.
Here is my HTML. this gets loaded why state provider in main page which also defines following template and a controller for it. so its just a view partial:
<form name="myForm">
<div ng-show="validationFailed">
<!-- here i want to display validation messages using cca.validationMsgs object -->
</div>
<input type="text" name="test" ng-model="cca.field1" require />
....
<input type="email" mg-model="cca.field2" />
<input type="button" name="mybutton" />
</form>
Now the controller defined in another JS file:
'use strict';
(function(){
angular.module('store', []);
app.controller("StoreController",function(){
var cca = this;
cca.validationMsgs = {};
cca.validationFailed = false; //this flag should decide whether validation messages should be displayed on html page or not.when its true they are shown.
...//other mapped fields
});
And here is unfinished directive which I want to define and write my logic. Logic will be something like this :
1) iterate all elements which have require set on them and check their
$validators object / $error.required object is set
2) if yes set validationFailed flag to true,add the validation message to validationMsgs object and break the loop.
3) check if email type field has $error.email object set and if yes
similarly set validationFailed flag to true and add corresponding message to the object. Not sure if I really need a directive for this. I would like to apply the directive inside a element.
app.directive("requireOnSubmit",function(){
var directiveDefinitionObject = {
restrict:'E',
//.... need to fill in
//I can use link funcion but not sure how to map
// validationMsgs and validationFailed objects in here.
};
return directiveDefinitionObject;
});
Without any code or use case to go after I'll just show you a generic way of validating input. I'm going to use a signup functionality as an example.
Create a service/factory which will carry out the validation and return a promise, if the validation fails it will reject the promise. If not, it will resolve it.
Important note: A promise can only be resolved once (to either be fulfilled or rejected), meaning that the first declaration of a resolve or reject will always "win", which means you can't override any resolve or reject. So in this example if a field is empty and a user's email is undefined, the error message will be All fields must be filled in and not Invalid email format.
auth.factory('validation', ['$q', function($q) {
return {
validateSignup: function(newUser) {
var q = $q.defer();
for (var info in newUser) {
if (newUser[info] === '') {
q.reject('All fields must be filled in');
}
}
if (newUser.email === undefined) {
q.reject('Invalid email format');
}
else if (newUser.password.length < 8) {
q.reject('The password is too short');
}
else if (newUser.password != newUser.confirmedPass) {
q.reject('The passwords do not match');
}
q.resolve(true);
return q.promise;
}
}
}]);
And then inject this into your controller
auth.controller('AuthCtrl', ['$scope', '$location', 'validation', function($scope, $location, validation) {
$scope.status = {
message: ''
}
// Make sure nothing is undefined or validation will throw error
$scope.newUser = {
email: '',
password: '',
confirmedPass: ''
}
$scope.register = function() {
// Validation message will be set to status.message
// This will also clear the message for each request
$scope.status = {
message: ''
}
validation.validateSignup($scope.newUser)
.catch(function(err) {
// The validation didn't go through,
// display the error to the user
$scope.status.message = err;
})
.then(function(status) {
// If validation goes through
if (status === true) {
// Do something
}
});
}
And in the HTML you can have something like this:
<form>
<div>
<label for="email">Email:</label>
<input type="email" id="email" ng-model="newUser.email">
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="confirm-pass" ng-model="newUser.password">
</div>
<div>
<label for="confirm-pass">Confirm password:</label>
<input type="password" id="confirm-pass" ng-model="newUser.confirmedPass">
</div>
<div>
<div>
<span ng-bind="status.message"></span>
</div>
</div>
<div>
<button ng-click="register(newUser)">Register</button>
</div>
</form>
You can use this example and modify it for your use case.

scope in directive breaks view scope with async validation

I am following a Year of Moo tutorial on async form validations and ngMessages (I'm using 1.3.0-beta.14 so I can't use the actual $async validator).
My validation is working, but the scope in the view is non-existent! On form submission, there is no username value and adding the appropriate {{username}} binding elsewhere in the view never returns a value. However, the console Log at the end of my directive does return the correct value, it just never transfers to the view?
Here is the directive, basically lifted from the article (the console.log before the final return does log the correct value from the view):
.directive('recordAvailabilityValidator', function($http) {
return {
require : 'ngModel',
link : function(scope, element, attrs, ngModel) {
var apiUrl = attrs.recordAvailabilityValidator;
function setAsLoading(bool) {
ngModel.$setValidity('recordLoading', !bool);
}
function setAsAvailable(bool) {
ngModel.$setValidity('recordAvailable', bool);
}
ngModel.$parsers.push(function(value) {
if(!value || value.length == 0) return;
setAsLoading(true);
setAsAvailable(false);
$http.get(apiUrl, { params: {attr : value }} )
.success(function(response) {
setAsLoading(false);
setAsAvailable(true);
})
.error(function() {
setAsLoading(false);
setAsAvailable(false);
});
console.log(value)
return value;
})
}
}
});
Here is the relevant part of the html template:
<p>
<label>Username</label>
<input type="text" ng-model="signup.username" class='form-control' name='username'
require minlength='3' record-availability-validator="/api/v1/validations/username"
ng-model-options="{ debounce : { 'default' : 500, blur : 0 } }">
</p>
<div ng-messages="signupForm.username.$error">
<div ng-message="required">You did not enter a username</div>
<div ng-message="minlength">Username must be at least 4 characters</div>
<div ng-message="recordLoading">Checking database...</div>
<div ng-message="recordAvailable">The username is already in use...</div>
</div>
{{signup.username}}
The debug {{signup.username}} never shows a value. If I change it to just {{signup}} the other values show fine. Also, if I add this directive to another input, like email the same strange behavior is there. I googled around and tried added scope: true to my directive, but nothing happened.

Resources