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
Related
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.
EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
I'm trying to get my head around sharing data between multiple controllers, but couldn't find out yet how this is supposed to work (the angular way). I have create a Data service that look something like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
/// ... do async stuff
$rootScope.$broadcast("Data::filtered");
},
brush: function(brushed) {
/// ... do async stuff
$rootScope.$broadcast("Data::brushed");
},
load: function() {
/// ... do async stuff
$rootScope.$broadcast("Data::loaded");
}
};
});
Next I want to reuse and update data from this service, so I use it in my controller as follows:
angular.module('myapp.controllers')
.controller('FilterCtrl', function ($scope, $rootScope, DataSet) {
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
function updateBrushed() {
$scope.safeApply(function() {
$scope.brushed = DataSet.brushed;
});
};
$scope.brushed = [];
$scope.keepSelected = function() {
DataSet.filter(DataSet.FilterMethod.KEEP);
};
$scope.removeSelected = function() {
DataSet.filter(DataSet.FilterMethod.REMOVE);
};
$scope.$on('Data::brushed', updateBrushed);
$scope.$on('Data::filtered', updateBrushed);
});
The problem I have is basically illustrated by the use of the saveApply call. Basically I got this code from here: https://coderwall.com/p/ngisma. What I don't understand though is why I need it. As far as I can see, I'm 'within' $angular when updating the DataSet service. Nevertheless, the view for the Filter controller doesn't get updated without a call to saveApply ($apply doesn't work at all because than I run into the apply already in progress issue).
So, basically my question boils down to: is the approach above a good way to share data, and if so how is notification of changes in the service supposed to work?
Update: Based on Julian Hollman his suggestion I came to the following solution: http://jsfiddle.net/Ljfadvru/7/. This more or less illustrates the full workflow I was working on, though some of it is automatically induced in the fiddle, as opposed to user-interaction based in my real application. What I like about this approach is that it only sends signals when all data is updated.
Working with references, as suggested by Ed Hinchliffe, is nice as well. However, I'm working on a web visualization framework and I'm expecting tens of thousands of items. Clearing arrays and pushing new elements (which seem to me the consequence of this proposal) is really not feasible (if I understand this paradigm well, it would also result in a re-rendering of my vis for every single change). I stand corrected though if there are suggestions for further improvement.
$broadcast doesn't trigger an $apply and I bet your "async stuff" is not $http from angular.
So something happens outside of angular and angular doesn't know that something has changed.
In my opinion the best thing in that case is to write a wrapper for your async code and trigger $apply after date came back from the backend. Don't do it in the controller.
To be honest, I'm not sure quite sure about exactly what is going on with the digest loops in your particular scenario, but I don't think you are approaching this the right way.
The 'angular' way, is to use promises.
Your service should be more like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
var returnData = []
$http.get('/some/stuff').then(function(data){
for(i in data){
returnData.push(data[i]);
}
});
return returnData;
}
};
});
This sets up an empty placeholder object (returnData) that can be immediately passed to the controller, but a reference is kept so that when the data returns you can retrospectively populate that object. Because the controller and the service reference the same object, it'll 'just work'.
This way you don't have to worry about dealing with $digest or $apply or $broadcast.
You controller can just call $scope.filtered = DataSet.filter();
EDIT
If you want to be able to access the exact same data from multiple controllers:
angular.module('myapp.services')
.factory('DataSet', function($http) {
var cache = {
filtered: []
}
return {
getFiltered: function(){
if(cache.filtered.length) return cache.filtered;
$http.get('/some/url/').then(function(data){
for(i in data){
cache.filtered.push(data[i]);
}
});
}
};
});
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.
All over my code I have things like:
SearchModel.findAll($scope.report).then(function (xhr) {
$scope.searchResults= xhr.data;
});
is there anyway to just automagically assign the searchResults variable to the view after the request is done. Seems like there should be if not...
Promises are only resolved during a $digest cycle, so this should "automagically" update.
It really is that easy!
To prove it I made a fiddle that simulates a server response using a service and assign the data to a scope. The dom will automatically display the data.
service.get().then(function(data) {
$scope.data = data;
});
Hope this helped!
If you are dealing with a promise that returns an array, you can use a helper function like this:
app.factory('PromiseList', function() {
return function(promise, error) {
var list = [];
promise.then(function (result) {
angular.copy(result, list); # or in your case .copy(result.data, list)
}, error);
return list;
}
});
Then in your code, do:
$scope.searchResults = PromiseList(SearchModel.findAll($scope.report))
You can also use this solution with other kinds of objects, however not with strings or numbers as they are immutable objects in JavaScript.
If you are only developing for an old version of AngularJS (think pre-1.2), you can also just pass the promise to the template and it will be unwrapped automatically.