Restangular response and ng-repeat - angularjs

I have recently started to learn angularjs using restangular to talk to my restfull API (sails). The problem I have stumbled upon is that the ng-repeat does not update after I change the list in the scope.
Controller:
app.controller('UsersCtrl', ['UsersSvc', '$scope', function(UsersSvc, s) {
UsersSvc.getList().then(function (new_users) {
s.users = new_users;
})
s.destroy = function (user) {
user.remove().then(function () {
s.users = _.without(s.users, user);
});
}
}]);
Service:
app.factory('UsersSvc', function(Restangular) {
return Restangular.all('users');
});
Template:
<div ng-controller="UsersCtrl">
...
<tr ng-repeat"user in users">
<td>{{user.firstName}}</td>
<td>{{user.lastName}} </td>
<td>{{user.emailAddress}}</td>
<td>{{user.age}}</td>
</tr>
...
</div>
When I inspect the scope the array of restangular objects is correctly assigned to the scope of the users controller but the template refuses to update.
Thanks in advance

AngularJS (and javascript) care about references vs. overwrites. So to be safe I always set my scope variables initially, and then update using angular.copy() or Restangular.copy() (if it's a Restangular object being set).
Below is how I'd refactor your controller to ensure bindings + digest cycles stay connected.
(Please note I renamed s to the "traditional" $scope for easier reading for everyone else)
app.controller('UsersCtrl', ['$scope', 'UsersSvc', 'Restangular', function($scope, UsersSvc, Restangular) {
// we're expecting a list, so default as array
$scope.users = [];
UsersSvc.getList().then(function (new_users) {
// In normal $resource/ng projects use: angular.copy(src, dst) but
// Restangular has an issue when using angular.copy():
// https://github.com/mgonto/restangular/issues/55
// so use their version of copy():
Restangular.copy(new_users, $scope.users);
});
$scope.destroy = function (user) {
user.remove().then(function () {
$scope.users = _.without($scope.users, user);
});
}
}]);

Related

Return service $http.get response to controller without using $scope

I've been searching around for hours, but I couldn't find a solution. I'm trying to retrieve data from a separate json-file in my AngularJS application. Using the $http I do get the data I want.
However, I want to get specific data from that file at in multiple places. In the service I want to define some functions that controllers can call to retrieve the data (that the service got using $http).
Now the problem is that when I return the all the data to the controller directly, or try to use it later in the service, the assigned variables are undefined.
I try to use controller-as syntax, so I do not want to use $scope. However every solution I've found suggests using $scope. This code now logs
f {$$state: {...}}.
Code of the service:
theApp.service('SettingsService', function($http) {
this.dataVar = $http.get('../settings.json')
.then(function(response) {
return response;
});
});
Code of the controller:
theApp.controller('SomeController', ['SettingsService', function(SettingsService) {
console.log(SettingsService.dataVar);
}]);
UPDATE:
https://jsfiddle.net/md954y0a/
what about calling your service at start-up module then passing to submodules through a service that loads the same instance
html:
<div ng-app='myApp' ng-controller="myctrl">
{{parent}}
<div ng-app='myApp1' ng-controller="myctrl1">
{{myApp1data}}
</div>
</div>
js:
angular.module('myApp', ['myApp1']).controller('myctrl', ['$scope', 'API', function($scope, API) {
$scope.parent = API.getData();
}]).service('API', function($q) {
var object = null;
this.getData = function() {
object = {
obj1: "DATA1",
obj2: "DATA2"
};
return object;
}
});
angular.module('myApp1', []).controller('myctrl1', ['$scope', 'API', function($scope, API) {
$scope.myApp1data = API.getData().obj1
}]);

service not working between two controllers getting undefined

I am showing employee record using AngularJS. I am using two views to show data, I am using two views emp-box.htm and its respective controller(empController) and in this controller employeeBoxController I am fetching data from service, I want the result which is obtained in employeeBoxController to be used in empController and show in view (emp-list.htm), I created a service eService
app.service('dataService',function() {
var s = {};
this.setData = function(data,key) {
s[key]=data;
},
this.getData = function(key) {
return s[key];
}
this.hello = function() {
return 'hello';
}
})
for fetching result and setting data in employeeBoxController and getting in empController but when I fetch the data using console.log(dataService.getData('result')); in empController i get undefined
The employeeBoxController is
app.controller("employeeBoxController", ['$scope', 'employeeService',
'dataService', function($scope, employeeService, dataService) {
$scope.getEmployeeDetails = function(eid) {
$scope.isLoading = false;
employeeService.getDetails($scope.eid).then(function(result) {
dataService.setData(result, 'result');
$scope.isLoading = true;
console.log(dataService.getData('result'));
})
}
}])
The empController is :-
app.controller("empController", ['$scope', 'employeeService', 'dataService',
function($scope, employeeService, dataService) {
$scope.result = dataService.getData('result');
//console.log(dataService.hello());
console.log(dataService.getData('result'));
console.log(dataService.hello());
}
])
The service class employeeService is :-
app.config(["employeeServiceProvider",function(employeeServiceProvider){
employeeServiceProvider.config('http://localhost:8080/pos');
}]);
app.provider("employeeService",function(){
var myurl='';
this.config=function(eurl){
myurl=eurl;
}
this.$get=['$http','$log',function($http,$log){
var employeeobj={};
employeeobj.getDetails=function(eid){
return $http.get(myurl+'/getEmployees/'+eid);
}
return employeeobj;
}];
});
emp-box.htm is:-
<div>
Enter the id: <input type="text" ng-model="eid"/>
<button ng-click="getEmployeeDetails()">show</button>
</div>
emp-list.htm is:-
<div class="panel panel-primary">
<div class="panel-body" style="text-align:center; margin:0 auto">
<h3>Employee Data</h3>
</div>
</div>
<div class="panel panel-primary">
<div class="panel-body">
<!-- <div ng-show="!isLoading" style="color:red">
<span class="glyphicon glyphicon-time"></span>Loading...
</div>-->
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>empno</th>
<th>salary</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="oemp in result.data">
<td>{{oemp.eid}}</td>
<td>{{oemp.name}}</td>
<td>{{oemp.empno}}</td>
<td>{{oemp.sal}}</td>
</tr>
</tbody>
</table>
</div>
</div>
From what I understand you're trying to cache your API results in another service, so you don't have to call the API again in another controller. Also, empController is executed first, and when you're doing dataService.getData('result') the API response of setting it isn't received yet which in turn is called from another service. I would suggest you combine these 2 services, so that instead of caching the exact values in service, you can cache the API call itself, and in case where cache doesn't contain your data, make an API call and cache it.
Here's something I'd make, say CacheAPIService.
app.factory('CacheAPIService', ['$http', function($http) {
var cache = {};
return {
get: function(api) {
if(angular.isUndefined(cache[api])) {
cache[api] = $http.get(api); //storing in cache while making API call
}
return cache[api]; //Return from cache
},
clear: function(api) {
delete cache[api];
}
}
}]);
So, whenever you need to make an cache an API call, use this service in addition to making API call it'll also cache it. And, if it is already cached, no new API call is made. Benefits is you'll never run into cases where it return's undefined, as you are returning promise.
In your 1st controller, updated code becomes:
app.controller("employeeBoxController", ['$scope', 'CacheAPIService', function($scope, CacheAPIService) {
$scope.getEmployeeDetails = function(eid) {
$scope.isLoading = true;
var endpoint = 'api/endpoint/'+$scope.eid; //Replace with your API endpoint
CacheAPIService.get(endpoint).then(function(result) {
$scope.isLoading = false;
console.log(dataService.getData('result'));
})
}
}]);
Here, 1st API call is made and is cached. Take a look at your other controller:
app.controller("empController", ['$scope', 'CacheAPIService', function($scope, CacheAPIService) {
CacheAPIService.get(endpoint).then(function(data) {
var endpoint = 'api/endpoint/'+$scope.eid; //your API endpoint
console.log('data =', data);
});
}]);
Here, you still use the same service, but it'll be cached, and if not cached, it'll make API call and cache that promise. Here I've directly used API endpoint as key for storing in cache. This way you don't have to provide unique keys every time, as endpoint itself is unique.
Note that in cases where you want to delete cached data, when making POST or PUT call, you can call CacheAPIService.clear(apiEndpoint) to clear details from cache.
$http.get will return a promise.
To correctly set data to dataService:
employeeService.getDetails($scope.eid).then(function(result) {
dataService.setData(result.data, 'result'); // note .data
$scope.isLoading = true;
console.log(dataService.getData('result'));
})
Storing the promise in your "cache" service is not the best option.
I expect getEmployeeDetails() function is going to route to empController, so if you still store $promise in your service as you stated, you can do.
dataService.getData('result').then(function(result){
$scope.result = result.data;
});
When $scope.result is set to $scope, ng-repeat will start iterating over.
With the first change suggested, you just don't need to touch empController but just the ng-repeat directive emp-list.html with:
<tr ng-repeat="oemp in result">
Refactor the http call with factory recipe and $resource.
https://docs.angularjs.org/api/ngResource/service/$resource
That is easier and quicker than writing providers.

$rootScope is not updated in SPA

My first controller:
angular.module('application')
.controller('FirstController',['$rootScope',function($rootScope) {
var data=[0,1,2];
$rootScope.items=data;
}]);
My second controller:
angular.module('application')
.controller('SecondController',['$rootScope',function($rootScope) {
$rootScope.items[0]=3;
console.log($rootScope.items); // [3,1,2]
}]);
When the second controller is running, its corresponding view is changed; however not the same happens when going back to the corresponding view of the first controller (both views are bound to $rootScope.items). Why that happens? I am using ui-router and FirstController has to do with the main page of the SPA and SecondController with another page. Moreover, by keeping track of $rootScope.items with:
<pre>
{{$root.items | json}}
</pre>
in both templates the second one is renewed to [3,1,2] and the first one remains [0,1,2].
Passing the same $scope between the two controllers isn't an ideal way of maintaining a single data model, and what you need to do is to establish a service module (or a factory) to manage the data for you, so that both controllers can talk to the factor for your data.
This is how you set up the factory
app.factory("Data",
function () {
var storage = [0,1,2]; // where your data is
return {
get: function () {
return storage;
},
set: function (toSet) {
storage = toSet;
return storage;
}
};
});
to let the controllers know where the data is, use
app.controller ("FirstController",
function ($scope, Data)
{
console.log(Data); // [0,1,2]
Data.set( [3,1,2]); // demoing change
}
same is for the second controller
app.controller ("FirstController",
function ($scope, Data)
{
console.log(Data); // [3,1,2]
}

Angularjs remove items from scope that are in another scope

The following is a mock up of what I have using data from the angular site. The goal is to remove any items in scope2 (newdevices) that already exist in scope1 (devices). I have a working model but do not feel it is the best method.
I have a controller that draws data from two different sources. For simplicity i have made the first scope static, whereas the second will take data via httpget from the angular site and this is initiated from a button click. (My prod code needs to use a button so i can inject variables into the call)
app.controller('customersCtrl', function($scope, $http) {
//Example static data for scope 1
$scope.devices = [
{"Name":"Around the Horn","City":"London","Country":"UK"},
{"Name":"B's Beverages","City":"London","Country":"UK"},
{"Name":"Chop-suey Chinese","City":"Bern","Country":"Switzerland"}
];
//scope 2 data from angular example site that is initiated from a button
$scope.loaddata = function() {
$http.get("http://www.w3schools.com/angular/customers_mysql.php")
.then(function (response) {
$scope.newdevices = response.data.records;
});
}
});
I then have a filter that compares the scopes:
app.filter('matcher', function() {
return function(newdevices, devices) {
var array2Ids = []
angular.forEach(devices, function(value, index) {
array2Ids.push(value.Name);
})
return newdevices.filter(function(val) {
return array2Ids.indexOf(val.Name) === -1;
})
}
});
Lastly, I apply the filter to my ng-repeat call:
<div ng-app="myApp" ng-controller="customersCtrl">
<button ng-click="loaddata()">load me</button>
<table>
<tr ng-repeat="x in newdevices | matcher: devices">
<td width="300px">{{ x.Name }}</td>
<td width="150px">{{ x.City }}</td>
<td width="100px">{{ x.Country }}</td>
</tr>
</table>
</div>
As mentioned, this currently works, but as I am already calling the second scope httpget from a function, is there a way I can integrate the filter into the loaddata function, so it happens all at once and can eliminate the need to filter on the ng-repeat stage?
I am still relatively new to this and have not yet been able to accomplish it.
you don't need an angular "filter". Just filter the response data before it gets assigned to $scope.newdevices. below code has been tested, but you get the idea.
$scope.loaddata = function() {
$http.get("http://www.w3schools.com/angular/customers_mysql.php")
.then(function (response) {
//do things here, i.e.
var array2Ids = [];
angular.forEach(devices, function(value, index) {
array2Ids.push(value.Name);
});
$scope.newdevices = response.data.records.filter(function(val) {
return array2Ids.indexOf(val.Name) === -1;
});
});
}
Controllers and services can retrieve filters using the $filter service.
var matcherFn = $filter('matcher');
var result = marcherFn(newdevices, devices);
AngularJS filters can be used both in templates and JavaScript.
The example in the Docs:
angular.module('filterExample', [])
.controller('MainCtrl', function($scope, $filter) {
$scope.originalText = 'hello';
$scope.filteredText = $filter('uppercase')($scope.originalText);
});
For more information, see AngularJS $filter Service API Reference.

How would I create a factory that only queries the database once, but can be used by many controllers?

I have a 'messages' factory that will query my database for a list of messages.
I'm using the list of messages in two different places. Once to add a message count indicator, and then once to show a list of messages. Since I'm injecting the service into two different controllers, it seems like it's creating two instances of my factory, and hitting the database twice for the list.
How would I set things up to only ask for the list once, and use the list for both display and count purposes in both controllers?
My factory looks like this:
myApp.factory('messagesService', [
'$rootScope',
function($rootScope) {
var messages = [];
function query() {
// Would actually hit the database asyncronously
messages = ['one', 'two', 'three', 'four'];
console.log('query');
$rootScope.$emit('messages.update');
}
function all() {
return messages;
}
return {
query: query,
all: all
}
}
]);
My controllers are using blocks like this to watch for changes:
$rootScope.$on('messages.update', function() {
$scope.messagesCount = messagesService.all().length;
});
But it means i need a messagesService.query(); in each controller for things to be reliable.
So here are a few jsFiddle examples of it as I have things now:
Doesn't work (only updates the header): http://jsfiddle.net/TSLfc/1/
Works but would break if I didn't load the dashboard controller:
http://jsfiddle.net/TSLfc/2/
Works every time, but queries the server twice:
http://jsfiddle.net/TSLfc/3/
Is there a better way to organize my code? Should I build out the messages factory into it's own full module?
Here (Plunkr) is how I would do it:
I have gone back and modified my previous answer, updating with what we discussed in the comments below as well as using promises instead of the timeout as an asynchronous simulation I was showing before (see revision history for reference).
I also removed every variable/function that didn't need to be returned to the controller from the service object, if it doesn't need to be accessed via the controller than it doesn't need to be included on the returned object.
var myApp = angular.module('myApp', []);
myApp.factory('messagesService', [
'$q',
'$rootScope',
'$http',
function ($q, $rootScope, $http) {
var mService = {};
mService.messages = [];
var queryInit = false;
// We don't need to access this function in the controller
// So I am not going to attach to the returned object
var getMessages = function () {
// Stops each controller from getting messages when loaded
if (!queryInit) {
queryInit = true;
// Using the $q promise library we use 'then()' to handle
// What happens after the async call is returned
// The first function parameter is the success/resolve callback
// The second function parameter is the error/reject callback
mService.query().then(function (successResults) {
// Tell all of the controllers that the data has changed
$rootScope.$broadcast('messages.update');
}, function (errorResults) {
console.error(errorResults);
});
}
};
// Used to force an update from the controller if needed.
mService.query = function () {
var deferred = $q.defer();
$http.get('path/to/file.php')
.success(function (data, status, headers, config) {
// assign the returned values appropriately
mService.messages = data;
// this callback will be called asynchronously
// when the response is available
deferred.resolve(data);
})
.error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
deferred.reject(data);
});
return deferred.promise;
};
mService.getCount = function () {
return mService.messages.length;
};
mService.all = function () {
return mService.messages;
};
// Initialize the messages
// so we don't need to get the messages in each controller
getMessages();
return mService;
}]);
In your html, on your first controller setup an init function (ng-init="init()") that instantiates the factory:
<div ng-app="myApp">
<div ng-controller="HeaderCtrl" class="header" ng-init="init()">
Messages Count: {{ messageCount }}
</div>
<div ng-controller="DashboardCtrl" class="dashboard">
<ul ng-repeat="message in messages">
<li>{{ message }}</li>
</ul>
<button ng-click="getMessages()">Check for new messages.</button>
</div>
</div>
And in your controllers you just have the $rootScope.$on('messages.update' fn) and you can call manually by calling the services query() function which returns the promise:
myApp.controller('HeaderCtrl', [
'$scope',
'$rootScope',
'messagesService',
function ($scope, $rootScope, messagesService) {
$rootScope.$on('messages.update', function () {
$scope.messageCount = messagesService.getCount();
});
// Manual call, if needed
$scope.getMessageCount = function () {
messagesService.query().then(function (successCallback) {
$scope.messageCount = messagesService.getCount();
});
};
}]);
myApp.controller('DashboardCtrl', [
'$scope',
'$rootScope',
'messagesService',
function ($scope, $rootScope, messagesService) {
$rootScope.$on('messages.update', function () {
$scope.messages = messagesService.all();
});
// Manual call, if needed
$scope.getMessages = function () {
messagesService.query().then(function (successCallback) {
$scope.messages = messagesService.all();
$rootScope.$broadcast('messages.update');
});
}
}]);
You can set cache:true on a $http request. There are numerous ways to data bind within angular without needing to use the $broadcast approach you are using. Also note, $broadcast from a scope will be receievd by all descendent scopes, so no need to inject $rootSCope just for that purpose, can listen on $scope.
Here's one approach that controllers use promise of $http to retrieve data. I used a button click to retrive data for DashControl so can see that request does get cached
myApp.factory('messagesService',function($http) {
return{
query:function query(callback) {
/* return promise of the request*/
return $http.get('messages.json',{ cache:true}).then(function(res){
/* resolve what data to return, can set additional properties of the service here if desired*/
return res.data
}).then(callback);
}
}
});
myApp.controller('HeaderCtrl',function($scope, messagesService) {
messagesService.query(function(messages){
$scope.messagesCount = messages.length;
});
});
myApp.controller('DashboardCtrl', function($scope, messagesService) {
/* use button click to load same data, note in console no http request made*/
$scope.getMessages=function(){
messagesService.query(function(messages){
$scope.messages = messages;
})
}
});
Essentially in this scenario, whatever controller calls the factory service first will generate the data cache
DEMO
I would do it like that:
myApp.factory('messagesService', function() {
var expose = {
messages: []
};
expose.query = function () {
// Would actually hit the database asyncronously
expose.messages = ['one', 'two', 'three', 'four'];
console.log('query');
};
// Initialization
expose.query();
return expose;
}
);
And in your controllers:
$scope.messagesCount = messagesService.messages.length;
Model with broadcasting and pre-hitting database looks heavy for me.
So here is code, that can be embedded in service:
var sv = this;
var deferred = sv.$q.defer();
if (sv._running) {
return sv._running;
}
sv._running = deferred;
It based on reusing promise. To make it query database once - just don't set sv._running to false and it will always return first obtained result.

Resources