AngularJS Scope not updating in view after async call - angularjs

I am having trouble updating my scope on the front-end while making a request to an API. On the backend I can see that the value of my $scope variable is changing but this is not being reflected in the views.
Here is my controller.
Controllers.controller('searchCtrl',
function($scope, $http, $timeout) {
$scope.$watch('search', function() {
fetch();
});
$scope.search = "Sherlock Holmes";
function fetch(){
var query = "http://api.com/v2/search?q=" + $scope.search + "&key=[API KEY]&format=json";
$timeout(function(){
$http.get(query)
.then(function(response){
$scope.beers = response.data;
console.log($scope.beers);
});
});
}
});
Here is a snippet of my html
<div ng-if="!beers">
Loading results...
</div>
<p>Beers: {{beers}}</p>
<div ng-if="beers.status==='success'">
<div class='row'>
<div class='col-xs-8 .col-lg-8' ng-repeat="beer in beers.data track by $index" ng-if="beer.style">
<h2>{{beer.name}}</h2>
<p>{{beer.style.description}}</p>
<hr>
</div>
</div>
</div>
<div ng-if="beers.status==='failure'">
<p>No results found.</p>
</div>
I've tried several solutions including using $scope.$apply(); but this just creates the common error
Error: $digest already in progress
The following post suggested to use $timeout or $asyncDefault
AngularJS : Prevent error $digest already in progress when calling $scope.$apply()
The code I have above uses $timeout and I have no errors but still the view is not updating.
Help appreciated

I you are using AngularJS 1.3+, you can try $scope.$applyAsync() right after $scope.beers = response.data; statement.
This is what Angular documentation says about $applyAsync()
Schedule the invocation of $apply to occur at a later time. The actual time difference varies across browsers, but is typically around ~10 milliseconds. Source
Update
As others have pointed out, you should not (usually) need to trigger the digest cycle manually. Most of the times it just points to a bad design (or at least not an AngularJS-friendly design) of your application.
Currently in the OP the fetch method is triggered on $watch. If instead that method was to be triggered by ngChange, the digest cycle should be triggered automatically.
Here is an example what such a code might look like:
HTML
// please note the "controller as" syntax would be preferred, but that is out of the scope of this question/answer
<input ng-model="search" ng-change="fetchBeers()">
JavaScript
function SearchController($scope, $http) {
$scope.search = "Sherlock Holmes";
$scope.fetchBeers = function () {
const query = `http://api.com/v2/search?q=${$scope.search}&key=[API KEY]&format=json`;
$http.get(query).then(response => $scope.beers = response.data);
};
}

As the comments suggest, you shouldn't need to use $timeout to trigger a digest cycle. As long as the UX that elicits the change is within the confines of an angular construct (e.g. controller function, service, etc.) then it should manifest within the digest cycle.
Based on what I can infer from your post, you are probably using a search input to hit an API with results. I'd recommend changing the logic up such that you are triggering your search on an explicit event rather than the $watcher.
<input ng-model="search" ng-change="fetch()">
Remove the $watch logic and the $timeout wrapper.
function fetch(){
var query = "http://api.com/v2/search?q=" + $scope.search + "&key=[API KEY]&format=json";
$http.get(query)
.then(function(response){
$scope.beers = response.data;
console.log($scope.beers);
//it's a good habit to return your data in the promise APIs
return $scope.beers;
});
}
The reasons I make this recommendation is:
You have finer control of how the ng-change callback is triggered using ng-model-options. This means you can put a delay on it, you can trigger for various UX events, etc.
You've maintained a clearer sequence of how fetch is called.
You have possibly avoided performance and $digest issues.

Hey guys I solved the issue but I'm not sure exactly why this changed anything. Rearranging my code on JS Fiddle I just put all my partials into the index.html file like so and the requests and scope variables updated smoothly. Is was there perhaps a controller conflict with my html above?
<body ng-app="beerify" ng-controller='searchCtrl'>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container"><!-- nav bar code -->
</div>
</nav>
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1>Title</h1>
<form ng-submit="fetch()">
<div class="input-group">
<input type="text" ng-model="search"
class="form-control" placeholder="Search the name of a beer" name="srch-term" id="srch-term">
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button>
</div>
</div>
</form>
</div>
</div>
<div class="container">
<div ng-if="!beers">
Loading results...
</div>
<div ng-if="beers.status==='success'">
<div class='row'>
<div class='col-xs-8 .col-lg-8' ng-repeat="beer in beers.data track by $index" ng-if="beer.style">
<!-- ng-if will make sure there is some information being displayed
for each beer -->
<h2>{{beer.name}}</h2>
<h3>{{beer.style.name}}</h3>
<p>AbvMin: {{beer.abv}}</p>
<p>AbvMax: {{beer.ibu}}</p>
<p>{{beer.style.description}}</p>
<hr>
</div>
</div>
</div>
<div ng-if="beers.status==='failure'">
<p>No results found.</p>
</div>
</body>

Related

Deep watch on array not working in AngularJS data binding

I am using the popover directive from AngularStrap. I have a colleciton of objects that get rendered as toggles. When the toggles are clickedthe data on the scope is not changed... I've tried loads of different ways of doing this.
This is the controller of the popover
app.controller('CurvesPopoverCtrl', ['$scope', '$filter', 'AppData',
function($scope, $filter, AppData) {
'use strict';
$scope.popoverCurrencies = AppData.getCurrencies();
$scope.$watch ('popoverCurrencies', function(val) {
if(val !== null && val !== undefined){ // only fires when popover opens.
console.log("toggled", val);
//AppData.setCurrencies($scope.currencies);
}
}, true); // deep watch
}
]);
This is the template of the popover
<div class="well curves-popover" ng-controller="CurvesPopoverCtrl">
<div class="row">
<div ng-repeat="currency in popoverCurrencies">
<div class="clearfix" ng-if="$index % 3 == 0"></div>
<div class="col-md-4">
<div class="togglebutton">
<label>
<span ng-class="currency.iconClass"></span> <span>{{currency.ccy}}</span>
<input type="checkbox" checked="currency.build">
</label>
</div>
</div>
</div>
</div>
</div>
It gets a little complicated when you're attempting to watch a collection while using ng-repeat. According to the docs:
Each template instance gets its own scope ...
Therefore, the watch is not notified when the values within these other scopes are updated. One workaround is to bind a separate controller to each of the iterations of ng-repeat, as seen here. But another, possibly cleaner, approach is to utilize the ng-change directive to intercept the updating of these values. I explained it a bit here.

how to update all instances of the same controller

I know how to share data between controllers using service but this case is different so please rethink the question.
I have something like this for the UI:
<jsp:include page="../home/Header.jsp" />
<div data-ng-view></div>
<jsp:include page="../home/Footer.jsp" />
Inside the ng-view, I instantiated a controller instance using "data-ng-controller="BuildController as ctrl". It will run this function that might take up to 2 hours. After it's completed, the buildCompletionMsg is updated and pop up a box saying it's completed.
self.buildServers = function(servers, version) {
BuildService.buildList(servers, version).then(
function(data) {
self.buildCompletionMsg = data;
$('#confirmationModal').modal('show');
},
function(errResponse) {
console.error("Error getting servers." + errResponse);
}
);
};
The problem is that I want the modal to be in the Header.jsp file so doesn't matter which view the user is in, they would see the notification. Therefore in Header.jsp I have another controller instance using "data-ng-controller="BuildController as ctrl" and bind it using
<div data-ng-controller="BuildController as ctrl">
<div class="modal fade" id="confirmationModal" role="dialog" aria-labelledby="confirmLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-body">
<h3>{{ ctrl.buildCompletionMsg }}</h3>
</div>
</div>
</div>
</div>
</div>
As you can see, even if I do something like:
self.buildCompletionMsg = BuildService.getCompletionMsg();
it would only update the ctrl instance of the ng-view page, and the one inside Header.jsp is still null.
How can I update all the instances of BuildController in different pages or just update the one in the Header.jsp file?
I found the answer to my own question. The solution is to to have an object reference or array in the service (it does not work for simple string) like this:
angular.module('buildModule').factory('BuildService', ['$http', '$q', function($http, $q) {
var self = {};
self.completionStatus = { data: "" };
then upon $http success update the completionStatus
self.status.data = response.data;
And in the controller, the variable is set directly to this object
self.buildCompletionMsg = BuildService.completionStatus;
This updates the variable {{ buildCompletionMsg }} on all the pages.

Angular two the same controllers in single view

Wondering how to use 2 the same controllers in a single view in Angular, I separated my controllers in different files to make a clean ctrl structure, but wondering if you're allowed to do something like this?
Basically I want to update the table view after uploading a file, its somewhat working but I have to refresh the whole page for the view to refresh.
<div class="view">
<div ng-controller="myCtrl as vm">
controller the table
</div>
<div ng-controller="otherCtrl as vm"> </div>
<div ng-controller="myCtrl as vm">
<button ng-click="clickbtn()"></button>
</div>
</div>
This code is in myCtrl controller.
$scope.$on("$UploadFile", function (event) {
// do the action here digestion
});
$scope.uploadCv = function(event) {
$scope.$broadcast("$UploadFile");
}

angularJS shows empty slots in array but it's not

It's weird and I don't have any idea of how to solve this issue...
I have my controller with an ajax call using a service with promise, which works great.
horariosOcupadosService.getHorariosOcupados($scope.formData.cmbUnidade, $scope.formData.cmbDiaSemana).then(function(response) {
//when I set my variable with the result, everything is fine
//and I can iterate with ng-repeat with no problem
$scope.horariosOcupados = response;
$scope.formData.qtdeHorarios = $scope.horariosOcupados.length;
}), function(error) {
console.log(response);
};
But in my HTML, I have a problem... after my ng-repeat, which works fine, if I try to print $scope.horariosOcupados ({{ horariosOcupados | json }}), it prints:
[{},{},{}]
It only shows something if I change the value of the ng-model field.
<div id="horarios">
<div ng-repeat="horarioOcupado in horariosOcupados track by $index" id="horario{{$index}}" class="form-group" style="margin: 15px; 0px;">
<div class="col-md-2 text-center" id="remove"><a ng-href="#" ng-click="removeHorario($index)"><i class="fa fa-trash-o fa-2x"></i></a></div>
<div class="col-md-3">
<div class="input-group">
<span class="input-group-addon">Horário inicial</span><input name="hrEntra{{$index}}" type="text" ng-model="horariosOcupados[$index].hrInicial">
</div>
</div>
</div>
</div>
<!-- GRADE DE HORÁRIOS - FIM -->
<pre>{{ horariosOcupados | json }}</pre>
Could anyone help me to figure it out?
Thanks!
You are probably doing something outside of the angular-world.
How do you fetch your data asynchronously ?
Do you maybe use a ajax call besides the angular built-in $http service ? or maybe with the "normal" setTimeout function instead of using the angular $timeout version ?
My guess is that you are getting it somehow asynchronously without staying inside the angular world. Therefore angular does not know anything about these changes and will not update the view accordingly in time.
What you can do for the moment is try
$scope.horariosOcupados = response;
$scope.formData.qtdeHorarios = $scope.horariosOcupados.length;
$scope.$apply();
to "tell" angular that something has changed hence angular will re-render the changes immediately.
Idealy though it's most of the times not necessary to do so if you stay inside the angular-world and use the angular built-in services
I figured it out. the problem was the maxlength of my INPUT field. The length of "1900-01-01 08:00" > 5, so angularJS set it to undefined.
Thank you all for helping me!

Live search in AngularJS: updating the results

I want a live search: the results are queried from web api and updated as the user types.
The problem is that the list flickers and the "No results" text appears for a fraction of second, even if the list of results stays the same. I guess I need to remove and add items with special code to avoid this, calculating differences between arrays, etc.
Is there a simpler way to avoid this flicker at least, and probably to have possibility to animate the changes?
It looks like this now:
The html part is:
<div class="list-group">
<a ng-repeat="test in tests track by test.id | orderBy: '-id'" ng-href="#/test/{{test.id}}" class="list-group-item">
<h4 class="list-group-item-heading">{{test.name}}</h4>
{{test.description}}
</a>
</div>
<div ng-show="!tests.length" class="panel panel-danger">
<div class="panel-body">
No tests found.
</div>
<div class="panel-footer">Try a different search or clear the text to view new tests.</div>
</div>
And the controller:
testerControllers.controller('TestSearchListCtrl', ['$scope', 'TestSearch',
function($scope, TestSearch) {
$scope.tests = TestSearch.query();
$scope.$watch('search', function() {
$scope.tests = TestSearch.query({'q':$scope.search});
});
}]);
You should use ng-animate module to get the classes you need for smooth animation. For each ng-repeat item that's moved, added, or removed - angular will add specific classes. Then you can style those classes via CSS or JS so they don’t flicker.
An alternative way of doing what you require is to use the angular-ui bootstrap Typeahead component (check at the bottom of the post). It has a type-ahead-wait property in milliseconds and also a template url for customising it.
<div ng-app>
<div ng-controller="MyController">
<input type="search" ng-model="search" placeholder="Search...">
<button ng-click="fun()">search</button>
<ul>
<li ng-repeat="name in names">{{ name }}</li>
</ul>
<p>Tips: Try searching for <code>ann</code> or <code>lol</code>
</p>
</div>
</div>
function MyController($scope, $filter) {
$scope.names = [
'Lolita Dipietro',
'Annice Guernsey',
'Gerri Rall',
'Ginette Pinales',
'Lon Rondon',
'Jennine Marcos',
'Roxann Hooser',
'Brendon Loth',
'Ilda Bogdan',
'Jani Fan',
'Grace Soller',
'Everette Costantino',
'Andy Hume',
'Omar Davie',
'Jerrica Hillery',
'Charline Cogar',
'Melda Diorio',
'Rita Abbott',
'Setsuko Minger',
'Aretha Paige'];
$scope.fun = function () {
console.log($scope.search);
$scope.names = $filter('filter')($scope.names, $scope.search);
};
}

Resources