AngularJS update view with service/model changes using $q promises - angularjs

I'm trying to load data from a service and update the view using $q, but it's not working. It works if I put the http call inside the controller, but I'd prefer it be part of the service.
Any help? Also, is there a better way to do this instead of promises?
Demo and code below.
---------- Fiddle Demo Link ----------
View
<div ng-init="getData()">
<div ng-repeat="item in list">{{item.name}}</div>
</div>
Controller
.controller('ctrl', ['$scope', 'dataservice', '$q', function ($scope, dataservice, $q) {
$scope.list = dataservice.datalist;
var loadData = function () {
dataservice.fakeHttpGetData();
};
var setDataToScope = function () {
$scope.list = dataservice.datalist;
};
$scope.getData = function () {
var defer = $q.defer();
defer.promise.then(setDataToScope());
defer.resolve(loadData());
};
}])
Service
.factory('dataservice', ['$timeout', function ($timeout) {
// view displays this list at load
this.datalist = [{'name': 'alpha'}, {'name': 'bravo'}];
this.fakeHttpGetData = function () {
$timeout(function () {
// view should display this list after 2 seconds
this.datalist = [{'name': 'charlie'}, {'name': 'delta'}, {'name': 'echo'}];
},
2000);
};
return this;
}]);

No need for ngInit or $q. This is how you should do it.
You should also not expose dataservice.list to the controller. That should be private to dataservice, which will contain most of the logic to determine whether to send the controller the existing list or update the list and then send it.
angular.module('app', [])
.controller('ctrl', ['$scope', 'dataservice', function ($scope, dataservice) {
loadData();
function loadData() {
dataservice.fakeHttpGetData().then(function (result) {
$scope.list = result;
});
}
}])
.factory('dataservice', ['$timeout', function ($timeout) {
var datalist = [
{
'name': 'alpha'
},
{
'name': 'bravo'
}
];
this.fakeHttpGetData = function () {
return $timeout(function () {
// Logic here to determine what the list should be (what combination of new data and the existing list).
datalist = [
{
'name': 'charlie'
},
{
'name': 'delta'
},
{
'name': 'echo'
}
];
return datalist;
},
2000);
};
return this;
}]);

Firstly, don't use ng-init in this way. As per the docs:
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
Secondly, promises are the perfect thing to use in this case, but you don't need to touch $q, as $http calls return promises for you.
To do this properly, simply return the $http result from the service:
this.getDataFromService = function() {
return $http(/* http call info */);
};
Then, inside you controller:
dataservice.getDataFromService().then(function(result){
$scope.list = result.data;
});
Also here is the updated fiddle: http://jsfiddle.net/RgwLR/
Bear in mind that $q.when() simply wraps the given value in a promise (mimicking the response from $http in your example).

Related

AngularJS Ctrl As Syntax with Services

I'm migrating my Angular 1.x code to use the newer, more preferred syntax of avoiding using $scope and using the controller as syntax. I'm having an issue with getting data from a service though.
Here's my example:
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$http', '$location', 'userService',
function($http, $location, userService) {
this.name = 'John Doe';
// this.userData = {
// }
this.userData = userService.async().then(function(d) {
return d;
});
console.log(this.userData);
}
]);
myApp.service('userService', function($http) {
var userService = {
async: function() {
// $http returns a promise, which has a then function, which also returns a promise
var promise = $http.get('https://jsonplaceholder.typicode.com/users').then(function(response) {
// The then function here is an opportunity to modify the response
// console.log(response);
// The return value gets picked up by the then in the controller.
return response.data;
});
// Return the promise to the controller
return promise;
}
};
return userService;
});
<body ng-app="myApp">
<div id="ctrl-as-exmpl" ng-controller="MainCtrl as mainctrl">
<h1>Phone Numbers for {{mainctrl.name}}</h1>
<button ng-click="">newest</button>
<p>{{mainctrl.userData}}</p>
<ul>
<li ng-repeat="users in mainctrl.userData">{{users.phone}} -- {{users.email}}</li>
</ul>
</div>
</body>
This issue I'm having is that the data returned from the userService.async is an object that I can't seem to drill down thru to get the data from, as the data is a child of $$state, which can't seem to be used in the view.
If this isn't the proper way to use services in the Controller As/$scope-less syntax, what is the proper way?
Live example here: http://plnkr.co/edit/ejHReaAIvVzlSExL5Elw?p=preview
You have a missed an important part of promises - when you return a value from a promise, you return a new promise that will resolve to this value.
The current way to set the data returned from a promise to a local variable in your controller is this:
myApp.controller('MainCtrl', ['$http', '$location', 'userService',
function($http, $location, userService) {
this.name = 'John Doe';
userService.async().then(function(d) {
this.userData = d;
});
}
]);
But now you are in a catch because of the scope of this, so it is common to use a "placeholder" variable for the controller this scope.
myApp.controller('MainCtrl', ['$http', '$location', 'userService',
function($http, $location, userService) {
var $ctrl = this; // Use $ctrl instead of this when you want to address the controller instance
$ctrl.name = 'John Doe';
userService.async().then(function(d) {
$ctrl.userData = d;
});
}
]);
this.userData = userService.async().then(function(d) {
return d;
});
In this bit of code this.userData is actually being defined as a promise. If you want the data that is being returned, you need to use the d parameter from your .then function like so:
userService.async().then(function(d){
this.userData = d;
//a console.log() here would determine if you are getting the data you want.
});
Edit
I just noticed in your Service that you are already using .data on your response object.
Honestly, my best advice to you would be to just return the promise from the http.get() call inside of your service like this:
myApp.service('userService', function($http) {
var userService = {
async: function() {
return $http.get('https://jsonplaceholder.typicode.com/users');
}
};
return userService;
});
And then you could use something like this to capture and utilize the response data:
userService.async().then(function(response){
this.userData = response.data;
);
This may be a bit cleaner of a solution

View not updating when using $controller service in angular

I have two controllers. The view is tied to the firstCtrl. I'm trying to run the function in the second one so the view is updated. Function runs but view not updated. Any ideas?
<div>{{people.length}}</div>
angular.module('myApp').controller('firstCtrl', function($scope, $http) {
var self = this;
$scope.people = [];
function getData() {
$http.get('/people').then(function(res) {
$scope.people = res.data;
});
}
getData();
$scope.getData = getData;
self.getData = function(){
$scope.getData();
};
return self;
});
angular.module('myApp').controller('secondCtrl', function( $controller, $scope, $http) {
var firstCtrl= $controller('firstCtrl', { $scope: $scope.$new() });
firstCtrl.getData(); //This runs but view is not updated above.
});
I think your code has some problem with the $scope. So instead of pass data directly in the firstCtrl. I pass a callback to getData function and assign data to $scope.people in the callback.
Here is the working Plunker:
> http://plnkr.co/edit/QznxFL

How to get the length of an array without ngRepeat

I'm trying to count the items in an array without using ng-repeat (I don't really need it, i just want to print out the sum).
This is what I've done so far: http://codepen.io/nickimola/pen/zqwOMN?editors=1010
HTML:
<body ng-app="myApp" ng-controller="myCtrl">
<h1>Test</h1>
<div ng-cloak>{{totalErrors()}}</div>
</body>
Javascript:
angular.module('myApp', []).controller('myCtrl', ['$scope', '$timeout', function($scope) {
$scope.tiles= {
'data':[
{'issues':[
{'name':'Test','errors':[
{'id':1,'level':2},
{'id':3,'level':1},
{'id':5,'level':1},
{'id':5,'level':1}
]},
{'name':'Test','errors':[
{'id':1,'level':2,'details':{}},
{'id':5,'level':1}
]}
]}
]}
$scope.totalErrors = function() {
if ($scope.tiles){
var topLevel = $scope.tiles.data
console.log (topLevel);
return topLevel[0].issues.map(function(o) {
return o.errors.length
})
.reduce(function (prev, curr){
return prev + curr
})
}
}
}]);
This code works on codepen, but on my app I get this error:
Cannot read property '0' of undefined
and if I debug it, topLevel is undefined when the functions is called.
I think it is related to the loading of the data, as on my app I have a service that looks like this:
angular.module('services', ['ngResource']).factory('tilesData', [
'$http', '$stateParams', function($http, $stateParams) {
var tilesData;
tilesData = function(myData) {
if (myData) {
return this.setData(myData);
}
};
tilesData.prototype = {
setData: function(myData) {
return angular.extend(this, myData);
},
load: function(id) {
var scope;
scope = this;
return $http.get('default-system.json').success(function(myData) {
return scope.setData(myData.data);
}).error(function(err) {
return console.error(err);
});
}
};
return tilesData;
}
]);
and I load the data like this in my controller:
angular.module('myController', ['services', 'ionic']).controller('uiSettings', [
'$scope', '$ionicPopup', '$ionicModal', 'tilesData', function($scope, $ionicPopup, $ionicModal, tilesData) {
$scope.tiles = new tilesData();
$scope.tiles.load();
$scope.totalErrors = function() {
debugger;
var topLevel;
topLevel = $scope.tiles.data;
console.log(topLevel);
return topLevel[0].issues.map(function(o) {
return o.errors.length;
}).reduce(function(prev, curr) {
return prev + curr;
});
};
}
]);
but I don't know what to do to solve this issue. Any help will be really appreciated. Thanks a lot
The $http.get() method is asynchronous, so you can handle this in your controller with a callback or a promise. I have an example using a promise here.
I've made an example pen that passes back the sample data you use above asynchronously.This mocks the $http.get call you make.
I have handled the async call in the controller in a slightly different way to what you had done, but this way it works with the .then() pattern that promises use. This should give you an example of how you can handle the async code in your controller.
Note as well that my service is in the same module as my controller. This shouldn't matter and the way you've done it, injecting your factory module into your main module is fine.
angular.module('myApp', [])
//Define your controller
.controller('myCtrl', ['$scope','myFactory', function($scope,myFactory) {
//call async function from service, with .then pattern:
myFactory.myFunction().then(
function(data){
// Call function that does your map reduce
$scope.totalErrors = setTotalErrors();
},
function(error){
console.log(error);
});
function setTotalErrors () {
if ($scope.tiles){
var topLevel = $scope.tiles.data
console.log (topLevel);
return topLevel[0].issues.map(function(o) {
return o.errors.length
})
.reduce(function (prev, curr){
return prev + curr
});
}
}
}])
.factory('myFactory', ['$timeout','$q',function($timeout,$q){
return {
myFunction : myFunction
};
function myFunction(){
//Create deferred object with $q.
var deferred = $q.defer();
//mock an async call with a timeout
$timeout(function(){
//resolve the promise with the sample data
deferred.resolve(
{'data':[
{'issues':[
{'name':'Test','errors':[
{'id':1,'level':2},
{'id':3,'level':1},
{'id':5,'level':1},
{'id':5,'level':1}
]},
{'name':'Test','errors':[
{'id':1,'level':2,'details':{}},
{'id':5,'level':1}
]}
]}
]})
},200);
//return promise object.
return deferred.promise;
}
}]);
Have a look : Link to codepen
Also, have a read of the $q documentation: documentation

Angularjs data binding between controller and service

I'm not able to get the data binding between controller and service working.
I have a controller and a factory which makes an HTTP call. I would like to be able to call the factory method from other services and see the controller attributes get updated. I tried different options but none of them seem to be working. Any ideas would be greatly appreciated.
Please see the code here:
http://plnkr.co/edit/d3c16z?p=preview
Here is the javascript code.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
app.controller('EventDetailCtrl', ['$http', 'EventDetailSvc', '$scope',
function ($http, EventDetailSvc, $scope) {
this.event = EventDetailSvc.event;
EventDetailSvc.getEvent();
console.log(self.event);
$scope.$watch(angular.bind(this, function () {
console.log('under watch');
console.log(this.event);
return this.event;
}), function (newVal, oldVal) {
console.log('under watch2');
console.log(newVal);
this.event = newVal;
});
}])
.factory('EventDetailSvc', ['$http', function ($http) {
var event = {};
var factory = {};
factory.getEvent = function() {
$http.get('http://ip.jsontest.com')
.then(function (response) {
this.event = response.data;
console.log('http successful');
console.log(this.event);
return this.event;
}, function (errResponse) {
console.error("error while retrieving event");
})
};
factory.event = event;
return factory;
}]);
It seems to me that you have nested the event object inside of a factory object. You should be returning event directly instead wrapping it with factory. As it stands now you would need to call EventDetailSvc.factory.event to access your object.

AngularJS, ngResource, Service and multiple controllers. How to keep data synchronized?

I searched a lot about this, and although I found some similar problems, they are not exactly what I want.
Imagine this kind of code:
angular
.module('foobar', ['ngResource'])
.service('Brand', ['$resource', function($resource){
return $resource('/api/v1/brands/:id', { id: '#id' });
}])
.service('Product', ['$resource', function($resource){
return $resource('/api/v1/products/:id', { id: '#id' });
}])
.controller('ProductController', ['$scope', 'Brand', function($scope, Brand){
$scope.brands = Brand.query();
}])
.controller('BrandController', ['$scope', 'Brand', function($scope, Brand){
this.create = function() {
Brand.save({label: $scope.label });
};
}])
For the moment, I'm using $broadcast and $on
In brand controller:
Brand.save({label: $scope.label }, function(brand){
$rootScope.$broadcast('brand-created', brand);
});
In product controller:
$scope.$on('brand-created', function(brand){
$scope.brands.push(brand);
});
It works, but I don't like this way of sync'ing datas.
Imagine you have 10 controllers which must be sync'ed, you should write the $scope.$on part in each. And do the save for each services...
Is there a better way to keep collection sync'ed wherever they are used?
Yes - using a shared service. I've used http in my example, but you could fairly easily substitute it with $resource. The main point over here is to keep an in memory copy of the list of the brands. This copy is referred by the productscontroller and whenever it is updated it will automatically reflect. You simply need to ensure that you update it correctly after making the $http put / resource put call.
http://plnkr.co/edit/k6rwS0?p=preview
angular.module('foobar', [])
.service('Brand', ['$http', '$q',
function($http, $q) {
var brandsList;
return {
getBrands: function() {
var deferred = $q.defer();
//if we have not fetched any brands yet, then we'll get them from the api
if (!brandsList) {
$http.get('brands.json').then(function(response) {
brandsList = response.data;
console.log('brands:' + brandsList);
deferred.resolve(brandsList);
});
} else {
deferred.resolve(brandsList);
}
return deferred.promise;
},
save: function(newBrand) {
// $http.put('brands.json', newBrand).then(function(){
// //update the list here on success
// brandsList.push(newBrand)
// })
brandsList.push({
name: newBrand
});
}
};
}])
.controller('productsController', ['$scope', 'Brand',
function($scope, Brand) {
Brand.getBrands().then(function(brands) {
$scope.brands = brands;
})
}
])
.controller('brandsController', ['$scope', 'Brand',
function($scope, Brand) {
$scope.create = function() {
Brand.save($scope.label);
};
}
])

Resources