Using Angular Promises and Defers effectively - angularjs

In Angular (and all SPA JS Frameworks), it is assumed that page navigation be extremely quick and "seamless" to the user. The only bottleneck to this speed is the use of API calls to retrieve data from our server. Therefore, it seems reasonable to find a solution where we can render an entire page, except for the physical data being presented, while we wait for our API call to get a response.
I am fairly new to Angular and I am running into a few problems that are keeping me from achieving the above goal and functionality. Let me take you through the 2 problems I came across while learning Angular
Problem 1: API response is too slow for Angular
At first, I was simply calling my API from within the controller. I have multiple tables of data that are going to be populated after I receive my response. Before the response is gotten, my tables have a simple <tr> that contains a .gif "spinner" that represents the loading of data. I have a $scope.gotAPIResponse property that is set to false initially and then after the API responds, it gets set to true.
I use this boolean in order to show and hide the "spinner" and the table data respectably. This causes the HTML to render successfully and everything looks fine to the user. However, a closer look at the JavaScript console and you can see that Angular threw many errors because it was attempting to $compile my templates BEFORE the API call got a response. Errors are never a good thing, so I had to improvise.
Inside the 'LandingController'
function init() {
$scope.gotAPIResponse = false;
// Get Backup data
API.Backups.getAll(function (data) {
console.log(data);
$scope.ActiveCurrentPage = 0;
$scope.InactiveCurrentPage = 0;
$scope.pageSize = 25;
$scope.data = data.result;
$scope.gotAPIResponse = true;
});
}
Here is the HTML template that throws errors.. both native template and pagination directive throws errors
<tbody>
<tr ng-hide="gotAPIResponse"><td colspan="6"><p class="spinner"></p></td></tr>
<tr ng-if="gotAPIResponse" ng-repeat="active in data.ActiveBackups | paginate:ActiveCurrentPage*pageSize | limitTo:pageSize">
<td><a ui-sref="order.overview({ orderId: active.ServiceId })" ng-click="select(active)">{{ active.Customer }}</a></td>
<td>{{ active.LastArchived }}</td>
<td>{{ active.ArchiveSizeGB | number:2 }}</td>
<td>{{ active.NumMailboxes }}</td>
<td>{{ active.ArchivedItems }}</td>
<td><input type="button" class="backup-restore-btn" value="Restore" /></td>
</tr>
</tbody>
</table>
<div pagination backup-type="active"></div>
Problem 2: Using resolve property is blocking everything
I then looked towards the resolve property that is available in the $stateProvider (ui-router) and $routeProvider (basic Angular). This allowed me to defer the instantiation of the Controller until the $promise on my API call was received.
This worked how it should have and I no longer received any errors because the API response was available before the HTML template could be rendered. The problem with this however, was that when you attempted to go to that state or route, the entire view would be unavailable for a split second until the $promise was received. This would appear to "lag" or "block" to the user (which just looks terrible)
Using resolve property
$stateProvider
.state('dashboard', {
url: '/',
controller: 'LandingController',
resolve: {
res: ['API', function(API){
return API.Backups.getAll(function (data) {
return data.result;
}).$promise;
}]
}
})
Then this controller is instantiated after API call, but the time between this is like 1-2 seconds...way too slow!
app.controller('LandingController', ['$scope', 'API', 'res', function ($scope, API, res) {
$scope.data = res.result;
...
Solution: Right in between
In order to solve this, we need a solution that is right in between the 2 that I have already tried. We need to instantiate the controller as quick as possible so $scope.gotAPIResponse can be set and therefore cause the "spinner" and rest of the DOM to show, however, we need to defer just specific parts of the HTML template and other Directives so that Errors are not thrown.
Is there an effective solution to this problem that people are using in production?

Update
In the end, this ended up being a much simpler fix. After some discussion we refactored the view a bit in order to avoid the ng-repeat executing before the ng-if which wasn't the desired behavior.
So instead of this:
<tbody>
<tr ng-if="!gotAPIResponse">
<!-- show spinner -->
</tr>
<tr ng-if="gotAPIResponse" ng-repeat="stuf in stuffs">
<!-- lots o' DOM -->
</tr>
</tbody>
We pulled the ng-if up one level to this:
<tbody ng-if="!gotAPIResponse">
<tr >
<!-- show spinner -->
</tr>
</tbody>
<tbody ng-if="gotAPIResponse">
<tr ng-repeat="stuf in stuffs">
<!-- lots o' DOM -->
</tr>
</tbody>
This way the ng-if isn't being duplicated for each row, and will halt further processing until the condition is satisfied.
Original Answer:
This is actually simple to achieve by using a directive and listening to some route change events on the $rootScope
The idea is to toggle a property on scope that can be used to Show/Hide some element. In this case a simple "Loading..." text, but in your case it could easily be used to show a spinner.
var routeChangeSpinner = function($rootScope){
return {
restrict:'E',
template:"<h1 ng-if='isLoading'>Loading...</h1>",
link:function(scope, elem, attrs){
scope.isLoading = false;
$rootScope.$on('$routeChangeStart', function(){
scope.isLoading = true;
});
$rootScope.$on('$routeChangeSuccess', function(){
scope.isLoading = false;
});
}
};
};
routeChangeSpinner.$inject = ['$rootScope'];
app.directive('routeChangeSpinner', routeChangeSpinner);
Then inside your HTML you would simply do this:
<div class="row">
<!-- I will show up while a route is changing -->
<route-change-spinner></route-change-spinner>
<!-- I will be hidden as long as a route is loading -->
<div ng-if="!isLoading" class="col-lg-12" ng-view=""></div>
</div>
Voila! Simple way to show a loading indicator while changing routes.
Full Punker Example

This is what you can do (example below):
Encapsulate your HTML in a directive
Create data in your controller (which you receive from the server side) and initialize it to an empty or initial value
Pass this data to your directive to have an isolated scope
Now fetch the data in your controller explicitly using the $http service and on resolution of the promise set the data tied to the directive to the data retrieved from the server
The directive will automatically update when the $http service code runs in the controller
Here's an explanation of the example I provided:
The controller initializes data to [1,2] but then calls the function changedata() which calls $http service to get the weather in New York and adds that to the data array. Hence you see "Sky is Clear" (or something else depending on when you run this example) rendered in your directive.
Here's the plnkr showing directive update with $http
// index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.2.x" src="https://code.angularjs.org/1.2.20/angular.js" data-semver="1.2.20"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<input type="button" ng-click="changedata()" value="Change"/>
<div myview data="model.data"></div>
</body>
</html>
// app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $http) {
$scope.name = 'World';
$scope.model = {
data: ["1", "2"]
};
$scope.changedata = function() {
// updating data explicitly
$scope.model.data = [4, 5, 6];
url = "http://api.openweathermap.org/data/2.5/weather"
return $http.get(url, {
params: {
q: "New York, NY"
}
}).then(function(result) {
console.log("result=");
console.log(result);
if (result !== null) {
$scope.model.data.push(result.data.weather[0].description);
return result.data.weather[0].description;
}
return "Weather not found";
});
};
// updating data implicitly
$scope.changedata();
});
app.directive("myview", function() {
return {
restrict: 'EA',
template: '<table><tr ng-repeat="d in data"><td>{{d}}</td></tr></table>',
scope: {
data: '='
}
};
});

Your first problem of getting errors when the template renders is because data does not exist until after the promise resolves. A simple $scope.data = { ActiveBackups:null } before initiating your API call should solve that. When angular evaluates the ng-repeat expression active in data.ActiveBackups it will no longer be trying to reference the undefined attribute data.
If you have a longer loading request then you can have more control over the UX by not using resolves. Otherwise resolves tend to make for simpler controller code.

Related

How get my controller to make GET request when tab is selected?

I am pretty new to angular and I am working on a data entry web page. The web page has three tabs. Vendor, Products and Types. I started working on the Types tab first. I'd be happy if I could just display the results of a GET request to my Rest API. My Rest API works:
# curl http://192.168.1.115:8080/type
[
{"_id":"56415e7703aba26400fcdb67","type":"Skiing","__v":0},
{"_id":"56417a8503aba26400fcdb68","type":"Bannana","__v":0},
{"_id":"56417a8d03aba26400fcdb69","type":"Carrot","__v":0},
{"_id":"56417a9603aba26400fcdb6a","type":"Beer","__v":0}
]
Here's the pertinent part of my html UPDATED I now have st-safe-src=all_typesbut still no joy ...
<div ng-controller="typeCtrl" class="tab-pane" id="types-v">
<p>The number {{3 + 4}}.</p>
<p>message is {{message}}</p>
<table st-table="displayedCollection" st-safe-src="all_types" class="table table-striped">
<tbody>
<tr ng-repeat="x in displayedCollection">
<td>{{x.type}}</td>
</tr>
</tbody>
</table>
</div> <!-- end Types Tab -->
... and here is my typeCtrl.js ...
app.controller("typeCtrl", function($scope,$http) {
$scope.type_to_look_for = "";
$scope.message = "this is the message. (from typeCtrl)";
$scope.itemsByPage=15;
$scope.all_types = function () {
$http.get("http://192.168.1.115:8080/type").then(function(response) {
console.log(response);
console.log(response.data);
return response.data;
});
}
});
... but when I click on the Types tab my data does not display. I looked developer console and I do not even see the GET request kickoff. And my web page looks like this ...
... what am I doing wrong?
There is nothing that calls all_types. Run http.get and assign the response to all_types
app.controller("typeCtrl", function($scope,$http) {
$scope.type_to_look_for = "";
$scope.message = "this is the message. (from typeCtrl)";
$scope.itemsByPage=15;
$http.get("http://192.168.1.115:8080/type").then(function(response) {
$scope.all_types = response;
});
}
});
My understanding is that you want a get request to be fired whenever you click on the Types tab, right? If so, just use ng-click to call your all_types function as follows:
<div ng-controller="typeCtrl" ng-click="all_types()" class="tab-pane" id="types-v" >
Also, you do not need to return response.data in your controller. Just assign the data to a scope object and use it in the template.
And finally, I would suggest wrapping all your ajax calls in factories and then inject those factories in your controllers.
Here is your code
<div ng-controller="typeCtrl" class="tab-pane" id="types-v">
<p>The number {{3 + 4}}.</p>
<p>message is {{message}}</p>
<table st-table="types" class="table table-striped"><!-- Do not need st-safe-src -->
<tbody>
<tr ng-repeat="x in types"><!-- Use the Collection name as types-->
<td>{{x.type}}</td>
</tr>
</tbody>
</table>
</div>
Controller Code
app.controller('typeCtrl', function($scope, $http) {
$scope.type_to_look_for = "";
$scope.message = "this is the message. (from typeCtrl)";
$scope.itemsByPage=15;
$http.get("http://192.168.1.115:8080/type").then(function(response) {
console.log(response.data);
$scope.types = response.data;
});
});
Here is working the plunker

Angular $watch just parent object instead of its multiple children

I have a div, listing properties of the object POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}. The object POI si owned by a Service. The directive's controller has the function getPoi() that gets the POI from the service, and returns it to the directive.
My current HTML is something like this:
<table ng-controller="Controller as ctrl">
<tr> <!-- address -->
<td>{{ctrl.getPoi().address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{ctrl.getPoi().phone}}</td>
</tr>
<tr> <!-- website -->
<td>
<a ng-href="{{ctrl.getPoi().website}}">
{{ctrl.getPoi().website}}
</a>
</td>
</tr>
</table>
The controller
.controller('Controller', function(CurrentPoiService)
{
this.getPoi = function()
{ return CurrentPoiService.POI; }
}
The service
.service('CurrentPoiService', function()
{
this.POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}
}
In this way I am adding 3 watchers. Is there a way to add just 1 watcher, since it's the same parent object? Here it is a JSFiddle
Thank you
[UPDATE 1]
This is the (still not working) JSFiddle using the solution proposed by #Charlie
[UPDATE 2]
This is the working JSFiddle
As Claies has mentioned in a comment, you should never call your data from
the view through a function this way.
In this scenario you can create a watch for the POI object with the objectEquality argument true to watch the properties of the object in a single $watch. Then find your elements inside the listener and change the value in the way you want.
$scope.$watch('POI', function(){
//Assume that $scope.propertyIndex is the current property to display
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.address);
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.phone);
//etc...
}, true);
You have a better control this way. But please keep in mind that this method is not suitable if POI is a complex object.
UPDATE:
Here is a working example of showing a random number every second using a watch and a factory. You should be able to learn from this and apply it to your project.
myApp.controller('myController', function($scope, dataSource) {
$scope.poi = {rand: 0};
$scope.$watch('poi', function() {
$('#rand').html($scope.poi.rand);
}, true);
dataSource.open($scope);
});
myApp.factory('dataSource', function($interval) {
return {
open: function(scope){
$interval(function() {
scope.poi.rand = Math.random();
}, 1000);
}
}
});
Try inside your controller :
$scope.POI = ctrl.getPoi();
HTML :
<tr> <!-- address -->
<td>{{POI.address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{POI.phone}}</td>
</tr>

Angular js redirect URL on same page?

I am building an node js app within which I have one page on angular js. On one of the pages, all the users are listed down(as hyperlinks) and I click on one of them to access the information about that user.
So, if the first page's URL(i.e. all the users) is /users, I want the second URL to look like /users/:id. So, in a way I want to mimic the REST API format.
The user's info has been found and once the user-id has been found, I m trying to use $location.path(index).
Initially I thought using something like ng-if would help me in rendering using the same page. So, on my .jade file, I have added this condition:
div(ng-show='loadAllUsers()')
$scope.loadAllUsers = function(){
console.log($scope.index == -1)
return $scope.index == -1;
};
$scope.index has been initialized to -1. So, I thought something like this would work when a new id value has been generated. But, the problem is it doesn't seem to reload the whole page or div.. I tried using window.location.href as well. That, even though updated the URL but even that breaks...
So, is there a way on doing this on the same jade file by any chance? If not , then how exactly should I go about this problem?
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, UserService) {
$scope.userDetails={};
//get the list of users from data base. here i am taking some dummy values.
$scope.users=[{
id:1,
name:"Rahul"
},
{
id:2,
name:"Ajay"
},
{
id:3,
name:"Mohit"
}];
// get user details
$scope.getUserDetails=function(userId){
UserService.get({id:userId},function(userDetails){
$scope.userDetails=userDetails;
});
};
});
// No need to change the page URL. Above function will
// call 'UserService' factory. UserService will communicate
// with REST API with URL pattern 'app/rest/users/:id'.
// ':id' will be replaced by the id passed by above function.
// like 'app/rest/users/2'
angular.module('myApp').factory('UserService',function ($resource) {
return $resource('app/rest/users/:id', {}, {
get':{method:'GET', isArray: false}
});
});
<div ng-app="myApp" ng-controller="myCtrl">
<div>
<table>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
<tr ng-repeat="user in users">
<td>{{user.id}}</td>
<td ng-click="getUserDetails(user.id)">{{user.name}}</td>
<tr>
</table>
</div>
<div>
<h3>User Details</h3>
id : userDetails.id <br/>
name : userDetails.name <br/>
// other details
</div>
</div>

ng-repeat ng-click when the click function has already been called earlier in the code

First off, I read the plethora of other questions and answers regarding ng-click, ng-repeat, and child and parent scopes (especially this excellent one.)
I think my problem is new.
I'm trying to call a function using ng-click within a table. The app allows for the sorting of Soundcloud likes. The problem is that when I try to call the ng click function using new data, it still tries to call the function using the old data. Let me explain better with the example:
Controller:
function TopListCtrl($scope, $http) {
$scope.sc_user = 'coolrivers';
$scope.getData = function(sc_user) {
var url = 'http://api.soundcloud.com/users/'+ $scope.sc_user +'/favorites.json?client_id=0553ef1b721e4783feda4f4fe6611d04&limit=200&linked_partitioning=1&callback=JSON_CALLBACK';
$http.jsonp(url).success(function(data) {
$scope.likes = data;
$scope.sortField = 'like.title';
$scope.reverse = true;
});
}
$scope.getData();
$scope.alertme = function(permalink) {
alert(permalink);
};
}
HTML
<div id="topelems">
<p id="nowsorting">Now sorting the Soundcloud likes of <input type=text ng-model="sc_user"><button class="btn-default" ng-click="getData(sc_user);">Sort</button></p>
<p id="search"><input ng-model="query" placeholder="Filter" type="text"/></p>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Song</th>
<th>Artist</th>
<th>Likes</th>
<th>Played</th>
<th>Tags</th>
</tr>
</thead>
<tr ng-repeat="like in likes.collection | filter:query | orderBy:sortField:reverse">
<td width="30%"><a href="{{ like.permalink_url }}">{{like.title}}</td>
(Trying to call it here) <td>{{like.user.username}}</td>
<td>{{like.favoritings_count}}</td>
<td>{{like.playback_count}}</td>
<td>{{like.tag_list}}</td>
</tr>
</table>
</div>
</body>
</html>
So I have that function getData. I use it to load the data in using the soundcloud api. I have a form at the top that uses getData to load a new user's Soundcloud data in. I also call the getData function in the controller so that there is an example on the page upon loading.
The problem is when I try to load a new user's data from a <td> I want to be able to click on the user to see and sort their likes.
How do I 'clear' the function or the global namespace (am I even refering to the right thing)? How can I reuse the getData function with a new variable?
Working Jsfiddle for this
In your getData function you have this line:
var url = 'http://api.soundcloud.com/users/'+ $scope.sc_user +'/favorites.json...
but you are passing in the variable sc_user to your getData function and should be using it like this (no $scope):
var url = 'http://api.soundcloud.com/users/'+ sc_user +'/favorites.json...
That being said... your initial data load fails because you are calling:
$scope.getData();
and not:
$scope.getData($scope.sc_user);

Refresh data on click outside controller in angular

I am trying to grasp the idea behind angular and ran into my first obstacle involving accessing data from outside the scope (the app?)
Here is a very simple example of what I'm trying to do:
HTML:
<div class=logo>
<a href='/refresh'>Refresh</a>
</div>
<div ng-app ng-controller="threadslist">
<div class='thread_list_header'>
<table class='thread_list_table'>
<tr class='table_header'>
<!--1--><td class='table_c1'></td>
<!--2--><td class='table_c2'>{{name}}</td>
<!--3--><td class='table_c3'>Cliq</td>
<!--4--><td class='table_c4'>Last Poster</td>
<!--5--><td class='table_c5'><a class="clickme">Refresh</a></td>
</tr></table>
</div>
<table class='thread_list_table' >
<tr class="thread_list_row" ng-repeat="user in users">
<!--1--><td class='table_options table_c1'></td>
<!--2--><td class='table_subject table_c2' title="">{{user.subject}}</td>
<!--3--><td class='table_cliq table_c3'></td>
<!--4--><td class='table_last table_c4'><span class="thread_username"><a href=#>{{user.username}}</a></span></td>
<!--5--><td class='table_reply table_c5'><abbr class="thread_timestamp timeago" title=""></abbr></td>
</tr>
</table>
</div>
JS:
function threadslist($scope, $http) {
$scope.name = 'Ricky';
// Initialising the variable.
$scope.users = [];
$http({
url: '/cliqforum/ajax/ang_thread',
method: "POST",
}).success(function (data) {
console.log(data);
$scope.users = data;
});
// Getting the list of users through ajax call.
$('.table_header').on('click', '.clickme', function(){
$http.get('/cliqforum/ajax/ang_thread').success(function(data){
$scope.users = data;
});
});
}
This is the part I can't figure out. My logo is supposed to clear whatever filter is on the current 'user' data. However, it sits outside the scope (and I imagine I shouldn't expand the scope to be the entire page?)
I have read something about scope.$spply but can't quite figure out what I'm supposed to do:
$('.logo').on('click', 'a', function() {
scope.$apply(function () {
$scope.users = data;
});
});
It's not quite necessary that I do it THIS way...I would just like to do what is correct!
Thanks!
and I imagine I shouldn't expand the scope to be the entire page?
Why not? That's definitely the way to do it. Just include the logo into the scope and you can then access it from your application, and use ng-click to add a click handler.
In fact, you should avoid using jQuery click handlers within your application. You could transform your JavaScript like so:
$scope.tableHeaderClick = function() {
$http.get('/cliqforum/ajax/ang_thread').success(function(data){
$scope.users = data;
});
});
Then update the HTML like so:
<tr class='table_header' ng-click="tableHeaderClick()">
it is an angular anti-pattern to include DOM elements in controller. you want to use the ng-click directive to respond to click events
see this plnkr:
http://plnkr.co/edit/KRyvifRYm5SMpbVvWNfc?p=preview

Resources