angularjs - streamline form (automatic) submission based on dirty scope - angularjs

Problem space
I have a problem where I'm submitting a form based on criteria being fulfilled, rather than having a form submission button.
Let's say I have 3 drop downs, the first two are grouped but one needs to be selected, meaning I can select one or the other but I can't leave them empty, the 3rd one is a required field.
After that, the page automatically fetches in results.
Lets say I have checkboxes and a few more dropdowns. Any future selections on the 3 dropdowns mentioned, checkboxes, and dropdowns automatically filters the results.
What I know
Now after reading angular documentation, I was checking up on $dirty, $pristine and operations on both, like $setDirty and $setPristine; however, it seems that this is for a FormController
So I'm assuming this is useful for an entire scope. I didn't see any inclination that I can figure out for selected scopes.
What I have so far
So basically, I was hoping that I'd be making use of the scope's tracking features, but I don't know much about it. I created a single controller for my application and a single scope, since that's what seemed easiest for me. I have 3rd party plugins that play a role into the scope like:
$scope.3rdpartyConfig = {
prop1: [],
prop2: getData()
}
I don't think something like that would be useful in checking to see form submission if I was going to check the $dirty state of my form.
Then I thought about the old way I used to do things, but "angularlizing" it:
so I'd have something like:
<input type="checkbox" ng-model="state.Checked" ng-change="checkIfWeCanSubmitThenSubmit()" id="ng-change-example1" />
So I'd be having ng-changes and ng-clicks all over my html form, hitting that function, where the function would look like this pseudocode:
$scope.checkIfWeCanSubmitThenSubmit= function() {
var validated = false;
//check to see if dropdown1 or dropdown2 are selected
//check to see if dropdown3 is selected
// add more here per requirement
//if the above are true, then validated = true
if (validated)
{
//add dropdown4 and 5, and checkbox groups into filter
}
submit();
}
But I was thinking this isn't the angular way of doing things since this certainly isn't facilitated.
I was hoping that the scope would offer some kind of way, where I can check to see what pieces of my scope is dirty or not before I can submit and fetch data, or if there is a better way than appending this function to every html element; like having some kind of scope tracker that I can check up on and watch.
Which reminds me, I don't want to have a series of $scope.$watch either, its just that it'd be way too much work to bind to every piece of html code, unless there's way to watch the scope of a collection of specific scope variables, then, I wouldn't mind.
like (forgive the pseudocode):
$scope.$watch('dropdown1, dropdown2, dropdown4', function(dirty, pristine)
{
if (dirty)
{ blah blah blah }
});
Edit (2/28/2013):
I tried doing it this way:
$scope.masterCriteria =
[
{ DropDown1: $scope.AppModel.Dropdown1},
{ DropDown2: $scope.AppModel.Dropdown2 },
{ DropDown3: $scope.AppModel.Dropdown3 },
{ Checkbox1: $scope.AppModel.Checkbox1 },
{ Checkbox2: $scope.AppModel.Checkbox2 }
];
$scope.$watch('masterCriteria', function (newVal) {
if (newVal) { logger.info("did I change?"); }
}, true);
The watcher detected nothing, and any values I changed to the scope of AppModel wasn't being picked up in the $watch. Was worth a try, still trying to figure this out.

You can slightly change your model and group fields related to input form together. Put them into single object. Like this:
$scope.state = { checkbox1: false, checkbox2: true, ... }
Later bind input boxes to field of state object:
<input ng-model="state.checkbox1" ... >
And watch state object to catch all updates of nested fields:
$scope.$watch('state', ...
JsFiddle example here

Related

Angularjs form.$dirty

I'm able to find form data is changed or not using $dirty.
ex: I changed text box or drop down and then $dirty become true. If I reverted to old data still it is true. I need to know if my changes are reverted or not. Do we have any property in Angularjs? If property is true I want to enable save button otherwise it should be disable.
https://docs.angularjs.org/api/ng/type/form.FormController
I need to implement around 10 pages and each page has 10 text boxes and a couple of drop downs. So I don't want track each control manually in my pages.
You can try using this module: https://github.com/betsol/angular-input-modified
From the README file:
This Angular.js module adds additional properties and methods to the
ngModel and ngForm controllers, as well as CSS classes to the
underlying form elements to provide end-user with facilities to detect
and indicate changes in form data.
This extra functionality allows you to provide better usability with
forms. For example, you can add decorations to the form elements that
are actually changed. That way, user will see what values has changed
since last edit.
Also, you can reset an entire form or just a single field to it's
initial state (cancel all user edits) with just a single call to the
reset() method or lock new values (preserve new state) just by calling
overloaded $setPristine() method.
DISCLAIMER: I haven't tried it myself and I notice the author overwrites the ngModel directive instead of adding a decorator, which could be dangerous...but at the very least, you can look at the source and get an idea of how to write your own service or directive with similar functionality.
Even though it does not follow the usage of $dirty, but an implementation similar to this might be helpful for you in the case of a Save button on update.
Inside your html:
<form name="testForm" ng-controller="ExampleController" ng-submit=" save()">
<input ng-model="val" ng-change="change()"/>
<button ng-disabled="disableSave">Save</button>
</form>
Inside your controller:
.controller('ExampleController', ['$scope', function($scope) {
$scope.disableSave = true; // Keep save button disabled initially
$scope.val = 'Initial'; // Initial value of the variable
var copyVal = $scope.val; // Copy Initial value into a temp variable
$scope.change = function() {
$scope.disableSave = $scope.val === copyVal;
};
$scope.save = function() {
// Save the updated value (inside $scope.val)
console.log($scope.val);
// Re-disable the input box (on successful updation)
copyVal = $scope.val;
$scope.disableSave = true;
};
}]);
Here is a working plunkr for the same.

ui-bootstrap pagination with filter

after some research and study of examples I implemented a pagniation with a filter function.
Im very new to angular, so I need your help if this application is ok or it has some bugs/logical errors.
The target is to select a collection (in this application load1 or load2) and create new objects, manipulate existing, or delete some of them. On every update of the data, it has to be checked if the pagination is synchronous to the collection size.
If the user enters something into the search field, a watcher in the controller is fired for updating the filtered data:
$scope.$watch('search.name', function (newVal, oldVal) {
$scope.filtered = filterFilter($scope.items, {name: newVal});
}, true);
I would be very happy if some of you angular pros can look into this code and give me some feedback. I want to use this in a productive system, so every answer would be great!
Here is a working plunkr: http://plnkr.co/edit/j9DVahEm7y1j5MfsRk1F?p=preview
Thank you!
Watchers are heavy if you use them explicitly throughout your large application.
Use ng-change instead. Also, by passing true to that watcher means you're deep watching which is really a bad thing to do, since it will check each property of the object in the array which is performance intensive.
Since I can't see that you need old and new value for a reason, you can simply use $scope.search.name. Whenever you type in something, $scope.search.name has the updated value. Just need to call a function on ng-change.
DEMO: http://plnkr.co/edit/TWjEoM3oPdfrHfcru7LH?p=preview
Remove watch and use:
$scope.updateSearch = function () {
$scope.filtered = filterFilter($scope.items, {name: $scope.search.name});
};
In HTML:
<label>Search:</label> <input type="text" ng-model="search.name" placeholder="Search" ng-change="updateSearch()" />
Previous answer is still the correct, but you will have to make sure to replace the "page" inside the pagination tag and change it to ng-model.
From the changelog (https://github.com/angular-ui/bootstrap/blob/master/CHANGELOG.md)
Since 0.11.0:
Both pagination and pager are now integrated with ngModelController.
page is replaced from ng-model.

Watch form model for changes

Assuming a given form such as <form name="myForm">, it's easy enough to watch for validity, error, dirty state, etc. using a simple watch:
$scope.$watch('myForm.$valid', function() {
console.log('form is valid? ', $scope.myForm.$valid);
});
However, there doesn't appear to be an easy way to watch if any given input in this form has changed. Deep watching like so, does not work:
$scope.$watch('myForm', function() {
console.log('an input has changed'); //this will never fire
}, true);
$watchCollection only goes one level deep, which means I would have to create a new watch for every input. Not ideal.
What is an elegant way to watch a form for changes on any input without having to resort to multiple watches, or placing ng-change on each input?
Concerning the possible duplicate and your comment:
The directive solution in that question works, but it's not what I had in mind (i.e. not elegant, since it requires blur in order to work).
It works if you add true as third parameter for your $watch:
$scope.$watch('myFormdata', function() {
console.log('form model has been changed');
}, true);
Further information see the docs.
Working Fiddle (check console log)
Another more angular way would be to use angular's $pristine. This boolean property will be set to false once you manipulate the form model:
Fiddle
Based on my experience with my forms (new dev, but working with Angular for a while now), the elegant way to watch a form for changes is actually not to use any type of watch statement at all actually.
Use the built-in Angular boolean $pristine or $dirty and those values will change automatically on any input field or checkbox.
The catch is: it will not change the value if you add or splice from an array which had me stumped for a while.
The best fix for me was to manually do $scope.MyForm.$setDirty(); whenever I was adding or removing from my different arrays.
Worked like a charm!

What is the correct way to handle AJAX driven DOM changes in Angular.js?

I realize that "the correct way" is subjective but I think this is a specific enough question that there is a best practices approach to it.
I'm new to Angular and trying to understand what the prescribed mechanism is for the following.
I have a series of dependant <SELECT>s which don't have any data associated with them at the time of launch.
The first one goes and fetches some items (that need to be populated as <option>s) via $http and the resulting JSON response is used to populate the next <SELECT>.
Depending on the response there may or may not be subsequent <SELECT>s, meaning if the user chooses option 1 there is a follow up choice but if they choose option 2 there isn't and I don't wish to hard code all the possible <SELECT>s into my model, I need it to be elastic.
So... from what I'm reading, the controller is not the right place to deal with this, and I should use a directive, though I'm having a hard time finding documentation on how exactly to handle the specifics of modifying the DOM as necessary to introduce new <SELECT>s as required. Additionally I'm not clear on where I should do my AJAX calls and how to connect them to whatever it is that will respond by modifying the UI.
I'm hoping someone can point me to some effective tutorial on how to deal with this (or similar) scenarios.
You are absolutely right that you need to use a directive to do the DOM manipulation, but in this case I don't think you'll have to write any of your own, you can use the built in ones that angular provides.
You should also stick to the best practice of providing your data (in this case, option values etc.) through a service. I'm going to assume you can handle the service side of things yourself. Because I am lazy I will just manually enter the data into the scope in my controller (you will still need a minimal controller to get the data from the service to the scope).
The first one goes and fetches some items (that need to be populated as <option>s) via $http and the resulting JSON response is used to populate the next <SELECT>.
It's not clear if you've worked out how to do this already or not, but you'll want to use the ng-options directive:
Provided you have data like this:
[
{ key: "Ford fiesta", value: "fordFiesta" },
{ key: "Audi TT", value: "audiTT" }
]
You can use the following
markup:
<select ng-model="selection"
ng-options="options.label as (options.key, options.value) in options">
Again, I'm being lazy so I used a simpler markup later where the key is the same as the value.
Depending on the response there may or may not be subsequent <SELECT>s, meaning if the user chooses option 1 there is a follow up choice but if they choose option 2 there isn't and I don't wish to hard code all the possible <SELECT>s into my model, I need it to be elastic.
For this you will need a more complex data structure than simply the array of options. For my example I devised something like the following:
[
{
modelName: "apples"
title: "Do you like apples?"
options: [ "yes", "no" ]
followUps: [
{
modelName: "appleType"
condition: "yes"
title: "Do you prefer Granny smiths or snow white?"
options: ["Granny Smith", "Snow White"]
}
]
},
{
modelName: "pears"
title: "Do you like pears?"
options: [ "yes", "no" ]
}
]
modelName will be how we save the results, followUps are dependent selects that are shown if the answer is condition.
You can then make use of ng-repeat to loop through this array.
Note the below code is Jade:
div.question(ng-repeat="select in selects")
span.title {{select.title}}
select(ng-model="results[select.modelName]",
ng-options="option for option in select.options")
div.subquestion(ng-repeat="subselect in select.followUps",
ng-show="!subselect.condition ||
subselect.condition == results[select.modelName]")
span.title {{subselect.title}}
select(ng-model="results[subselect.modelName]",
ng-options="option for option in subselect.options")
Essentially what you are doing is repeating your title followed by the select populated with the options (using ng-options), as well as all the followUps selects, but we control the visibility of the followUp selects based on whether the answer matches the condition or not using the ng-show directive.
This could be neatened up significantly (make your own directive with a template), and also made tolerant to an infinite number of layers of followUps, but hopefully this puts you on the right track?
See it working in this plunker.
Here is a good video from the AngularJS conference in Salt Lake City... he covers some of what you are interested in within 20 min.
http://youtu.be/tnXO-i7944M?t=15m20s
AJAX request belongs in a factory, and that factory is injected in the controller as a dependency.
EDIT: So totally missed the guts of your question, sorry about that. You would setup the select using the ng-repeat directive like so:
<select ng-repeat="select in selects">
<option ng-repeat="option in select.options" handle-fetch-select>{{ option }}</option>
</select>
app.factory('selectFactory', function (['$http']){
var factory = {};
factory.getSelects = function(){
return $http.get('/selects.json');
}
factory.getSomeOtherSelect = function(){
return $http.get('/otherSelects.json');
}
return factory;
});
app.controller('SelectController', function( ['$scope', 'selectFactory'] ){
$scope.selects = [];
init();
function init(){
selectFactory.getSelects().success(function(data){
//would be $scope.selects = data; just mocking a response
$scope.selects = [ { label : 'Foo', options : ['opt1', 'opt2', 'opt3']} ]};
});
}
});
app.directive('handleFetchSelect', function(['$scope', 'selectFactory']){
return function(scope, element, attrs){
element.bind('click', function(){
//
//Add logic to determine if a fetch is required or not
//
//if (noFetchRequired)
// return;
//determine which selects to request from server
switch (expression) {
case (expression1) :
selectFactory.getSomeOtherSelect.success(function(returnedArrayOfSelects){
scope.apply(function(returnedArrayOfSelects){
scope.selects.concat(returnedArrayOfSelects);
});
}).error(function(){});
break;
}
}
})
});
Didn't debug this stub so... <-- disclaimer :) Hopefully you get the idea.

AngularJS typeahead select on blur

I'm using typeahead through in my AngularJS project and I would like to have it select the entry if I type the full value and click out of the field.
I've put together an example of what I mean
http://plnkr.co/edit/NI4DZSXofZWdQvz0Y0z0?p=preview
<input class='typeahead' type="text" sf-typeahead options="exampleOptions" datasets="numbersDataset" ng-model="selectedNumber">
If I type in 'two' and click on 'two' from the drop down then I get the full object {id: 2, name: 'two'}. This is good, if however I type 'two' and click to the next field without selecting is there a way to accept the top of the list on loss of focus on a text field?
I'm not sure if I'd want to have that sort of functionality in my app. The user hasn't actually selected anything. So selecting something for them would introduce frustrations.
But I do understand that often odd requirements are needed. In this case, I'd attack it using ngBlur. Assign a function to be called on blur. You can grab the contents of ng-model and then loop through your data (assuming static & not being sent via server) to find a match.
You can most likely just look at the source code of your typeahead directive and strip out the part does the comparison and then choose the first item in the array.
Unfortunately the underlying component does not emit any events for this condition. This will make the solution more complex. However when the value is being entered and the Typehead magic has happened you can supplement those events and catch them to update your ngModel.
I have created a plnkr based on your plnkr and although have not cleaned up but it is a working plnkr doing by far what you need.
The gist of this is following code however you can put this code wherever best suited
The explanation below:
//Crux - this gets you the Typeahead object
var typeahead = element.data('ttTypeahead');
//This gets you the first
var datum = typeahead.dropdown.getDatumForTopSuggestion();
if (datum){
//you can do lot of things here however
//..I tried to - fill in the functionality best suited to be provided by Typeahead
//for your use case. In future if Typeahead gets this
//..feature you could remove this code
typeahead.eventBus.trigger("hasselections", datum.raw, datum.datasetName);
}
In the above code you can also save the datum somewhere in the scope for doing whatever you like with it later. This is essentially your object {num: 'Six'} Then you may also use ngBlur to set it somewhere (however the plnkr I created doe snot need these gimmicks.)
Then further down - ngModel's value is set as below
element.bind('typeahead:hasselections', function(object, suggestion, dataset) {
$timeout(function(){
ngModel.$setViewValue(suggestion);
}, 1);
//scope.$emit('typeahead:hasselections', suggestion, dataset);
});
I'm with EnigmaRM in that ngBlur seems to be the way to do what you want. However, I agree with the others that this could be somewhat strange for the end users. My implementation is below (and in plnkr). Note that I trigger on ngBlur, but only apply the model if and only if there is only one match from Bloodhound and the match is exact. I think this is probably the best of both worlds, and hope it should give you enough to go on.
$scope.validateValue = function() {
typedValue = $scope.selectedNumber;
if(typedValue.num !== undefined && typedValue.num !== null)
{
return;
}
numbers.get(typedValue, function(suggestions) {
if(suggestions.length == 1 && suggestions[0].num === typedValue) {
$scope.selectedNumber = suggestions[0];
}
});
};

Resources