Is it possible to get access to a controller without declaring it? - angularjs

I have a bit of a weird situation which requires me to pass my controllers to my directives via a directive scope variable. This works great as long as there are only one controller in use per route, which is declared in my $routeProvider.
But now I have to have 2 controllers in use in the same template, which causes problems because I can't declare my controllers using ng-controller because that will throw a routeProvider error since I'm trying to access data from my route resolve. (You can only access route resolve data if you declare the controller in the same route as the resolve, which then makes using ng-controller in the template and controller in the directive useless to me).
So this is what I want to do:
// Declare one controller in the routing
.when('/someroute', {
controller: 'MyCtrl'
}
// But pass a different controller to my directive that hasn't been declared
// in either the route, template or directive
<my-directive ctrl="MyOtherCtrl"></my-directive>
But my question is, is it possible to access a controller and its functions without declaring the controller as ng-controller, controller in directive or in route? My far-fetched idea is that there's a service or something that you can inject which holds all of the controllers, but so far I've come up with none.

You could inject th controller by name into your directives controller using $controller.
var someOtherController = $controller('SomeOtherController ',{$scope: $scope});
It's methods would now be available on $scope. Be careful with this though, things can get hairy quickly.

There is a service to get an instance of any controller, use the $controller
in your directive, inject the $controller service then use it in your link function:
myApp.directive("myDirective", function($controller){
return {
scope: {
ctrl: "#"
},
link: function(scope, element, attrs){
var myNeededCtrl = $controller(scope.ctrl, {$scope: scope, otherDepenciesThatTheControllerNeed: ...});
myNeededCtrl.doSomething();
}
};
});

Related

is there a way to have a directive put an object in the injector for its controller?

{
controller: 'myController',
templateUrl: '/templates/my-template.html',
restrict: "E",
};
Is there any way that I can add an object in the injector so that it's available inside myController? I know angular bootstrap does this with it's "resolve" attribute, when it news up a controller, so I would like to know if there's a way to do this here. And yes, I realize I can make this object available to the controller by setting it on the scope property, but I would like to know if I can do this through the injector rather than scope.
There is a way to manually create a controller and supply dependencies with $controller, however in case of passing the object from the link function to the controller in the directive (as seems to be in your case) there is an easier way and one does not need to pollute the scope to achieve that.
All that is needed is to obtain the reference to the already instantiated controller. This is simply done with require of own controller:
.directive("foo", function(){
return {
require: "foo",
controller: function($scope){
//...
},
link: function(scope, element, attrs, fooCtrl){
fooCtrl.someObj = {a: "aaa"};
}
}
});
Unlike the "resolved" value, this object is not available when the controller function runs. This is rarely needed though. If you need, however, to perform some initialization only when the object is available, then you can always expose a this.init = function(){...} function on the controller, and invoke it as needed.
You can create a service and inject it in your controller. Let's say the service is called "myObject", you can then define your controller as:
controller: function($scope, myObj) {
// myObj is accessible in the controller
}

How to get the current route's controller (or scope)?

I have a controller that needs a thing provided by a route resolve function:
$routeProvider.when('/some/url', {
controller: MyController,
controllerAs: 'myCtrl',
resolve: {
theAnswer: ['deepThought', function(deepThought) {
return deepThought.computeTheAnswerAndReturnAPromise();
}]
}
});
var MyController = ['$route', function($route) {
this.theAnswer = $route.current.theAnswer;
}];
Now I want to do an end-to-end test, checking that the route matches and that parameters are propagated properly:
// ...set up the routes...
$location.path('/some/url');
$rootScope.$digest();
var ctrl = ???;
expect(ctrl.aThing).toBe(42);
In the non-test setup, I can put in a log statement and see that the controller is being created successfully and gets the correct data injected. The only problem is: how to get hold of the controller in the test?
There is $route.current.controller, but it contains the controller's constructor function and not the controller instance.
The documentation promises a $route.current.locals.$scope, from which I could get myCtrl, but the $scope property doesn't actually exist unless we also use ngView (it gets set here).
The controller isn't registered with any module, so I can't use $provide to intercept its creation and stash the controller somewhere.
Found it, thanks to #PSL's comment. The thing that actually constructs the controller is the ngView link function. We can fake that easily enough:
var ctrl = $controller(MyController, $route.current.locals);

AngularJS: when creating a new directive, why there is a controller?

To my understanding, controller is responsible for preparing the model, and pass the model to the directive which is responsible for updating DOM.
So when creating a new directive, why there is a controller inside?
Does this mean that I can do something like connecting server inside a directive?
app.directive('hover', function () {
return {
restrict: 'E',
controller: function ($scope) {
// what is the controller for?
...
}
}
}
});
From the docs on $compile service:
controller
[…] The controller is instantiated before
the pre-linking phase and it is shared with other directives (see
require attribute). This allows the directives to communicate with
each other and augment each other's behavior. The controller is
injectable (and supports bracket notation) with the following locals:
$scope - Current scope associated with the element
$element - Current element
$attrs - Current attributes object for the element
$transclude - A transclude linking function pre-bound to the correct transclusion scope. The scope can be overridden by an optional first argument.
function([scope], cloneLinkingFn).

Can you emit event from Controller in one module to Directive Controller in another module?

I have a controller "MyController" and a directive "MyDirective" in separate modules "app" and "directiveModule" respectively. DirectiveModule has been injected into the angular.module of "app".
The issue I am having is as part of "app", I have a controller that emits an event "TEST" that the controller for the directive does not pick up. How can I successfully get the directive of its own module to catch the emit? Is this possible? (Note: I tried $scope originally, then used $rootScope, but both do not make a difference.)
My controller:
app.controller('MyController', function($scope, $rootScope) {
$rootScope.$emit('TEST');
});
My directive:
directiveModule.directive('MyDirective', ['$rootScope', 'MyService',
function($rootScope, MyService) {
return {
restrict: 'E',
controller: ['$scope', '$rootScope', function($scope, $rootScope) {
$rootScope.$on('TEST', function() {alert("Event Caught")})
}]};
}];
UPDATE: It looks like my directive has not initiated by the time the event is broadcast. Is there a way to make it such that I could "wait for directive to instantiate" as opposed to waiting an arbitary "1000" ms or another alternative?
I think the only case your directive needs to catch that event is to get some initial data depending on the MyController because if your directive is independent of the MyController, you don't need to catch that event, just initiate the directive independently.
My solution is using $watch to get notified when the initial data from the MyController is ready.
app.controller('MyController', function($scope) {
$scope.data = {}; //initialize the data, this data could be fetched from an ajax
});
directiveModule.directive('MyDirective', ['MyService',
function(MyService) {
return {
restrict: 'E',
controller: ['$scope', function($scope) {
$scope.$watch("data",function(newValue,OldValue,scope){
//do your work here when data is changed.
});
}]};
}];
In case you use isolate scope in your directive:
directiveModule.directive('MyDirective', ['MyService',
function(MyService) {
return {
restrict: 'E',
scope : {
directiveData:"=",
},
controller: ['$scope', function($scope) {
$scope.$watch("directiveData",function(newValue,OldValue,scope){//watch on the directive scope property
//do your work here when data is changed.
});
}]};
}];
Your html may look like this:
<my-directive directive-data="data" /> //bind isolate scope directive property with controller's scope property.
I think below questions are some similar cases like this:
AngularJS : Directive not able to access isolate scope objects
AngularJS : directives and scope
all modules injected into main module share same rootScope. $emit however travels up the scope tree, you need $broadcast.
If you $broadcast from $rootScope it will be received by all active scopes so in directive could actually use $scope.$on and wouldn't need to inject $rootScope just for the purpose of using $on
Similarly if directive is descendant of the controller, could also broadcast from controller scope
You can. Demo: http://plnkr.co/edit/vrZgnu?p=preview
A side note: if you try to communicate to children element that is lower in DOM tree, you should use $broadcast. $emit is used to communicate with parents elements ("up" direction).
But your true problem might be that when you tried to emit the event, the directive might not be initiated yet. I used $timeout service to wait 1000ms to broadcast the event.
Update
If you don't want to wait a arbitrary 1000ms, you can let your directive $emit "ready" event to inform controller that it's ready. Then broadcast from the controller.
Also removed the 1000ms in this demo http://plnkr.co/edit/GwrdIw?p=preview, everything still works. But I don't think this is a reliable solution.

$routeParams is empty in main controller

I have this piece of layout html:
<body ng-controller="MainController">
<div id="terminal"></div>
<div ng-view></div>
<!-- including scripts -->
</body>
Now apparently, when I try to use $routeParams in MainController, it's always empty. It's important to note that MainController is supposed to be in effect in every possible route; therefore I'm not defining it in my app.js. I mean, I'm not defining it here:
$routeProvider.when("/view1", {
templateUrl: "partials/partial1.html"
controller: "MyCtrl1"
})
$routeProvider.when("/view2", {
templateUrl: "partials/partial2.html"
controller: "MyCtrl2"
})
// I'm not defining MainController here!!
In fact, I think my problem is perfectly the same as this one: https://groups.google.com/forum/#!topic/angular/ib2wHQozeNE
However, I still don't get how to get route parameters in my main controller...
EDIT:
What I meant was that I'm not associating my MainController with any specific route. It's defined; and it's the parent controller of all other controllers. What I'm trying to know is that when you go to a URL like /whatever, which is matched by a route like /:whatever, why is it that only the sub-controller is able to access the route parameter, whereas the main controller is not? How do I get the :whatever route parameter in my main controller?
The $routeParams service is populated asynchronously. This means it will typically appear empty when first used in a controller.
To be notified when $routeParams has been populated, subscribe to the $routeChangeSuccess event on the $scope. (If you're in a component that doesn't have access to a child $scope, e.g., a service or a factory, you can inject and use $rootScope instead.)
module.controller('FooCtrl', function($scope, $routeParams) {
$scope.$on('$routeChangeSuccess', function() {
// $routeParams should be populated here
});
);
Controllers used by a route, or within a template included by a route, will have immediate access to the fully-populated $routeParams because ng-view waits for the $routeChangeSuccess event before continuing. (It has to wait, since it needs the route information in order to decide which template/controller to even load.)
If you know your controller will be used inside of ng-view, you won't need to wait for the routing event. If you know your controller will not, you will. If you're not sure, you'll have to explicitly allow for both possibilities. Subscribing to $routeChangeSuccess will not be enough; you will only see the event if $routeParams wasn't already populated:
module.controller('FooCtrl', function($scope, $routeParams) {
// $routeParams will already be populated
// here if this controller is used within ng-view
$scope.$on('$routeChangeSuccess', function() {
// $routeParams will be populated here if
// this controller is used outside ng-view
});
);
As an alternate to the $timeout that plong0 mentioned...
You can also inject the $route service which will show your params immediately.
angular.module('MyModule')
.controller('MainCtrl', function ($scope, $route) {
console.log('routeParams:'+JSON.stringify($route.current.params));
});
I have the same problem.
What I discovered is that, $routeParams take some time to load in the Main Controller, it probably initiate the Main Controller first and then set $routeParams at the Child Controller. I did a workaround for it creating a method in the Main Controller $scope and pass $routeParams through it in the Child Controllers:
angular.module('MyModule')
.controller('MainController', ["$scope", function ($scope) {
$scope.parentMethod = function($routeParams) {
//do stuff
}
}]);
angular.module('MyModule')
.controller('MyCtrl1', ["$scope", function ($scope) {
$scope.parentMethod($routeParams);
}]);
angular.module('MyModule')
.controller('MyCtrl2', ["$scope", function ($scope) {
$scope.parentMethod($routeParams);
}]);
had the same problem, and building off what Andre mentioned in his answer about $routeParams taking a moment to load in the main controller, I just put it in a timeout inside my MainCtrl.
angular.module('MyModule')
.controller('MainCtrl', function ($scope, $routeParams, $timeout) {
$timeout(function(){
// do stuff with $routeParams
console.log('routeParams:'+JSON.stringify($routeParams));
}, 20);
});
20ms delay to use $routeParams is not even noticeable, and less than that seems to have inconsistent results.
More specifically about my problem, I was confused because I had the exact same setup working with a different project structure (yo cg-angular) and when I rebuilt my project (yo angular-fullstack) I started experiencing the problem.
You have at least two problems here:
with $routeParams you get the route parameters, which you didn't define
the file where you define a main controller doesn't really matter. the important thing is in which module/function
The parameters have to be defined with the $routeProvider with the syntax :paramName:
$routeProvider.when("/view2/name1/:a/name2/:b"
and then you can retrieve them with $routeParams.paramName.
You can also use the query parameters, like index.html?k1=v1&k2=v2.
app.js is the file where you'd normally define dependencies and configuration (that's why you'd have there the app module .config block) and it contains the application module:
var myapp = angular.module(...);
This module can have other modules as dependencies, like directives or services, or a module per feature.
A simple approach is to have a module to encapsulate controllers. An approach closer to your original code is putting at least one controller in the main module:
myapp.controller('MainCtrl', function ($scope) {...}
Maybe you defined the controller as a global function? function MainCtrl() {...}? This pollutes the global namespace. avoid it.
Defining your controller in the main module will not make it "to take effect in all routes". This has to be defined with $routeProvider or make the controller of each route "inherit" from the main controller. This way, the controller of each route is instantiated after the route has changed, whereas the main controller is instantiated only once, when the line ng-controller="MainCtrl" is reached (which happens only once, during application startup)
You can simply pass values of $routeParams defined into your controller into the $rootScope
.controller('MainCtrl', function ($scope, $routeParams, MainFactory, $rootScope) {
$scope.contents = MainFactory.getThing($routeParams.id);
$rootScope.total = MainFactory.getMax(); // Send total to the rootScope
}
and inject $rootScope in your IndexCtrl (related to the index.html)
.controller('IndexCtrl', function($scope, $rootScope){
// Some code
});

Resources