I am facing a "problem" with AngularJS, services and scope.
It is not really a problem (I found a couple of ways to make it work), but I would like to know if I am doing the right thing, or if what I am doing can lead to problems in the future
I have a service that holds some global data; the service has two methods:
getData()
refreshData()
refreshData triggers some work (rest invocation etc.) and it is called at precise points inside different controllers, in response to user actions (button clicks etc).
getData is (obviously) called to get the data back.
In the controllers, how should I use it to access the data and put it in scope, so that it can be accessed from the view(s)?
Alternative 1:
controller('MyController', function ($scope, myService) {
$scope.data = myService.getData();
//use data in functions and in the view, ex: ng-hide="data.forbidden"
Alternative 2:
controller('MyController', function ($scope, myService) {
$scope.data = function() { return myService.getData(); }
//use data() in functions and in the view, ex: ng-hide="data().forbidden"
Alternative 3:
controller('MyController', function ($scope, myService) {
$scope.forbidden = function() { return myService.getData().forbidden; }
//... one function for each data.member used in this view
//usage in the view: ng-hide="forbidden()"
Alternative 4: use $apply or $watch
I am currently using the second approach, as it works even when a new controller is not created (think about different partials in the same page, with different controllers).
Does it make any sense? Or is there a better approach?
It depends on the usage. The Alternative 1 or 3 maybe used when you want to populate the data when the page is loaded or when the controller is initialized. The Alternative 2 can be used when you want to trigger the data refresh by clicking on a button or some other actions. Alternative 4 can be used when you want data load is driven by data change on other data model. So I think every alternative you posted makes sense in the correct scenario.
Related
I am beginning my development in angular and I don't know much. What I'm trying to do is that I am trying to pass a fairly large collection of data from one controller to another. This is how I managed to do it.
angular.module("myApp").controller("controllerName", function($rootScope, $scope, *...other stuff...*)
{ /* code */ }
Later there is one specific method which is accessed from outside, and I copy the collection like this:
$rootScope.selectedItems = angular.copy($scope.selected.items);
(This is an array of 5k strings)
Which is then catched in another controller. Other developers said it is unsafe to pass this through $rootScope but after the data is passed and copied into local controller, I use this to get rid of the collection in rootScope
delete $rootScope.selectedItems;
Is this a safe way to do this? It works perfectly, and nothing seems dangerous to me
As a general rule, don't use $rootScope to pass data. By using it you make your module dependent on unspecified functionality which may not be a dependency of your module. It's a structural issue which will create problems later.
Instead, use a service:
angular.module("myApp").service("myAppService", function () {
this.selectedItems = [];
});
angular.module("myApp").controller("controllerName1", function(myAppService, $scope) {
$scope.selectedItems = myAppService.selectedItems;
});
angular.module("myApp").controller("controllerName2", function(myAppService, $scope) {
$scope.selectedItems = myAppService.selectedItems;
});
It's also recommended that all your logic goes into services (and factories/providers where appropriate). Controllers should be used only to expose service functionality, unless a necessary special case can be proven. This makes the logic in services easier to unit test.
There are many service are available you should go with broadcast
Here is example for $broadcast service
https://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
I'm attempting to create a persistent Array throughout a client's session without actually using $window.sessionStorage. Right now every single time I change route the array empties out, even if it's the same exact route I was just on. Is it possible to make data persistent without using sessions or localStorage?
var a = [];
Pushing anything into it:
a.push(b);
Result of a after rerouting: [];
I would suggest using a service. A service in AngularJS is a singleton - meaning, that the same instance can be injected throughout the app.
This is better than the alternative of using the $rootScope, since it "pollutes" the scope and also does not lend itself to ease of testing with mocked injectables. It's hardly any better than using a global variable.
You could just create an injectable value that contains that array:
app.value("AVal", []);
and that would be enough. Of course, if you created a service, it would allow you to abstract away the details of the data structure:
app.factory("ASvc", function(){
var a = [];
return {
add: function(val){
a.push({v: val})
},
pop: function(){
var item = a.splice(a.length - 1, 1);
return item[0].v || null;
}
};
});
However you choose to do this, both are available as injectables, for example:
app.controller("MainCtrl", function($scope, AVal, ASvc){
AVal.push({v: 5});
// or
ASvc.add(5);
});
Your controller function will re-run on route changes, clearing your local variables every time. There are a few ways to skin the cat here, but for something like this I would suggest using $rootScope, which is a special top level controller that won't re-run unless the whole app refreshes.
// controller
function WhateverController ($scope, $rootScope) {
// create array if one doesn't exist yet
$rootScope.persistentArray = $rootScope.persistentArray || []
$rootScope.persistentArray.push('Heyoo')
$scope.localArray = $rootScope.persistentArray
}
$rootScope can be passed to factories as well (pretty sure), but you can also achieve what you want with a typical factory, with properly scoped variables with getter / setters
I have a requirement to list, edit and delete an entity. I have different views for each of this operations. I want to know if it is a good practice to use the same Angular just controller for these operations that works with each of the operation or should there be a separate controller for each?
Also if using same controller for these operations, is it possible to call different function when different views are loaded? So when user goes to the list view, a list method is called on page load and when he goes to the edit view, an edit method of the controller is called on edit view's load. I manage to achieve this by calling the methods using ngInit but apparently that is not recommended in v1.2 and should only be used with ngRepeat.
My question is similar to this one. Angular - Using one controller for many coherent views across multiple HTTP requests
However I also want to know if there is a way to call different initialisation methods of the same controller depending on the view the controller is used by.
A better approach could be to write a utility service which can be used across the controller. Use this service in your different controllers.
Your service will look something like this:
(function() {
'use strict';
// this function is strict...
angular
.module('myapp.services', [])
.service('Utility', function() {
var Utility = {};
Utility.edit = function(id, dataset) {
//perform edit related task here
};
Utility.delete = function(id, dataset) {
//perform edit related task here
};
return Utility;
})
}());
I have got my answer here: Using same controller for all CRUD operations (Rails-alike)
Apparently it is a good practice to use a different controller for each view, and it shouldn't work as a service. This is quite different for someone coming from MVC/WebAPI into angular.
The code below represents a situation where the same code pattern repeats in every controller which handles data from the server. After a long research and irc talk at #angularjs I still cannot figure how to abstract that code, inline comments explain the situations:
myApp.controller("TodoCtrl", function($scope, Restangular,
CalendarService, $filter){
var all_todos = [];
$scope.todos = [];
Restangular.all("vtodo/").getList().then(function(data){
all_todos = data;
$scope.todos = $filter("calendaractive")(all_todos);
});
//I can see myself repeating this line in every
//controller dealing with data which somehow relates
//and is possibly filtered by CalendarService:
$scope.activeData = CalendarService.activeData;
//also this line, which triggers refiltering when
//CalendarService is repeating within other controllers
$scope.$watch("activeData", function(){
$scope.todos = $filter("calendaractive")(all_todos);
}, true);
});
//example. another controller, different data, same relation with calendar?
myApp.controller("DiaryCtrl", function($scope, Restangular,
CalendarService, $filter){
//this all_object and object seems repetitive,
//isn't there another way to do it? so I can keep it DRY?
var all_todos = [];
$scope.todos = [];
Restangular.all("diary/").getList().then(function(data){
all_diaries = data;
$scope.diaries = $filter("calendaractive")(all_diaries);
});
$scope.activeData = CalendarService.activeData;
$scope.$watch("activeData", function(){
$scope.todos = $filter("calendaractive")(all_diaries);
}, true);
});
DRY should be followed purposefully, not zealously. Your code is fine, the controllers are doing what they are supposed to be doing: connecting different pieces of the app. That said, you can easily combine repeated code in a factory method that returns a function reference.
For example,
myApp.factory('calendarScopeDecorator', function(CalendarService, Restangular, $filter) {
return function($scope, section) {
$scope.todos = [];
$scope.activeData = CalendarService.activeData;
Restangular.all(section+"/").getList().then(function(data){
$scope.all_data = data;
$scope.filtered_data = $filter("calendaractive")(data);
});
$scope.$watch("activeData", function(){
$scope.todos = $filter("calendaractive")($scope.all_data);
}, true);
}
});
And then incorporate this into your controller:
myApp.controller("DiaryCtrl", function($scope, calendarScopeDecorator){
calendarScopeDecorator($scope, 'diary');
});
I wouldn't do this kind of thing with a watcher and local reference like in this controller. Instead, I would use $on() / $emit() to establish a pub-sub pattern from the service out to the controllers that care about its updates. This is an under-used pattern IMO that provides a more "DRY" mechanism. It's also extremely efficient - often more so than a watcher, because it doesn't need to run a digest to know something has changed. In network-based services you almost always know this for certain, and you don't need to go from knowing it for certain to implying it in other locations. This would let you avoid the cost of Angular's deep inspection of objects:
$rootScope.$on('calendarDiariesUpdated', function() {
// Update your $scope.todos here.
}, true);
In your service:
// When you have a situation where you know the data has been updated:
$rootScope.$emit('calendarDiariesUpdated');
Note that emit/on are more efficient than using broadcast, which will go through all nested scopes. You can also pass data from the service to listening controllers this way.
This is a really important technique that does a few things:
You no longer need to take a local reference to activeData, since you aren't actually using it (it's DRY).
This is more efficient in most/many cases than a watcher. Angular doesn't need to work out that you need to be told of an update - you know you do. This is also kind of a DRY principle - why use a framework tool to do something you don't actually need? It's an extra step to put the data somewhere and then wait for Angular to digest it and say "whoah, you need to know about this."
You may even be able to reduce your injections. There's no need to take CalendarService because that service can pass a reference to the array right in its notification. That's nice because you don't need to refactor this later if you change the storage model within the service (one of the things DRY advocates also advocate is abstracting these things).
You do need to take $rootScope so you can register the watcher, but there's nothing in pub-sub concepts that violate DRY. It's very common and accepted (and most important: it performs very well). This isn't the same thing as a raw global variable, which is what scares people off from using $rootScope in the first place (and often rightly so).
If you want to be "Super DRY" you can re-factor the call to $filter into a single method that does the filtering, and call it both from your REST promise resolution and from the calendar-update notification. That actually adds a few lines of code... but doesn't REPEAT any. :) That's probably a good idea in principle since that particular line is something you're likely to maintain (it takes that static "calendaractive" parameter...)
It looks like the code in the 2 controllers is identical except for the path to which the API call is made: "vtodo/" or "diary/".
One way to achieve something closer to DRY-ness is to pass the API path as an option to the controller as an attribute. So, assuming we call the controller ApiController, this can be used as
<div ng-controller="ApiController" api-controller-path="vtodo/">
<!-- Todo template -->
</div>
and
<div ng-controller="ApiController" api-controller-path="diary/">
<!-- Diary template -->
</div>
Which is then accessible in the controller by the injected $attrs parameter:
myApp.controller("ApiController", function($scope, $attrs, Restangular, CalendarService, $filter) {
// "vtodo/" or "diary/"
var apiPath = $attrs.apiControllerPath;
As a caution I would beware of over-architecting, not everything needs to be factored out, and there is an argument that you are just following a design pattern rather than copy+pasting code. However, I have used the above method of passing options to a controller for a similar situation myself.
If you are worried about making multiple calls to the same resource CalendarService, I'd recommend finding a way to cache the result, such as defining a variable in that service or using Angular's $cacheFactory.
Otherwise, I don't see anything wrong with your patterns.
I was told if you need to share between controllers you should use a service. I have controller A, which is a list of news websites, and controller B which is a list of articles on the sites from controller A. Controller B contains the list of articles and an iframe to display the articles. But when you click on controller A it should fade out the iframe and fade in the list. In order to accomplish this I give Controller B's scope to a service that is injected into both controller A and controller B. My question is whether or not it's okay to do that.
Basically, I do this:
app.factory("sharedService", function () {
var $scope = null;
return {
SetScope: function (scope) {
$scope = scope;
},
ControllerB_Action: function () {
$scope.doSomething();
}
};
});
app.controller("controllerA", ["$scope", "sharedService", function ($scope, sharedService) {
$scope.onaction = function () {
sharedService.ControllerB_Action();
}
}]);
app.controller("controllerB", ["$scope", "sharedService", function ($scope, sharedService) {
sharedService.SetScope($scope);
}]);
I would say its not a good pattern, since basically, the $scope is an Object that represents your current view or DOM-State. A controllers (and/or Link-Functions of directives) are the Glue between this state and your Application-Logic - so in my opinion, the $scope-Object should always remain inside the Controllers/Links.
Therefore if you wanna share Data between 2 Controllers, you should extract what you wanna share inside the Service, but not put the whole scope there (since it has lots of additional information that you dont need and want inside both controllers).
What you can do is simply link the data you wanna share by reference - that way, your Service will also Sync the Data between the two Scopes.
There's probably a world of ways of doing this, but I'll tell you what I would do:
I'd make use of event emmiters.
Disclaimer: I haven't tested this code
Assuming that Controller B is nested in Controller A:
Controller A
var controllerBFunction_A;
$scope.$on('EventFromControllerB',function(data){
controllerBFunction_A = data.sharedFunctions.controllerBFunction_A;
});
Controller B
$scope.$emit('EvenFromControllerB',{
sharedFunctions: [
controllerBFunction_A,
controllerBFunction_B,
someOtherObject
]
});
I think this could work. In my opinion this has the benefit of selecting what you want to share between those controllers...but probably there's a more elegant way of doing this.