AngularJS: disabling all form controls between submit and server response - javascript

I have a dilemma about what is the best (and correct) approach if I want to disable form controls (or at least make them unavailable for user interaction) during a period of time when user clicks sort of "Save" or "Submit" button and data travelling over the wire. I don't want to use JQuery (which is evil!!!) and query all elements as array (by class or attribute marker)
The ideas I had so far are:
Mark all elements with cm-form-control custom directive which will subscribe for 2 notifications: "data-sent" and "data-processed". Then custom code is responsible for pushing second notification or resolve a promise.
Use promiseTracker that (unfortunatelly!) enforces to produce extremely stupid code like ng-show="loadingTracker.active()". Obviously not all elements have ng-disabled and I don't want to user ng-hide/show to avoid "dancing" buttons.
Bite a bullet and still use JQuery
Does any one have a better idea?
UPDATED:
The fieldset idea DOES work. Here is a simple fiddle for those who still want to do the same http://jsfiddle.net/YoMan78/pnQFQ/13/
HTML:
<div ng-app="myApp">
<ng-form ng-controller="myCtrl">
Saving: {{isSaving}}
<fieldset ng-disabled="isSaving">
<input type="text" ng-model="btnVal"/>
<input type="button" ng-model="btnVal" value="{{btnVal}}"/>
<button ng-click="save()">Save Me Maybe</button>
</fieldset>
</ng-form>
</div>
and JS:
var angModule = angular.module("myApp", []);
angModule.controller("myCtrl", function ($scope, $filter, $window, $timeout) {
$scope.isSaving = undefined;
$scope.btnVal = 'Yes';
$scope.save = function()
{
$scope.isSaving = true;
$timeout( function()
{
$scope.isSaving = false;
alert( 'done');
}, 10000);
};
});

Wrap all your fields in fieldset and use ngDisabled directive like this:
<fieldset ng-disabled="isSaving"> ... inputs ...</fieldset>
It will automatically disable all inputs inside the fieldset.
Then in controller set $scope.isSaving to true before http call and to false after.

There is an simple solution in modern browsers:
define a css class
.disabled {
pointer-events: none;
... ...
}
add this class to ng-form
<ng-form data-ng-class="{ 'disabled': isSaving }"> ... inputs ... </ng-form>
Here is the pointer-events support chart.
Note: even if you set pointer-events: none, you can still tab to input element with your keyboard.

Related

Submit with button, on enter, outside of form with only Angular

What solution would you implement for an Angular "form", where you wanted it to submit on an enter key press, but you can't rearrange the elements to fit in a <form> element. For instance something like this:
<div>
<input/>
<input/>
//This might act as a kind of custom built select
<ul>
<li></li>
</ul>
<form></form> //Dropzone form for instance.
<button type="submit" ng-click="submitFunction()">Submit</button>
</div>
I want the user to be able to hit enter while the focus is on any of these elements(Not just the true form elements), and have the form submit(validation passing of course). The solution should be purely Angualr(if possible). I'd rather it didn't use complex CSS positioning, I don't want to have to rearrange and muddle the html structure.
I saw a solution online for a custom "ngEnter" type directive that I thought had promise but I don't think everyone on the team is gonna like that solution. I'd like to know how else one might approach this.
See this article for more info.
Here's a snippet showing how to conditionally log to the console using ng-keypress:
angular.module('myExample', [])
.controller('myController', ['$scope', function($scope) {
$scope.myFunction = function() {
console.log('hi!');
}
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myExample">
<div ng-controller="myController">
<input placeholder="type something" ng-keypress="$event.which === 13 && myFunction()">
</div>
</div>

Disabling submit button based on fields added with ng-bind-html

JSFiddle here: http://jsfiddle.net/c6tzj6Lf/4/
I am dynamically creating forms and buttons and want to disable the buttons if the required form inputs are not completed.
HTML:
<div ng-app="choicesApp">
<ng-form name="choicesForm" ng-controller="ChoicesCtrl">
<div ng-bind-html="trustCustom()"></div>
<button ng-repeat="button in buttons" ng-disabled="choicesForm.$invalid">
{{button.text}}
</button>
</ng-form>
</div>
JavaScript:
angular.module('choicesApp', ['ngSanitize'])
.controller('ChoicesCtrl', ['$scope', '$sce', function($scope, $sce) {
$scope.custom = "Required Input: <input required type='text'>";
$scope.trustCustom = function() {
return $sce.trustAsHtml($scope.custom);
};
$scope.buttons = [
{text:'Submit 1'},
{text:'Submit 2'}];
}]);
choicesForm.$invalid is false and does not change when entering text into the input field.
Solution:
I ended up using the angular-bind-html-compile directive from here: https://github.com/incuna/angular-bind-html-compile
Here is the relevant bit of working code:
<ng-form name="choicesForm">
<div ng-if="choices" bind-html-compile="choices"></div>
<button ng-click="submitForm()" ng-disabled="choicesForm.$invalid">
Submit
</button>
</ng-form>
And choices might be a snippit of HTML like this:
<div><strong>What is your sex?</strong></div>
<div>
<input type="radio" name="gender" ng-model="gender" value="female" required>
<label for="female"> Female</label><br>
<input type="radio" name="gender" ng-model="gender" value="male" required>
<label for="male"> Male</label>
</div>
The main problem is that ngBindHtml doesn't compile the html - it inserts the html as it is. You can even inspect the dynamic input and see that it doesn't have the ngModel's CSS classes (ng-pristine, ng-untouched, etc) which is a major red flag.
In your case, the form simply doesn't know that you've added another input or anything has changed for that matter. Its state ($pristine, $valid, etc) isn't determined by its HTML but by the registered NgModelControllers. These controllers are added automatically when an ngModel is linked.
For example this <input required type='text'> won't affect the form's validity, even if it's required, since it doesn't have ngModel assigned to it.
But this <div ng-model="myDiv" required></div> will affect it since it's required and has ngModel assigned to it.
The ngDisabled directive on your buttons works as expected since it depends on the form's $invalid property.
See this fiddle which showcases how ngModel registers its controller. Note that the html containing the dynamic input gets compiled after 750ms just to show how NgModelControllers can be added after FormController has been instantiated.
There are a few solutions in your case:
use a custom directive to bind and compile html - like this one
use ngInclude which does compile the html
use $compile to compile the newly added HTML but this is a bit tricky as you won't know exactly when to perform this action
This is an answer yet imcomplete because i cannot do the code at the moment.
I think your html will be included, not compiled. So the inputs are not bind to angular and are not part of the angular form object.
The only way i see is to use a directive that will compile the passed html and add it to your form. This may be quite tricky though, if you want to go on this way i suggest to edit your question to ask for the said directive.
However i'm not really familiar with $compile so i don't know if it'll work to just add $compile around $sce.trustAsHtml()
You can write a method as ng-disabled does not work with booleans, it works with 'checked' string instead:
So on your controller place a method :
$scope.buttonDisabled = function(invalid){
return invalid ? "checked" : "";
};
And on your view use it on angular expression :
<button ng-repeat="button in buttons" ng-disabled="buttonDisabled(choicesForm.$invalid)">
Here is a working fiddle
Working DEMO
This is the solution you are looking for. You need a custom directive. In my example I have used a directive named compile-template and incorporated it in div element.
<div ng-bind-html="trustCustom()" compile-template></div>
Directive Code:
.directive('compileTemplate', function($compile, $parse){
return {
link: function(scope, element, attr){
var parsed = $parse(attr.ngBindHtml);
function getStringValue() { return (parsed(scope) || '').toString(); }
//Recompile if the template changes
scope.$watch(getStringValue, function() {
$compile(element, null, -9999)(scope); //The -9999 makes it skip directives so that we do not recompile ourselves
});
}
}
});
I found the directive in this fiddle.
I believe what is really happening though due to jsfiddle I'm unable to dissect the actual scopes being created here.
<div ng-app="choicesApp">
<ng-form name="choicesForm" ng-controller="ChoicesCtrl">
<div ng-bind-html="trustCustom()"></div>
<button ng-repeat="button in buttons" ng-disabled="choicesForm.$invalid">
{{button.text}}
</button>
</ng-form>
</div>
The first div is your top level scope, your form is the first child scope. Adding the div using a function creates the dynamically added input field as a child of the first child, a grandchild of the top level scope. Therefore your form is not aware of the elements you're adding dynamically causing only the static field to be required for valid form entry.
A better solution would be to use ng-inclue for additional form fields or if your form isn't to large then simply put them on the page or template you're using.

Angularjs, checking if radio buttons in form have been selected

I'm starting with AngularJS, and I'm building a multi-step form where user has to fill different pages. When finished a page, he's allowed to press a next button and fill the following page.
For the first page, I've built in the HMTL a form (named pageOneForm), with different text input fields, marked as required, and in the relative controller I'm doing this watch:
$scope.$watch('pageOneForm.$valid', function(validity) {
ModelData.actualPageCompleted = validity;
})
And it works like a charme. My model (ModelData) is updated.
I was trying to apply the same logic to the following part of the app, the second page. Instead of input text, the user has to select two options from 2 different radio buttons groups.
So I built in the html a list of buttons via ng-repeat :
<div ng-Controller="PageTwo" ng-show='data.actualPage == 2'>
<form name="pageTwoForm">
<h3>General Information > Knowledge About </h3>
<div>
<b>User</b>
<div ng-repeat="option in userOptions">
<input type="radio" name="userGroups" ng-model="data.knowledgeAboutUser" ng-value="option.id" id="{{option.id}}" required>{{option.text}}
</div>
<div ng-repeat="option in targetGroupUserOptions">
<input type="radio" name = "targetUserGroup" ng-model="data.knowledgeAboutTargetGroup" ng-value="option.id" id="{{option.id}}" required>{{option.text}}
</div>
</div>
</form>
and I've implemented the same code as above in its controller:
$scope.$watch('pageTwoForm.$valid', function(validity) {
ModelData.actualPageCompleted = validity;
})
but apparently it doesn't work, and in my model actualPageCompleted is always true...
What am I doing wrong?
Thanks
I did my best to create a controller with some dummy data to get a fiddle working with your example code. Here is the fiddle You need to force the $digest cycle to update your form's validity state on ng-click for the radio buttons (see this SO post for more details), which is why the method
$scope.forceDigest = function(){
setTimeout(function(){ $rootScope.$$phase || $rootScope.$apply(); });
};
is necessary. Alternatively, you can get rid of the method call and uncomment the html code
<h3 ng-show="false">{{data.knowledgeAboutTargetGroup}}</h3>
<h3 ng-show="false">{{data.knowledgeAboutUser}}</h3>
in the fiddle to force the form object to update as well.
And I would make sure that ModelData.actualPageCompleted is not retaining its true value from when pageOneForm.$valid became true and it was set.
I hope that this helps!

AngularJS checkbox model value is undefined

I have a problem where I'm attempting to post the value of a checkbox in my model to the server and as the checkbox has not been interacted with on the form, angular seems to have not assigned it a value, when I ask for the value of the checkbox it comes back as undefined.
Here is my markup:
<div class="form-group">
<input id="templateDisable" type="checkbox" ng-model="template.disabled" />
<label for="templateDisable">Disabled</label>
</div>
And here's a reduced version of my save action on my controller:
$scope.save = function (form) {
if (form.$valid) {
var formData = new FormData();
// this is the problem line of code
formData.append("disabled", $scope.template.disabled);
// ... some other stuff
}
};
Actually, ticking then unticking the checkbox before I hit the save action results in the template.disabled property being false, which is what I would have expected without any manual intervention.
I've seen other related questions, e.g. AngularJS: Initial checkbox value not in model but surely stuff like a simple checkbox should be baked in? I shouldn't have to be writing directives to manage checkboxes surely?
This is per design. If you want a default value on your model than you should initialise it inside the controller (recommended), or make use of ng-init.
app.controller('AppController',
[
'$scope',
function($scope) {
$scope.template = {
disabled = false
};
}
]
);
<div class="form-group">
<input type="checkbox" ng-model="template.disabled" ng-init="template.disabled=false" />
<label>Disabled</label>
</div>
The following will always set the state back to "unchecked" when the page is loaded (or refreshed). In other words it will overwrite the user's actual selection whenever the page is refreshed.
<input type="checkbox" ng-model="template.disabled"
ng-init="template.disabled=false" />
If, however, you want the checkbox state set to a default state initially and you also want it to remember user interactions, then the following is what you want.
<input type="checkbox" ng-model="template.disabled"
ng-init="template.disabled = template.disabled || false" />

AngularJs empty form and remove invalid state from inputs

I'm using a form to add elements to list that is displayed on the side of the form.
Markup is:
<form name="thingForm">
<input required type="text" ng-model="thing.name"/>
<input required type="text" ng-model="thing.value"/>
<input type="submit" ng-click="addThing(thing)"/>
</form>
<ul>
<li ng-repeat="thing in things">{{thing.name}} with value of {{thing.value}}</li>
</ul>
And in a controller I have:
$scope.things = [];
$scope.addThing = function(thing) {
$scope.things.push(thing);
$scope.thing = {};
};
Working jsfiddle: http://jsfiddle.net/cXU2H/1/
Now as you can see, I can empty the form by emptying the model, however since the inputs have the required tag the browser still displays an error message (at least Chrome does).
I looked at the similar questions and:
I've also looked at this answer: https://stackoverflow.com/a/16296941/545925 however the jsfiddle behaves exactly the same as in my example: after the input is cleared it still has an ng-invalid-required class remaining (and it also triggers a HTML5 error message)
since I'm not on the 1.1.x branch $setPristine() is not available for me $setPristine() behaves the same way
I can of course write a function that iterates through the elements of a form and removes every ng-invalid-required and ng-invalid class, but that is not the way I would like to solve this. That is what I would do with jQuery.
Are you using $setPristine right? You can easily see in your fiddle that if you add it, it works. http://jsfiddle.net/X6brs/
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.things = [];
$scope.addThing = function(thing) {
$scope.things.push(thing);
$scope.thing = {};
$scope.thingForm.$setPristine(true);
};
}
$scope.thingForm.$setPristine();
$scope.thingForm.$setUntouched();
will do the trick.

Categories

Resources