Why does $digest() is not being called automatically from Parse callback? - angularjs

I have a select2 drop down list that is bound to a data in my scope ($scope.makes).
It Is "living" inside an ng-modal.
I update $scope.makes inside a promise, which is a Parse.com call (parse.cloud.run) but after that it is over the DDL is not refershed with the new data.
But if I add the call of $scope.$digest() it is working.
Why is that?
This is the modal controller's relevant part:
Parse.Cloud.run('getCarMakes', {}, {
success: function (results) {
console.log('C');
if (results===undefined){
console.log('The query has failed');
return;
}
for (var i=0;i<results.length;i++){
$scope.makes.push(results[i]);
}
$scope.$digest();
If i remove the last line - the dropdown list is not being refreshed.
Here's the HTML part of the Modal that is relevant
<label class="control-label" for="MakeSelect">Make </label>
<select class="form-control col-md-2" id="MakeSelect" name="make" ui-select2 ng-model="carDetails.make">
<option ng-repeat="make in makes" value="{{ make }}">{{ make}}</option>
</select>

That's because success is out of Angular's scope, you need to explicitly tell Angular that something was updated it must run $digest or $apply(which internally calls $digest) to update it's scope too.
Angular will account for only those model changes which are done inside AngularJS’ context, it has no idea about callback and can't update itself, as it would if change was inside it's scope.
For more information on how you should tell Angular to update it's scope, with $digest() or $apply() read this article or this question $apply vs $digest in directive testing.

Related

angular $scope digest update from a dom event

I have an angular $scope variable that gets instantiated through a window event. The $scope variable is displayed correctly to the user, but updates are not being set through ng-model. What's odd is that ng-change has the correct value that I can use to manually set the scope variable.
I really don't understand why this is happening.
Event Handler:
document.addEventListener('updateSearchBar', handleUpdate);
function handleUpdate(eventData) {
$scope.benefitsFromDate = eventData.detail.fromDate;
$scope.$apply();
}
Front end:
<input
type="date"
name="fromDate"
ng-change="updateBenefitsFromDate(benefitsFromDate)"
ng-model="benefitsFromDate"
/>
<input
type="button"
value="Search"
class="btn btn-default"
ng-click="searchDocuments()"
ng-disabled="isSearchingDocuments"
/>
Angular snippet:
$scope.updateBenefitsFromDate = function(change) {
console.log($scope.benefitsFromDate); //still has old value
console.log(change); //has updated value when param is the $scope variable
//$scope.benefitsFromDate = change; //only way to update
};
$scope.searchDocuments = function() {
//ng-model never updates the variable when the value is changed and uses the instantiated value
console.log($scope.benefitsFromDate);
};
Why doesn't ng-model reflect the change, but passing $scope.benefitsFromDate in the ng-change function have the updated value?
I use the $timeout module when processing these kind of updates and avoid the call to $scope.$apply().
$timeout(function() {
$scope.benefitsFromDate = eventData.detail.fromDate;
});
I learned that angular is finicky about "dot rules".
All I had to do was prefix my data values with something else.
$scope.data = {};
$scope.data.benefitsFromDate = ...;
Or you can declare your controller to use as
ng-controller="appController as vm"

Directive with ng-model Attribute Not Resolving Using $http

Trying to make a rating directive but I'm stuck at getting rating2 to work. The first rating worked because the rating1 is hardcoded within the controller. But normally I have to get the saved rating from the db, which I'm trying to do with rating2, as u can see the value is fetched but the directive is not appearing.
https://codepen.io/eldyvoon/pen/MbBNLP
<div star-rating ng-model="rating.rating1" max="10" on-rating-select="rating.rateFunction(rating)"></div>
<br>but rating2 is actually there:
{{rating.rating2}}
<star-rating ng-model="rating.rating2" readonly="rating.isReadonly"></star-rating>
Need expert of directive to help.
Initiate rating2 :
function RatingController($http) {
this.rating1 = 5;
this.rating2 = 0; //ADD THIS LINE
var self = this;
it works for me
check here
First of all, I'm not a directive expert but i'm trying to help. I think that when html is first load, the values from db not finish execute and bind into html. The best way is not using directive instead using controller to fetch data from db.
You pass a model without rating2 into your directive and the changes from the parent controller won't affect it, because variable is created afterwards. Adding a watcher in your linker on parent scope will solve the problem;
scope.$parent.$watch('', function(rating){
updateStars();
});
Other solution would be to define a starting value in your controller.
this.rating2 = 1;
Notice that it is bad design to have a scope variable for each rating. It is cleaner to have an array of ratings and you actually do not need the watcher by doing so.
https://codepen.io/hoschnok/pen/LbJPqL
angular controller
function RatingController($http) {
this.ratings = [4];
var self = this;
$http.get('https://api.myjson.com/bins/o0r69').then(function(res){
self.ratings.push(res.data.rating2);
});
}
HTML
<div ng-app="app" ng-controller="RatingController as rating" class="container">
<div ng-repeat="r in rating.ratings">
<div star-rating ng-model="r" max="10" on-rating-select="rating.rateFunction(rating)"></div>
</div>
</div>
The watcher change handler function has parameters reversed:
//INCORRECT parameters
//scope.$watch('ratingValue', function(oldValue, newValue) {
//CORRECT parameters
scope.$watch('ratingValue', function(newValue, oldValue) {
if (newValue) {
updateStars();
}
});
The first argument of the listening function should be newValue.
The DEMO on CodePen
ALSO
The ng- prefix is reserved for core directives. See AngularJS Wiki -- Best Practices
JS
scope: {
//Avoid using ng- prefix
//ratingValue: '=ngModel',
ratingValue: '=myModel',
max: '=?', // optional (default is 5)
onRatingSelect: '&?',
readonly: '=?'
},
HTML
<!-- AVOID using the ng- prefix
<star-rating ng-if='rating' ng-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
-->
<!-- INSTEAD -->
<star-rating ng-if='rating' my-model="rating.rating2"
max="10" on-rating-select="rating.rateFunction(rating)">
</star-rating>
When a custom directve uses the name ng-model for an attribute, the AngularJS framework instantiates an ngModelController. If the directive doesn't use the services of that controller, it is best not to instantiate it.

Angular checkbox update not running a full digest onclick to display template

How can I make checkbox run a full digest onclick, instead of onblur?
My problem is, the checkbox click only runs the local scope for the directive it is inside. I also have other directives that need to update onclick, instead of onblur.
I tried using ng-click and $scope.$apply() but I was getting $rootScope digest errors. After looking around a bit more I added $timeout around the $scope.$apply(). This fixed all my issues.
scope.ngclickApply = function(){
$timeout(function(){
scope.$apply();
},0,false)
}
and in the html
<label ng-click="ngClickApply()">
<input type="checkbox">{{label}}
</label>

Setting initial value of a select list generated through ng-repeat in angular

I am creating a select list from an associative array using ng-repeat.The ng-model of list is bound to a scope variable that has some initial value.The value of the options generated are the key of the array.
However, the list does not initialize with the value of the model.I thought it might have something to do with the asynchronous behaviour of ng-repeat, so i created a directive to capture the end of rendering and emit an event.This event is caught in the controller and is used to re-assign the value of the ng-model.
The event is caught correctly, but the update to the ng-model does not reflect.
If I attach a function to a button that updates the variable , it shows correctly in the list.
I know it is so because of the way ng-repeat works.How can i work around this automatically set the initial value after the list has been rendered.
Here is an example of my issue:
http://jsbin.com/gapemenova/1/edit?html,js,output
HTML:
<h1 align='center'>
{{value}}
<select ng-model='value' >
<option ng-repeat='(tid,groups) in templates' value='{{tid}}' on-last-repeat >{{tid}} </option>
</select>
<button ng-click='refresh()' >Refresh</button>
</body>
JS:
var app=angular.module('gobo',[]);
//Create A Directive To Detect The End Of ng-repeat cycle.
app.directive('onLastRepeat',function(){
return function(scope,element,attrs) {
if(scope.$last) { setTimeout(function(){
scope.$emit('onRepeatLast',element,attrs);
},10);
}
};
});
app.controller('goboCtrl',function($scope){
//Initial Value
$scope.value="2";
$scope.$on('onRepeatLast', function(scope, element, attrs){
$scope.value='3';
alert($scope.value); });
//Data Source For ng-repeat
$scope.templates={};
$scope.templates["1"]=[{"prop":"value1"}];
$scope.templates["2"]=[{"prop":"value2"}];
$scope.templates["3"]=[{"prop":"value3"}];
$scope.refresh=function(){
$scope.value="2";
};
}); //Controller ends
Check this working demo: JSBin
Simply change the option to:
<option ng-repeat='(tid,groups) in templates' value='{{tid}}'
ng-model="value" ng-selected="value === tid" on-last-repeat >{{tid}}</option>
Adding ng-model binds the selected value to $scope.value. Using ng-selected to update the selected item dynamically.
Explanations
Updating at the end of ng-repeat through $on is out of Angular $digest lifecycle, you need to run $scope.$apply to trigger a $digest cycle manually. Check JSBin
ngClick will trigger a $scope.$apply by default. Check ngClick definition in Angular 1.4.1 Line 23244:
scope.$apply(callback);
You should be using ng-options instead of using an ng-repeat to bind your options to your array. More on ng-options: https://docs.angularjs.org/api/ng/directive/ngOptions

Angular - data not available inside ng-init

Angular partial - HTML.
BaseCtrl
<div ng-controller="SelectTagCtrl">
<input type="text" ng-init="setTags(viewData['users'])" ui-select2="tagAllOptions" ng-model="tagsSelection" name="users" />
{{viewData['users']}} ECHOES CORRECTLY.
But undefined when passed inside ng-init callback.
</div>
<input type="text" class="span12" placeholder="Brief Description" name="description" value="{{viewData['description']}}">
ECHOES CORRECTLY.
Controller.js
function SelectTagCtrl(){
$scope.setTags = function(data){
// data is undfined when viewData['users'] is used. <-- PROBLEM
// correct when I pass some static string.
}
}
//POPULATING viewData to be used inside view partial.
function BaseCtrl(){
$http.get(url).success(function(data){
$scope.viewData = data.data || [];
$scope.view_type = $scope.viewData['view_type'];
$scope.fields = data.data.fields;
console.log($scope);
}).error();
}
Using timeout would be a workaround, instead I would take a $scope variable inside the controller to know if the ajax call has completed.
The problem is ng-init might get called before ajax completion.
I already had ui-if directive configured in my angular project, so I used it with the combination of $scope variable to get the things working.
<div ng-controller="SelectTagCtrl" ui-if="ajax_done">
<input type="text" ng-init="setTags(viewData['users'])" ui-select2="tagAllOptions" ng-model="tagsSelection" name="users" />
</div>
And inside controller,
$http.get(gurl + '.json').success(function(data,status){
// some stuff
$scope.ajax_done = true;
}).error();
Because of angular's magical two-way binding, the element will get updated. Now it sees that ajax request is completed, ui-if will get a truthy value and ng-init directive of the element will get a chance to execute its callback.
EDIT: ui-if was removed from Angular UI in favour of ng-if which is now built in.
Here are two different changes to your fiddle that appear to work.
Fiddle 1 - this version uses $scope.$apply(exp) as described in the documentation here and is useful when you are modifying angular bound data outside of the angular framework. In this example setTimeout is the culprit.
setTimeout(function(){
console.log("updateVal" );
$scope.$apply(function() {
$scope.updateVal2();
});
console.log($scope.tagsSelection);
},5000);
Fiddle 2 - this version uses angular's wrapper for setTimeout called the $timeout service.
$timeout(function(){
console.log("updateVal" );
$scope.updateVal2();
console.log($scope.tagsSelection);
},5000);

Resources