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
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.
I am writing my first non-trival Angular App and I have hit a snag with a directive. The directive takes data from a controller's scope and applies it to Google Chart. The chart is not the issue - which is to say it works fine with dummy data - it is access to the properties of the scope object which were obtained via http:
I am accessing data returned via an API in a service which utilizes $http:
dashboardServices.factory('SearchList', ['$http','$q',
function($http, $q){
return {
getSearchDetails:function(searchType, resultType){
return $http.get("api/searches/"+searchType+"/"+resultType)
.then(function(response){
if (typeof(response.data === 'object')) {
return response.data;
} else {
return $q.reject(response.data);
}
},function(response){
$q.reject(response.data);
});
}
}
}]);
In my controller, I am taking the response from this service and attaching to my scope via the promises' "then" method:
dashboardControllers.controller('DashboardCtrl', ['$scope', 'SearchList',
function($scope, SearchList){
$scope.searchData = {};
$scope.searchData.chartTitle="Search Result Performance"
SearchList.getSearchDetails("all", "count").then(function(response){
$scope.searchData.total = response.value; //value is the key from my API
});
SearchList.getSearchDetails("no_results", "count").then(function(response){
$scope.searchData.noResults = response.value;
});
}]);
To an extent this works fine, i can then use the 2-way binding to print out the values in the view AS TEXT. Note: I want to be able to write the values as text as I am trying to use a single scope object for both the chart and the textual data.
{{searchData.total | number}}
As mentioned, I have written a directive that will print a specific chart for this data, in this directive ONLY the $scope.searchData.chartTitle property is accessible. The values that were set in the then functions are not accessible in the directive's link method:
Here is the directive:
statsApp.directive('searchResultsPieChart', function(){
return{
restrict : "A",
scope:{
vals:'#vals'
},
link: function($scope, $elem, $attr){
var dt_data = $scope.vals;
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
}
});
Here is how I am using the directive in the view:
<div search-results-pie-chart vals="{{searchData}}"></div>
I can see that the issue is that the numeric values are not available to the directive despite being available when bound to the view.
Clearly the directive needs to be called later when these items are available or via some callback (or perhaps an entirely different approach), unfortunately i am not sure why this is the case or how to go about solving.
Any help would be greatly appreciated. I hope this makes sense.
I think the following will help you.
First change the directive scope binding for vals to use = instead of # (see this question for good explanation of the differences - basically # interpolates the value whereas = binds to the variable in the parent scope)
Then, move the part of the directive that creates the graph into a render function within your link function.
Then, $watch vals for any changes, then call the render function with the new values
You would also have to slightly change the approach of using ele[0], as you'll need to clear out the contents of it and add a new element with the new chart when the data changes (otherwise many charts will be added as the data changes!)
Here is an example of what to do in your link function with regard to the $watch and new render function (changing the $scope binding like I mentioned is not shown):
$scope.$watch('vals', function(newVals, oldVals) {
return $scope.render(newVals);
}, true);
$scope.render = function (dt_data) {
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
Hope this helps you out!!!
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 have an app that has items, and you can do things like add new items, update the text of an item, move the item to a different folder, etc.
I have an items factory that holds all the items as plain objects inside an array, and the factory returns a singleton that has various methods, like get(), set(), etc.
To add some context to the question, I'm working with Node.js and MongoDB as well.
Anyway, due to all the various factories I have, like items, folders, and all the various controllers for different views, I am relying heavily on events. To give some examples:
// items factory
update: function(params) {
// add to database, then...
.then(function() {
$rootScope.$emit('itemCreated');
});
}
// items controller
// I need to refresh the items list in the scope
$rootScope.$on('itemCreated', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
These are their own kind of "subset" of events, in that they all pertain to "CRUD" operations on items.
But, I also have other events that I use. For example, I have an interceptor that listens to any requests and responses. I have a loading widget (an image of a spinning wheel) that uses a directive. This directive will show the loading widget when a request begins, and hide the loading widget when a request ends. This is also event based.
// on request
$rootScope.$emit(_START_REQUEST_);
// on any response
$rootScope.$emit(_END_REQUEST_);
I attempted to "modularize" these request and response events by simply making them constants.
.constant('_START_REQUEST_', '_START_REQUEST_');
I am trying to find a solution in order to "modularize" all my other events, like the events emitted on CRUD operations for items. One idea I've had is to define all of the item CRUD events inside the items factory:
events: {
update: 'itemUpdate',
create: 'itemCreated'
// etc.
}
Then, I can simply inject my items factory into a controller, and reference events like so:
$rootScope.$on(items.events.update, function() {});
I also considered simply defining all events, regardless of whether they are interceptor events or item events, as constants in my app. However, it seemed like this solution directly coupled item events to the module itself, rather than to the items factory, which is where I feel they "belong".
Basically, the issue is that right now all my events definitions seem to be scattered around. My question is: what pattern or best practice would you recommend for modularizing and defining events in AngularJS?
I agree that these item events should belong to the event source. You could implement a observer pattern in the item factory that hides the dependency on $rootScope for event listeners. This way the event key itself is a private detail of the item factory, and the subscription to the event is made explicit by calling a dedicated function for it. This approach makes your code more independent of $rootScope and easier to maintain than an event name convention (thinking about usages search for the specific event subscription method vs. usages of $rootScope.$emit / $on):
angular.module('events', [])
.service('items', ['$rootScope', function($rootScope) {
var createdEventKey = 'item.created';
return {
create: function () {
$rootScope.$emit(createdEventKey, {"name": "aItemName"});
},
onCreated: function(callback, scope) {
var unsubscribeFunction = $rootScope.$on(createdEventKey, function(event, payload) {
callback(payload);
});
// allow to unsubscribe automatically on scope destroy to prevent memory leaks
if (scope) {
scope.$on("$destroy", unsubscribeFunction);
}
return unsubscribeFunction;
}
}
}])
.controller('TestController', function($scope, items) {
items.onCreated(function (item) {
console.log("Created: " + item.name);
}, $scope);
});
complete example: http://jsfiddle.net/8LtyB/32/
If all you want is a way to create a separate object for containing the names of events, why not use a service?
myApp.service('itemEvents', function () {
var events = {
update: 'itemupdate',
create: 'itemcreate',
...
};
return events;
});
This is essentially what you had before when you were suggesting using a factory to contain the event definitions, except that a service is a single object instance, and is instantiated at module start-up. In contrast, a factory creates a new instance when injected into a controller. (Here's a good SO post on the difference between services and factories)
You can inject this service into your controllers or directives:
myApp.controller('ItemController', function ($scope, itemEvents) {
$scope.on(itemEvents.update, function () { /* something interesting */ });
});
This gives you a nice place to centralize your event name definitions. As a side note, some people hold to the convention of using all lowercase when defining event names (so itemupdate instead of itemUpdate). Hope this helps!
You can use the following:
app.config(function($provide) {
$provide.decorator("$rootScope", function($delegate) {
var Scope = $delegate.constructor;
var origBroadcast = Scope.prototype.$broadcast;
var origEmit = Scope.prototype.$emit;
Scope.prototype.$broadcast = function() {
console.log("$broadcast was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origBroadcast.apply(this, arguments);
};
Scope.prototype.$emit = function() {
console.log("$emit was called on $scope " + Scope.$id + " with arguments:",
arguments);
return origEmit.apply(this, arguments);
};
return $delegate;
});
})
example: http://plnkr.co/edit/cn3MZynbpTYIcKUWmsBi?p=preview
src: https://github.com/angular/angular.js/issues/6043
assuming these $scope.$emit works like jquery events I would suggest you name your emits to be generic for example in you database update simply do this:
$rootScope.$emit('Created')
then in your items controller do this :
$rootScope.$on('Created.item', function() { // when an item is added to the database
$scope.items = items.getAll(); // retrieve all items from the items factory
});
then you can wire to the created event in any of your controllers and its name is generic. The .item should add a namespace. if you make all of your events in your items controller have the .item name space you should be able to do a
$rootScope.$off('item')
This will clear up memory leaks
I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>