angular: proper to bind view to service data - angularjs

What is the proper way to bind view to data from a service? I have seen that $watch is discouraged.
I have a view that renders items from an array that come from the service.
The service will periodically refresh its data from the server
$http({...some params...})
.success(function(data) {
cache[key] = data;
});
In the view controller the data is exposed :
this.items = SomeFactory.cache[key]
View is binding :
ng-repeat="item in $ctrl.items"
my understanding is that since cache[key] is being assigned to a new array then the view is still binding to the old address and thus not updated.
What is the proper way to do this?
It does work if I bind it using a function (eg: ng-repeat="item in $ctrl.items()" but then how do i modify item? also performance wise i dont know if this is less efficient than binding directly?)

There are a few ways of doing this which I have used quite successfully before:
Option #1 - bind one way "up"
Assume we have the same factory structure:
angular.module('someApp').factory('SomeFactory', function($http) {
var cache = {};
$http({...some params...})
.success(function(data) {
// ...
cache[key] = data;
});
return {
cache: cache
};
});
Instead of setting items to be the cache[key] directly, we can bind to the factory itself. After all, a factory (or a service) is just an object:
this.cache = SomeFactory.cache // bind to container which is going to persist
Then in the template you just use ng-repeat="item in $ctrl.cache[key]" and it will automatically get the latest changes. You can even go the extra step and just bind your service to the scope directly, as this.SomeFactory = SomeFactory and then use it in your views as $ctrl.SomeFactory.cache[key]. It works well for data structures which are loaded once per app, or for services made specially for a specific controller.
There's a potential problem with this approach depending on how you use it. You mentioned modifying the array but since we override the reference asynchronously, it might be difficult to make sure those changes persist.
Option #2 - modify the array, not replace it
Another way of solving this is to modify our factory a little bit:
angular.module('someApp').factory('SomeFactory', function($http) {
var cache = {};
cache[key] = []; // initialise an array which we'll use everywhere
$http({...some params...})
.success(function(data) {
// ...
cache[key].push.apply(cache[key], data); // essentially a pushAll
// we could also set cache[key].length = 0 to clear previous data or do something else entirely
});
return {
cache: cache
};
});
As you can see, instead of replacing the reference, we're operating on the same array that the items property is referencing. The benefit here is that if we manipulate the array in any way (i.e. add extra items), we have a bit more flexibility about what to do with them once we receive new asynchronous data.
This is in fact similar to the approach ngResource uses - collection query methods return an array which is filled out later on, once the data is actually fetched.
Option #3 - expose a promise
The final way which I've used before is to just expose a promise for the data:
angular.module('someApp').factory('SomeFactory', function($http) {
var cache = {};
var whenReady = $http({...some params...})
.then(function(data) {
// ...
cache[key] = data;
return data;
});
return {
cache: cache,
whenReady: whenReady
};
});
Then in the controller you just assign it once it resolves:
SomeFactory.whenReady.then(items => {
this.items = items; // or this.items = SomeFactory.cache[key];
});
All of these avoid an extra $watch but it depends on what's most convenient for your app.

Related

Best practice transferring list data from controller to the another controller

I want to display view once the list of content is retrieved from the database display it in the view.
On a high level what I am doing now works more or less but upon first access the previous results appears to be cached or still saved in my storage service.
This is what the service looks like.
function StorageService() {
var opportunities;
var getOpportunities = function() {
return opportunities;
}
var setOpportunities = function(data) {
opportunities = data;
}
return {
getOpportunities: getOpportunities,
setOpportunities: setOpportunities
}
}
So when I click on the tab to getOpportunities I go direct to the view first and the load the data.
$scope.getAllOpportunities = function() {
$state.go("app.opportunities");
communicationService.getOpportunitiesAll().then(function(data) {
StorageService.setOpportunities(data);
if (!data.length) {
$("#noShow").html("There are no opportunities");
}
}, function(error) {})
}
once the view is rendered I retrieve the results and bind it to the view.
controller('OpportunitiesController', function($scope) {
$scope.$on("$ionicView.afterEnter", function(scopes, states) {
$scope.opportunities = StorageService.getOpportunities();
});
What I am doing here also feels a bit wrong.
Is there a better way or a way that I can improve on the existing.
I want to load the view and the replace the loader with the data once the data is ready.
Thanks in advance
You should resolve the promise in the route, using the resolve property. That way, the data will always be available when the controller is instantiated.
https://toddmotto.com/resolve-promises-in-angular-routes/
Unless the resource is huge and you want to show som loading animation while getting the data. Then it would probably be more proper to just get the data in the controller.
controller('OpportunitiesController', function($scope) {
communicationService.getOpportunitiesAll().then(function(response){
$scope.opportunities = response;
})
});
html:
<span ng-if="!opportunities">Getting stuff</span>
<span ng-if="opportunities">Stuff fetched</span>
Also, there is no use to have getter and setters in the service. Javascript objects are passed by reference so you can just expose the property directly.

Can't get my service value to update after initial assignment

I am using an Angular service to keep track of the state of some buttons and whether they should be shown or not. I am having issues that after the first assignment, I cannot change the value.
app.service('EntityButtonStateService', function(){
var state = {
showDelete: false,
showNew: false
};
this.getState = function(){
return state;
};
});
In my controller:
var buttonState = EntityButtonStateService.getState();
table.on('select', function(e, dt, node, config) {
var id = getSelectedRowId(dt);
buttonState.showDelete = true; //this assignments works
buttonState.showNew = true;
});
table.on('deselect', function(e, dt, node, config) {
buttonState.showDelete = false; //this assignment does not work
buttonState.showNew = true;
});
What could I be doing wrong?
UPDATED:
It appears that your biggest concern is keeping your data model for button state in sync with your UI state. There are a couple ways I can think of that will help you do that. First of all, I'll note that your service seems to only track boolean values for a couple of things (showNew, showDelete, etc.). It is a highly trivial task, one best suited for scoped variables, and to abstract this into a factory would be a showcase in elegant abstractions for the sake of elegant abstractions. Also note that your UI is likely very tightly-coupled to your data model anyway, so once again abstraction here would be futile. But all this is completely obvious, which means perhaps you are trying to do something more... So for the sake of posting something useful to you, here is another suggestion (also note, a fiddle would still be useful):
-> Set your factory instance to a scoped variable:
Instead of var buttonState = EntityButtonStateService.getState();
do $scope.buttonState = EntityButtonStateService.getState();
If you're not already familiar, Angular's scope service is what provides the automatic 2-way binding--hence my earlier comment. So, this will probably work, allowing your UI to stay in sync with the values for showNew and showDelete, etc. However, sometimes updating $scope can be troublesome (i.e. if your updates happen late in a digest cycle). At the first sign of trouble, I'll wrap my updates in a $timeout, which will push the updates to the next digest, and that usually does the trick.
Good luck.
OLD:
Try returning an object instance:
app.service('EntityButtonStateService', function(){
var state = function(){
this.showDelete = false,
this.showNew = false
};
this.getStateService = function(){
return new state();
};
});
And then you should be able to operate directly on the values of the returned service instance.
OR
If you want to track the state inside the service, then add getter and setters to the individual values. For example, a generic approach would look like this:
app.service('EntityButtonStateService', function(){
var state = {
showDelete: false,
showNew: false
};
this.get = function(item){
return state[item];
};
this.set = function(item, value){
state[item] = value;
};
});
Then you could use this code elsewhere:
// To set:
buttonState.set('showDelete', true);
// To get:
buttonState.get('showDelete'); // returns true

Is this the best way to retrieve an array with $firebase()?

I have arrays stored in Firebase, one of which I need to retrieve when a user logs in. Each user has their own array which requires authentication for read. (It would be inconvenient to switch to another data structure). Since $firebase() always returns an object, as per the docs, I'm using the orderByPriority filter. However, if I do simply
$scope.songs = $filter('orderByPriority')($firebase(myref));
that doesn't work as songs always get an empty array.
I don't understand why this happens, but what I've done to solve it is use the $firebase().$on('loaded',cb) form and applied the filter in the callback. Is this a good solution?
The drawback is that I cannot do $scope.songs.$save()
Here's my controller, including this solution:
.controller('songListController', function($scope, $rootScope, $firebase, $filter, $firebaseSimpleLogin){
var authRef = new Firebase('https://my-firebase.firebaseio.com/users'),
dataRef;
$scope.loginObj = $firebaseSimpleLogin(authRef);
$scope.songs = [];
$rootScope.$on("$firebaseSimpleLogin:login", function(event, user) {
// user authenticated with Firebase
dataRef = $firebase(authRef.child(user.id));
dataRef.$on('loaded', function(data){
$scope.songs = $filter('orderByPriority')(data);
});
});
//other controller methods go here
$scope.save = function(){
if (!$scope.loginObj.user)
{
alert('not logged in. login or join.');
return;
}
//Was hoping to do this
//$scope.songs.$save().then(function(error) {
//but having to do this instead:
dataRef.$set($scope.songs).then(function(error) {
if (error) {
alert('Data could not be saved.' + error);
} else {
alert('Data saved successfully.');
}
});
};
});
---Edit in response to Kato's answer---
This part of my app uses Firebase as a simple CRUD json store without any realtime aspects. I use $set to store changes, so I think I'm okay to use arrays. (I'm using jQueryUI's Sortable so that an HTML UL can be re-ordered with drag and drop, which seems to need an array).
I don't need realtime synchronisation with the server for this part of the app. I have a save button, which triggers the use of the $scope.save method above.
The problem with the approach above is that orderByPriority makes a single copy of the data. It's empty because $firebase hasn't finished retrieving results from the server yet.
If you were to wait for the loaded event, it would contain data:
var data = $firebase(myref);
data.$on('loaded', function() {
$scope.songs = $filter('orderByPriority')(data);
});
However, it's still not going to be synchronized. You'll need to watch for changes and update it after each change event (this happens automagically when you use orderByPriority as part of the DOM/view).
var data = $firebase(myref);
data.$on('change', function() {
$scope.songs = $filter('orderByPriority')(data);
});
Note that the 0.8 release will have a $asArray() which will work closer to what you want here. Additionally, you should avoid arrays most of the time.

Dynamic Service Strategies in AngularJS

How can I switch out a service on-the-fly and have all components (relying on the service) automatically be bound to the data on the new strategy?
I have a Storage service and two storage strategies, StorageStrategyA and StorageStrategyB. Storage provides the public interface to controllers and other components to interact with:
angular.module('app').factory('Storage', function ($injector) {
var storage;
var setStrategy = function (name) {
storage = $injector.get(name);
};
setStrategy('StorageStrategyB');
return {
getItems: function () {
return storage.getItems();
}
// [...]
};
});
But when the strategy is changed, the two-way binding breaks and the view doesn't update with items from getItems() from the new strategy.
I've created a Plunker to illustrate the problem.
Is there a way to combine the strategy pattern with AngularJS and keep the two-way binding?
Please note that in my actual app I cannot just call Storage.getItems() again after the strategy has been changed, because there are multiple components (views, controllers, scopes) relying on Storage and the service change happens automatically.
Edit:
I have forked the Plunker to highlight the problem. As you can see, the data in the upper part only updates because I manually call Storage.getItems() again after the strategy has been changed. But I cannot do that, because other component - for example OtherController - are also accessing data on Storage and also need to automatically get their data from the new strategy. Instead, they stay bound to the initial strategy.
Javascript works on references. Your array items in app is same reference as items of strategyB items initially with the below statement and when you update the StrategyB items automatically items in your view gets updated(since same reference).
$scope.items = Storage.getItems();
So, when you switch strategy you are not changing the reference of items. It still points to StrategyB items reference.
You have to use the below mechanism to change the reference.
Then you can do something where you can communicate between controllers to change the items reference.
Please find the plunkr I have updated.
$rootScope.$broadcast("updateStrategy");
And then update your item list and others.
$scope.$on("updateStrategy",function(){
$scope.name = Storage.getName();
$scope.items = Storage.getItems(); //Here changing the reference.
//Anything else to update
});
the two way binding is still ok, you have a reference issue.
when the AppController set up the $scope.items set to the StorageStrategyB items, then when you switch to StorageStrategyA, the AppController $scope.items is still set to StorageStrategyB items.
angular.module('app').controller('AppController', function ($scope, Storage) {
Storage.setStrategy('StorageStrategyB');
$scope.current = Storage.getName();
$scope.items = Storage.getItems();
$scope.setStrategy = function (name) {
Storage.setStrategy(name);
$scope.current = Storage.getName();
$scope.items = Storage.getItems();
console.log( $scope.items);
console.log($scope.current);
};
$scope.addItem = function () {
Storage.addItem($scope.item);
$scope.item = '';
};
});
You forgot
$scope.items = Storage.getItems();
nice question :)
plnkr

AngularJS - refresh list after adding new record to collection / db

How do I update/refresh my $scope.list when a new record is added to the db/collection - storage.set() method - please see comment in the code.
Please see code below.
angular.module("app", [])
.factory('Storage', function() {
var storage = {};
storage.get = function() {
return GetStuffHere();
}
storage.set = function(obj) {
return SetStuffHere(obj);
}
return storage;
})
.controller("MainCtrl", function($scope, Storage) {
$scope.addStuff = function(){
var obj = {
"key1" : "data1",
"key2" : "data2"
};
Storage.set(obj);
// update $scope.list here, after adding new record
}
$scope.list = Storage.get();
});
Here's an approach that stores the received data in the service as an array. It uses promises within the service to either send the previously stored array (if it exists) or makes an HTTP request and stores the response. Using promise of $http, it returns the newly stored array.
This now allows sharing of the stored array across other controllers or directives. When adding, editing, or deleting, it is now done on the stored array in the service.
app.controller('MainCtrl',function($scope, Storage){
Storage.get(function(data){
$scope.items=data
});
$scope.addItem=function(){
Storage.set({name: 'Sue'});
}
})
app.factory('Storage', function($q,$http) {
var storage = {};
storage.get = function(callback) {
/* see if already cached */
if( ! storage.storedData){
/* if not, get data from sever*/
return $http.get('data.json').then(function(res){
/* create the array in Storage that will be shared across app*/
storage.storedData=res.data;
/* return local array*/
return storage.storedData
}).then(callback)
}else{
/* is in cache so return the cached version*/
var def= $q.defer();
def.done(callback);
defer.resolve(storage.storedData);
return def.promise;
}
}
storage.set = function(obj) {
/* do ajax update and on success*/
storage.storedData.push(obj);
}
return storage;
})
DEMO
It's not 100% clear what you want to do, but assuming the storage is only going to update when the user updates it (i.e. there's no chance that two users in different locations are going to be changing the same stuff), then your approach should be to either:
Return a promise containing the newly stored object from the storage service after it's completed, and use .then(function() {...}) to set the $scope.list once it's complete.
You would want to take this approach if the storage service somehow mutates the information in a way that needs to be reflected in the front-end (for example an id used to handle future interaction gets added to the object). Note that $http calls return a promise by default so this isn't much extra code if you're using a web service for storage.
Just add the object to the list on the line after you call it with $scope.list.push(obj)
If you have something that changes on the server side without input from that particular client, then I would look into using a websocket (maybe use socket.io) to keep it up to date.
Solution below will work. However, I am not sure if it is best practice to put this in a function and call when needed (within MainCtrl):
i.e:
On first load
and then after new item added
.controller("MainCtrl", function($scope, Storage) {
$scope.addStuff = function(){
var obj = {
"key1" : "data1",
"key2" : "data2"
};
Storage.set(obj);
// rebuild $scope.list after new record added
$scope.readList();
}
// function to bind data from factory to a $scope.item
$scope.readList = function(){
$scope.list = Storage.get();
}
// on first load
$scope.readList();
});
You have to use
$scope.list = Storage.get;
and in template you can then use i.e.
<ul>
<li ng-repeat="item in list()">{{whateverYouWant}}</li>
</ul>
With this approach you will always have the current state of Storage.get() on the scope
couldn't
return SetStuffHere(obj)
just return the updated list as well? and assign that:
$scope.list = Storage.set(obj);
If this is an API endpoint that returns the single inserted item you could push() it to the $scope.list object.
but maybe I'm missing something you are trying to do...
Updating your backend/Factory stuff is a basic Angular binding done by calling a set/post service. But if you want to automatically refresh your controller variable ($scope.list) based on changes occuring in your factory then you need to create a pooler like function and do something like :
.run(function(Check) {});
.factory('Storage', function() {
var storage = {};
var Check = function(){
storage = GetStuffHere();
$timeout(Check, 2000);
}
// set...
Check();
return storage;
})
.controller("MainCtrl", function($scope, Storage) {
$scope.list = Storage.storage;

Resources