Enable/disable validation for angular form with nested subforms using `ng-form` - angularjs

I need to enable/disable all validation rules in Angular form or subform under ng-form="myForm" based on a scope variable $scope.isValidationRequired. So, if isValidationRequired is false, none of the validations set for the designated group of fields will run, and the result will always be myForm.$valid==true, otherwise, the validation rules will run as usual.
I did a lot of research, and realized that this feature is not available out of the box with Angular. However, I found some add-ons or with some customization, it is possible.
For example, I can use the add-on angular-conditional-validation (github and demo) with custom directive enable-validation="isValidationRequired". This will be perfect, except that I cannot apply this feature for a group of fields under ng-form. I have to add this directive for each and every field where applicable.
The other solution is to use custom validation using Angular $validators pipeline. This requires some extra effort and I don't have time since the sprint is almost over and I have to give some results in a few days.
If you have any other suggestions please post an answer.
Use Case:
To clarify the need for this, I will mention the use-case. The end user can fill the form with invalid data and he can click Save button and in this case, the validation rules shouldn't be triggered. Only when the user clicks Validate and Save then the validation rules should be fired.
Solution:
See the final plunker code here.
UPDATE: as per comments below, the solution will cause the browser to hang if inner subforms are used under ng-form. More effort is needed to debug and resolver this issuer. If only one level is used, then it works fine.
UPDATE: The plunker here was updated with a more general solution. Now the code will work with a form that has sub-forms under ng-form. The function setAllInputsDirty() checks if the object is a $$parentForm to stop recursion. Also, the changeValidity() will check if the object is a form using $addControl then it will call itself to validate its child objects. So far, this function works fine, but it needs a bit of additional optimization.

One idea is to reset the errors in the digest loop if the validation flag is disabled. You can iterate through the form errors on change and set them to valid, one by one.
$scope.$watch(function() {
$scope.changeValidity();
}, true);
$scope.changeValidity = function() {
if ($scope.isValidationRequired === "false") {
for (var error in $scope.form.$error) {
while ($scope.form.$error[error]) {
$scope.form.$error[error][0].$setValidity(error, true);
}
}
}
}
Here is a plunkr: https://plnkr.co/edit/fH4vGVPa1MwljPFknYHZ

This is the updated answer that will prevent infinite loop and infinite recursion. Also, the code depends on a known root form which can be tweaked a bit to make it more general.
References: Pixelastic blog and Larry's answer
Plunker: https://plnkr.co/edit/ycPmYDSg6da10KdoNCiM?p=preview
UPDATE: code improvements to make it work for multiple errors for each field in each subform, and loop to ensure the errors are cleared on the subform level
var app = angular.module('plunker', []);
app.controller('MainCtrl', ["$scope", function($scope) {
$scope.isValidationRequired = true;
var rootForm = "form";
function setAllInputsDirty(scope) {
angular.forEach(scope, function(value, key) {
// We skip non-form and non-inputs
if (!value || value.$dirty === undefined) {
return;
}
// Recursively applying same method on all forms included in the form except the parent form
if (value.$addControl && key !== "$$parentForm") {
return setAllInputsDirty(value);
}
if (value.$validate){
value.$validate();
}
// Setting inputs to $dirty, but re-applying its content in itself
if (value.$setViewValue) {
//debugger;
return value.$setViewValue(value.$viewValue);
}
});
}
$scope.$watch(function() {
$scope.changeValidity();
}, true);
$scope.changeValidity = function(theForm) {
debugger;
//This will check if validation is truned off, it will
// clear all validation errors
if (!theForm) {
theForm = $scope[rootForm];
}
if ($scope.isValidationRequired === "false") {
for (var error in theForm.$error) {
errTypeArr = theForm.$error[error];
angular.forEach (errTypeArr, function(value, idx) {
var theObjName = value.$name;
var theObj = value;
if (theObj.$addControl) {
//This is a subform, so call the function recursively for each of the children
var isValid=false;
while (!isValid) {
$scope.changeValidity(theObj);
isValid = theObj.$valid;
}
} else {
while (theObj.$error[error]) {
theObj.$setValidity(error, true);
}
}
})
}
} else {
setAllInputsDirty($scope);
}
}
}]);

Related

How to view the ParsleyJS errors without blocking form submition

Is there a way to get the list of errors from parsley.js? I have a form that has one field that I want validate and give feedback to the user as a warning, but I don't want the error state for that field to block form submission. I am handling the form submission myself, so I'm looking for something like
$("form[name='client']").on('submit'), function(e) {
e.preventDefault();
var form = $(this);
form.parsley().validate();
// pseudo code as I don't know how to do this yet with parsley
var errors = form.parsley().errors().filter(function(err) { return err.field != field_to_ignore })
if (errors.length ) {
// error handling
} else {
// submit form
}
});
You could change the inputs or excluded options when you click on submit so that your inputs are all excluded.
My Solution is to work with two validations:
1.The first one is binding the error to the UI.
2.The second one is after adding the data-parsley-excluded=true attribute to your field_to_be_ignore.
$("#myForm").on('submit'), function(e) {
e.preventDefault();
var form = $(this);
//the first validation bind the error message to the screen
if (form.parsley().validate() == false) {
$('myFieldToIgnore').attr('data-parsley-excluded','true');
//Now let make a second validation:
form.parsley().validate();
}
else {
//submit
}
});

Using Angular Material, is it possible to close a specific dialog

I have an AngularJS app using the Angular Material UI framework.
The app has different mechanisms showing dialogs (e.g error and loading spinner) and it would be preferable to only close one specifically chosen in certain scenarios, e.g. when an AJAX request is finished fetching data, I would like my loading spinner to close, but not any error dialog that may be the result of the fetching.
What I can find in documentation and code doesn't agree (though code should win the argument):
Documentation says only the latest can be closed, with an optional response
The code says the latest, a number of latest or all open can be closed, with an optional reason
Example in the documentation says a specific dialog can be closed, with a flag denoting how or why
I have made a demo of my intent, as MCV as possible – these are the highlights:
var dialog = {},
promise = {};
function showDialogs(sourceEvent) {
showDialog(sourceEvent, "one");
showDialog(sourceEvent, "two");
}
function showDialog(sourceEvent, id) {
dialog[id] = $mdDialog.alert({...});
promise[id] = $mdDialog.show(dialog[id]);
promise[id].finally(function() {
dialog[id] = undefined;
});
}
function closeDialogs() {
$mdDialog.hide("Closed all for a reason", {closeAll: true});
}
function closeDialogLatest() {
$mdDialog.hide("Closed from the outside");
}
function closeDialogReason() {
$mdDialog.hide("Closed with a reason");
}
function closeDialogSpecific(id) {
$mdDialog.hide(dialog[id], "finished");
}
EDIT:
I know the code always wins the argument about what happens, but I wasn't entirely sure it was the right code I was looking at.
I have updated the examples to better test and illustrate my point and problem. This shows things to work as the code said.
What I'm really looking for is whether it might still be possible to achieve my goal in some other way that I didn't think of yet.
Using $mdPanel instead of $mdDialog I was able to achieve the desired effect; I forked my demo to reflect the changes – these are the highlights:
var dialog = {};
function showDialogs() {
showDialog("one");
showDialog("two");
}
function showDialog(id) {
var config = {...};
$mdPanel.open(config)
.then(function(panelRef) {
dialog[id] = panelRef;
});
}
function closeDialogs() {
var id;
for(id in dialog) {
closeDialogSpecific(id, "Closed all for a reason");
}
}
function closeDialogSpecific(id, reason) {
var message = reason || "finished: " + id;
if(!dialog.hasOwnProperty(id) || !angular.isObject(dialog[id])) {
return;
}
if(dialog[id] && dialog[id].close) {
dialog[id].close()
.then(function() {
vm.feedback = message;
});
dialog[id] = undefined;
}
}
I would suggest having two or more dialogs up at the same time isn't ideal and probably not recommended by Google Material design.
To quote from the docs
Use dialogs sparingly because they are interruptive.
You say:
when an AJAX request is finished fetching data, I would like my
loading spinner to close, but not any error dialog that may be the
result of the fetching.
My solution here would be to have one dialog which initially shows the spinner. Once the request is finished replace the spinner with any messages.

Failing To Check if Element is Disabled

I have a dropdown with elements that get disabled when conditions are met. In the test, I check them for being disabled, but all tests fail and always return the element state as enabled (Clearly incorrectly. I have ensured that this is not a timing issue - refreshed the page and gave ample wait time with browser sleep - the elements are clearly disabled on the screen). There is an anchor within a list item. Please see image:
I have tried checking both the list item and the anchor, like so:
var actionDropDownList = $$('[class="dropdown-menu"]').get(1);
var checkOutButtonState = actionDropDownList.all(by.tagName('li')).get(6);
actionsButton.click();
actionDropDownList.all(by.tagName('li')).count().then(function(count){
console.log('THE NUMBER OF ELEMENTS IN THE DROPDOWN IS...............................................................' + count);
}) //verify that I have the correct dropdown - yes
checkOutButtonState.isEnabled().then(function(isEnabled){
console.log('CHECKING checkOutButton BUTTON STATE: ' + isEnabled);
}) //log state - shows incorrectly
I have also tried checking the button itself for disabled state (the element below is what I tried checking instead of the list element):
var checkOutButton = $('[ng-click="item.statusId !== itemStatus.in || checkOut()"]');
This failed as well.
Not sure which one I should check and why both are failing. How do I correct this and get it to show that the disabled button is...well, disabled.
TEMPORARY ADD ON EDIT:
For simplicity's sake, I am trying:
var hasClass = function (element, cls) {
return element.getAttribute('class').then(function (classes) {
return classes.split(' ').indexOf(cls) !== -1;
});
var checkOutButtonState = actionDropDownList.all(by.tagName('li')).get(6);
expect(hasClass(checkOutButtonState, 'disabled')).toBe(true);
It still fails, however, despite the element clearly having the class. Alec - your solution throws "function is not defined," I am not sure if I need something else for it to see jasmine. Tried, but can't find anything wrong with it, not sure why I can't get it to work.
Edit:
If I run...since it only appears to have one class:
expect(checkOutButtonState.getAttribute('class')).toBe('disabled');
I get "expected 'ng-isolate-scope' to be 'disabled'"
In a quite similar situation I've ended up checking the presence of disabledclass:
expect(checkOutButtonState).toHaveClass("disabled");
Where toHaveClass() is a custom jasmine matcher:
beforeEach(function() {
jasmine.addMatchers({
toHaveClass: function() {
return {
compare: function(actual, expected) {
return {
pass: actual.getAttribute("class").then(function(classes) {
return classes.split(" ").indexOf(expected) !== -1;
})
};
}
};
},
});
});

CheckAll/UncheckAll issue with Subscribe ? Knockout

I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .
Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.
I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/
The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/

How should I handle partial forms with angularjs?

I think someone must have run into this situation before. Basically I have a big "form" which is composed of multiple smaller "forms" inside. (In fact, they are not real forms, just sets of inputs that are grouped together to collect info for models).
This form is for a checkout page, which contains:
shipping address
shipping method
billing address
billing method
other additional info such as discounts code input, gift wrapping etc.
I would like to update the user filled info to the server as soon as they complete each part (for example, when they complete shipping address). However, I want to make it work seamlessly without the need for the users to click some kind of "update" button after filling each partial part. I wonder if there is some way to go around this?
You'll want to $watch the fields in question and act upon them (say save to db) when they are filled in. The issue you will run into is how to determine when a user has filled fields in. Things like onblur etc don't work very well in practice. I would recommend using what is called a debounce function which is basically a function that allows the user to pause for X amount of time without our code going "ok done! now let's.. ohh wait still typing..."
Here's an example that I use on my own cart - I want to automatically get shipping quotes once I have an address so I watch these fields, allow some pausing with my debounce function then call my server for quotes.
Here's some controller code:
// Debounce function to wait until user is done typing
function debounce(fn, delay) {
var timer = null;
return function() {
var context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
// Apply debounce to our shipping rate fetch method
var fetch = debounce(function() {
$scope.fetching = true;
cartService.updateShipping($scope.shipping, function(data) {
$scope.fetching = false;
$scope.quotes = data;
});
}, 1000);
// Watch the shipping fields - when enough done and user is done typing then get quote
$scope.$watch('shipping', function(newVal, oldVal) {
// I use this to play around with what fields I actually want before I do something
var fields = ['street', 'region', 'name', 'postal', 'country', 'city'];
var valid = true;
fields.forEach(function(field) {
if (!$scope.form[field].$valid) {
valid = false;
}
});
if (valid) fetch();
}, true);
My form fields are setup like this:
<input type="text" name="street ng-model="shipping.street" required>
<input type="text" name="name" ng-model="shipping.name" required>
Notice how I make them part of a "shipping" object - that allows me to watch the shipping fields independently of others such as billing.
Note that the above is for the extreme cases such as shipping fields. For simple things such as subscribing to a newsletter if they check a box then you don't need to use the above and can simply do an ng-click="spamMe();" call in your checkbox. That function (spamMe) would be in your controller and can then call your server etc...
var spamMe = function() {
// Grab the email field that might be at top - ideally check if it's filled in but you get the idea
var email = $scope.email;
$http.post('/api/spam', ....);
}
I'd apply a $scope.$watch on each of those variables to trigger a function that checks to see if all the fields for a given section are filled out, and if so, then submit it to the server as an ajax request.
Here's my attempt at writing this:
var shippingFields = ['address', 'city', 'state', 'zip'] // etc
function submitFieldsWhenComplete(section, fields) {
fieldValues = fields.forEach(function (field) {
return $scope[section][field]
});
if (fieldValues.every()) {
// We've got all the values, submit to the server
$http.post({
url: "/your/ajax/endpoint",
data: $scope.shipping
})
}
}
shippingFields.forEach(function(field) {
$scope.$watch(function() {
return $scope['shipping'][field]
}, function(val) {
submitFieldsWhenComplete('shipping', shippingFields);
});
});

Resources