Following situation:
There are two directives with controllers (A & B) - which are both children of another controller (C).
Controller A manages stuff for its model.
Now Controller C needs to call functions from Controller A to modify some stuff. And furthermore it needs access to some properties and read them.
I'm uncertain on what the right approach to communication is. And wheter to stick to one approach.
Following up is a small code example to illustrate the problem in a more concrete way.
First off there is a provider where components can register themselfes.
angular.module('components', [])
.provider('db', function(){
this.registerComponent = function(name, component){
...
}
});
Now there is a directive & a Controller (A) which displays concrete components.
angular.module('components')
.directive('componentDashboard', function(){
return {
scope:{
concreteComponents: '='
},
controller: function($scope){
$scope.model = concreteComponents;
$scope.model.someImportantProp = "foo";
$scope.addComponent = function(c){...}
}
}
})
That is basically the setup. The directive componentDashboard can display a set of components which registered to the db provider.
There are controller functions (A) like addComponent which needs to be called from outside of the controller (Controller B wants to call this). Furthermore Controller B wants to access different properties and so on.
Whats the preferred way of doing this?
At the moment there are these paradigmas used:
1) Factory hack ?! Basically there is a factory with some functions:
angular.module('components')
.factory('componentStub', function($log){
return {
addComponent : function(c){
$log.error("stub not overwritten");
}
}
})
These functions are now overwritten by the component's directive:
angular.module('components')
.directive('componentDashboard', function(componentStub){
return {
scope:{
concreteComponents: '='
},
controller: function($scope){
$scope.model = concreteComponents;
$scope.model.someImportantProp = "foo";
$scope.addComponent = function(c){...}
componentStub.addComponent = function(c){
$scope.addComponent(c);
}
}
}
})
2) event based
angular.module('components')
.factory('notificationCenter', function(){
return {
registerToNotification: function(id, not, cb){..}
}
})
.directive('componentDashboard', function(notificationCenter){
return {
scope:{
concreteComponents: '='
},
controller: function($scope){
$scope.model = concreteComponents;
$scope.model.someImportantProp = "foo";
$scope.addComponent = function(c){...}
notificationCenter.registerToNotification("foo", "doAddComponent", function(c){
$scope.addComponent(c)
}
}
}
})
At the moment there are both approaches used. There are some advantages of using this. Its developed fast, there are just few depencencies or restrictions to it. And it works.
But! I'm unsure about whether it is a good approach. I'm experiencing kinda bad maintenance on that and the more complex it gets(lets say some components can be added and some not -> states) the more it feels like not the right approach.
How should this problem be approached ?
Sorry for the long question, thanks in advice
Schemii
There are multiple methods how you can solve this. Ensuring that it is maintainable comes down to isolation: let the different components work on their own without dependencies of each other.
Combine any of the following tactics (from easiest to harder to implement):
Let the parent view pass the model (or parts of it) to child directives (via attributes)
$watch for changes in the main controller/directive to react on changes.
Let the parent controller/directive register ($scope.$on) callbacks to certain events. Childs can $emit events upwards to their parents. Parents can $broadcast events downwards to their children.
Let the child directive expose a callback/expression (see the & prefix in the isolate scope options)
Share a (singleton) service instance that will handle model changes. Inject this service where needed. Since this is a singleton, you'll have to make sure that you'll cleanup callbacks whenever a scope is destroyed otherwise you'll leak memory.
Hope this helps.
Cheers.
Related
I have a module called LegalModule, there are three components that subscribe to the same module, basically:
Both components have their own folder and each have an index.js file where they bootstrap like:
angular.module('LegalModule')
.component('person', require('person.component.js')
.controller('PersonController', require('person.controller.js');
and another file like
var component = {
templateUrl: 'person-tamplate.html',
controller: 'PersonController',
bindings: {info: '<'}
}
module.exports = component;
Then in that controller i have something like :
var controller = ['PersonRepository','$stateParams', function(PersonRepository, $stateParams)
{
var vm = this;
//other code
function Save(){
//code that saved
}
function onSuccess(){
//Let another component know this happened and call its refresh function.
}
}];
Other component / controller
angular.module('LegalModule')
.component('buildings', require('buildings.component.js')
.controller('BuildingController', require('buildings.controller.js');
and the component
var component = {
templateUrl: 'building-template.html'
controller: 'BuildingController'
}
Controller
var controller = ['BuildingReader',function(BuildingReader){
function refreshBuildings(){
//this needs to be called on success of the save of the Person Repository
}
}];
On the main tamplate:
<div class="LegalFacilities">
<person></person>
<buildings></buildings>
</div>
So i am new to components and i am not sure how to make in a way that when something is saved in the person controller, on it's success, that it can trigger the refresh function in the building controller to fire.
I really do not want to use $scope or anything like that , there is gotta be a cleaner way?. (not sure but i would appreciate any inputs).
Since you have two components that are not on the same DOM element, your methods of communicating between them are more limited. You still have several ways that you can do it:
onSuccess() emits an event on the $rootScope and all interested controllers listen for that event (just make sure to unsubscribe to the event on $destroy).
Create one or more services that contain the all the non-UI shared application state. All controllers that need access to state inject the service that contains that state. And controllers can also $watch a variable on the service to be notified when something changes and something needs to be refreshed.
Pass state around using the parent scope. Ie- each child scope declares a scope variable that is bound to the same variable in the parent scope. And if the state changes in one of the child scopes, the $digest cycle will ensure that the state is propagated to the other child scope.
In general, my preference is #2. The reason is that this keeps a clear separation between application state and UI state. And it becomes very easy to ensure that all parts of your application can share bits that they need to.
In your case, since you need to notify that an action happened, you can trigger this through changing a successHash number (an opaque number that just gets incremented on every save such that all watchers are notified).
Edit: a very simple example of sharing state using services.
angular.module('mymod').service('myService', function() {
this.val = 9;
});
angular.module('mymod').directive('dir1', function(myService, scope) {
scope.doSomething().then(res => myService.val = res);
});
angular.module('mymod').directive('dir2', function(myService, scope) {
scope.$watch(() => myService.val, () => console.log(`It happened! ${myService.val});
});
I'm building a small two-language app with the use of angular-translate. I want to have a language switcher in every view (controller). I'm trying to figure out how to put the code responsible for language switching into every controller. The code looks like this:
var langSwitch = $Scope.setLang = function (langKey) {
$translate.use(langKey);
};
So far I've figured that I can create a factory that looks like this:
app.factory('langSwitch', function ($rootScope, $translate) {
var langSwitch = $rootScope.setLang = function (langKey) {
$translate.use(langKey);
};
return langSwitch;
});
and inject it into controllers in this maner:
app.controller('HomeCtrl', function (langSwitch) {
// normal controller code here
});
This works but 1) I'm using $rootScope and I have a feeling this is bad practice & 2) jsHint screams that "langSwitch" is not defined. Maybe there is a simpler way to make the function global without putting it into every controller?
I'm still pretty new to Angular so don't scream at me :) Thanks.
edit
My view:
<button ng-click="setLang('en_GB')">English</button>
<button ng-click="setLang('pl_PL')">Polish</button>
Although you got the idea, you overcomplicated things a bit. You could declare the service as follows:
app.service('langSwitch', function ($translate) {
this.setLang = function (langKey) {
$translate.use(langKey);
};
});
And then inject langSwitch in the controller responsible for lang switching, as you already did. No need to inject $rootScope in the service.
You don't need $rootScope indeed unless you need to process some global events in your application. All services and factories in angular are singletons by default. That means once it created, it will be passed as the same instance in every place it is declared as a dependency. So if you want to share data and functionality between different controllers - the services will suit fine. You can change your factory code to:
app.factory('langSwitch', function($translate) {
return {
setLang: function(langKey) {
$trasnlate.use(langKey);
};
};
});
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 a directive with injected providers (with private variables) - sample code below and in http://jsfiddle.net/jycchoong/BmaQD/3/. The provider injected is as expected when (a) its method is invoked inline in the directive's link function, or (b) through a $broadcast invoked in the directive's link function. However, when (c) it is invoked via a $timeout (or in the case of my code, $watch after an AJAX call dirties the watched object), the provider injected is associated with the last instantiation of the directive.
You can see (c) occurring in the output (first line, third sentence). Whereas in (a) or (b) (first two sentences in the output), "Instance 1" is returned from the provider, for (c), "Instance 2" is returned. The $scope itself is correct (see $scope.$id in the output, so we're in the correct directive instance), but the provider instance is for the last directive.
Any suggestions for whether I'm doing something wrong or should I be structuring the code differently? For a broader explanation of the pattern I'm pursuing (which you do not have to read), have a look at the background notes after the code fragments below, if there are suggestions for different ways of achieving the overall objective.
Thanks for any help!!!
PS. This is not a closure issue. All scenarios (a), (b) and (c) have the same local/closure visibility in the log function.
HTML:
<div my-directive id="Instance 1"></div>
<div my-directive id="Instance 2"></div>
JS:
var app = angular.module('app', ['myModule']);
angular.module('myModule',[])
.directive('myDirective', function (myProvider, $timeout) {
return {
restrict: 'A',
scope: true,
link: function ($scope, element) {
element.text('');
var log = function ($event, eventData) {
element.text(element.text()+'For source "'+($event ? eventData.source : 'Inline')
+ '", scope id = ' + $scope.$id + ' with provider "' + myProvider.externalFn()
+ '". ');
};
myProvider.init(element.attr('id'));
// Inline
log();
$scope.$on('myEvent', log);
// Invoke broadcast inline
$scope.$broadcast('myEvent', {source: 'Broadcast Inline'});
$timeout(function() {
// Invoke broadcast in $timeout or from $watch
$scope.$broadcast('myEvent', {source: 'Broadcast Delayed'});
}, 1000, false);
}
};
})
.provider('myProvider', function () {
var internalVar;
return {
$get: function () {
return {
init: function (value) {
internalVar = value;
},
externalFn: function () {
return internalVar;
}
};
}
};
});
Output:
For source "Inline", scope id = 003 with provider "Instance 1". For source "Broadcast Inline", scope id = 003 with provider "Instance 1". For source "Broadcast Delayed", scope id = 003 with provider "Instance 2".
For source "Inline", scope id = 004 with provider "Instance 2". For source "Broadcast Inline", scope id = 004 with provider "Instance 2". For source "Broadcast Delayed", scope id = 004 with provider "Instance 2".
Background
What I'm looking to do isn't, I expect, that unusual. I'm keeping the directive code and the provider code separate. I chose the provider because the service needs to be able to store its own private variables, and each instance of the provider/directive combo has different values in the private variables. This allows different developers to be working on services vs. directives (or to allow different variants of a particular service to be injected). Everything was working great until I had to get my data updated from an AJAX call and all of a sudden, the injected provider was no longer the one I was expecting.
An obvious workaround is to have the $scope passed into the provider - as you would have seen in the code above, the right $scope is available. That's kind of ugly and breaks the benefits of the pattern. I certainly don't want the provider developer fiddling around with the whole $scope. Another solution is to create a $scope.myProvider object and then only pass that down to the provider to work with (and store everything in) so that the rest of the $scope isn't visible, but this seems like a bit of a hack.
Comments welcomed...
Okay, I think I figured it out. Rookie AngularJS mistake. Services are not traditional classes. You can't instantiate them to have different private variables for each directive that they are injected into. There's only one "instantiation". If there's a need to have different variable values per directive, you must stick those variables into the $scope (each directive will have a different $scope). It takes a bit of getting used to given it's a substantial change in thinking from traditional OO classes.
The solution is to push the $scope into the provider and if there's a desire to limit the provider's "scope" to avoid mucking up the rest of the scope, do something like the following (http://jsfiddle.net/jycchoong/BmaQD/5/). It's self policing (in the provider) though. I would love to know if there's a better way...
.provider('myProvider', function () {
var myProviderScope;
var setMyProviderScope = function($scope) {
myProviderScope = $scope.myProvider;
};
return {
$get: function () {
return {
init: function ($scope, value) {
if (!$scope.myProvider) $scope.myProvider = {};
setMyProviderScope($scope);
myProviderScope.val = value;
},
externalFn: function ($scope) {
setMyProviderScope($scope);
return myProviderScope.val;
}
};
}
};
});
I have a function that I will attach to my scope like this. It's attached to the scope as I use this function in my HTML pages Presently I am doing this in more than one controller. Note that my controllers are all top level controllers so I cannot really put this in a higher up controller and have it inherited.
$scope.isNotString = function (str) {
return (typeof str !== "string");
}
I asked how I could share this functionality and was given the following
example:
app.service('myService',function(){
this.sharedFunction = function() {
//do some stuff
};
});
myCntrl1($scope,myService) {
$scope.doSomething = function() {
myService.sharedFunction();
}
}
myCntrl2($scope,myService) {
$scope.doSomething = function() {
myService.sharedFunction();
}
}
Is there a way that I could more directly share it by passing
in $scope to the service and in that way eliminating the need for:
$scope.doSomething = function() {
myService.sharedFunction();
}
In each controller.
You can attach the function to the parent (root) scope, but using a service is the preferred way of sharing code between controllers.
You could call myService.init($scope) in the controller and that function could append properties to the scope but more likely you would want to use a parent controller from which you inherit.
You either declare a shared function in a top level "controller" or in a "service" like you mentioned in your example. There's no other better way so far.
You can assign the scope to a global variable, that will expose that scope globally, but will behave the same way like "service", except you don't have to inject it like service, rather can call by globalVar.dosomething().