22

I'm trying to dynamically assign a controller for included template like so:

<section ng-repeat="panel in panels">
    <div ng-include="'path/to/file.html'" ng-controller="{{panel}}"></div>
</section>

But Angular complains that {{panel}} is undefined.

I'm guessing that {{panel}} isn't defined yet (because I can echo out {{panel}} inside the template).

I've seen plenty of examples of people setting ng-controller equal to a variable like so: ng-controller="template.ctrlr". But, without creating a duplicate concurrant loop, I can't figure out how to have the value of {{panel}} available when ng-controller needs it.

P.S. I also tried setting ng-controller="{{panel}}" in my template (thinking it must have resolved by then), but no dice.

Jakob Jingleheimer
  • 30,952
  • 27
  • 76
  • 126
  • a plunker would be helpful. – jaime Dec 19 '12 at 01:38
  • 5
    Sorry, what is a "plunker"? Tried googling it and saw stuff about baseball… – Jakob Jingleheimer Dec 19 '12 at 01:50
  • my bad, should've linked: http://plnkr.co/ . Regarding the issue I'm curious about what you have in `panels` are these strings or functions? A plunker can help us find what's causing the problem faster. – jaime Dec 19 '12 at 02:07
  • Oh that looks like jsfiddle. I'll check it out when I get to work in the AM and try to set one up (it looks really complicated?). The values of `panel` are strings that match names of controllers: `$scope.sidepanels = ["Alerts","Subscriptions"];` <-- set in `Main()` controller. – Jakob Jingleheimer Dec 19 '12 at 03:43
  • http://stackoverflow.com/questions/21204371/from-an-angularjs-controller-how-do-i-resolve-another-controller-function-defin/21204497#21204497 – Khanh TO May 13 '15 at 13:57

5 Answers5

16

Your problem is that ng-controller should point to controller itself, not just string with controller's name.

So you might want to define $scope.sidepanels as array with pointers to controllers, something like this, maybe:

$scope.sidepanels = [Alerts, Subscriptions];

Here is the working example on js fiddle http://jsfiddle.net/ADukg/1559/

However, i find very weird all this situation when you might want to set up controllers in ngRepeat.

SET001
  • 11,480
  • 6
  • 52
  • 76
  • To clarify, the reason I'm dynamically assigning a controller within an ngRepeat is because each panel does something different: Panel1 is Alerts (and the Alerts logic is in the Alerts controller); Panel2 is Subscriptions (and the Subscriptions logic is in the Subscriptions controller. Each panel deals with very different info. Thanks again! – Jakob Jingleheimer Dec 19 '12 at 18:05
  • 3
    This solution requires that the (controller) scope that defines the tab configurations, and presumably renders links to activate them, has a reference to each controller. That's easy if they're global functions, but that's not best practice I believe. AFAIK, it's not possible to inject controllers into other ones, or into a TabConfigService, so I don't know how to accomplish this. – enigment Jan 11 '13 at 18:56
  • Can you make this approach work if the controllers are defined using the canonical "angular.module('app.controllers', []).controller('Alerts', ...)" style? – cjerdonek Jul 07 '14 at 19:52
  • @enigment I wrote up a solution below that doesn't require putting the controllers in the global scope. In my write-up, a controller can access another controller via a service (i.e. using Angular's dependency injection setup). – cjerdonek Jul 08 '14 at 07:57
  • @cjerdonek: It's possible with a custom directive: http://stackoverflow.com/questions/21204371/from-an-angularjs-controller-how-do-i-resolve-another-controller-function-defin/21204497#21204497 – Khanh TO May 13 '15 at 14:00
  • The use case is clear, say you have a generic "widgetContainer" and want to assign varying controllers from saved user preferences, like a list of modules in order that are different for each user. You can't hand code this, you have to assign controllers dynamically. – FlavorScape Sep 03 '15 at 18:45
6

To dynamically set a controller in a template, it helps to have a reference to the constructor function associated to a controller. The constructor function for a controller is the function you pass in to the controller() method of Angular's module API.

Having this helps because if the string passed to the ngController directive is not the name of a registered controller, then ngController treats the string as an expression to be evaluated on the current scope. This scope expression needs to evaluate to a controller constructor.

For example, say Angular encounters the following in a template:

ng-controller="myController"

If no controller with the name myController is registered, then Angular will look at $scope.myController in the current containing controller. If this key exists in the scope and the corresponding value is a controller constructor, then the controller will be used.

This is mentioned in the ngController documentation in its description of the parameter value: "Name of a globally accessible constructor function or an expression that on the current scope evaluates to a constructor function." Code comments in the Angular source code spell this out in more detail here in src/ng/controller.js.

By default, Angular does not make it easy to access the constructor associated to a controller. This is because when you register a controller using the controller() method of Angular's module API, it hides the constructor you pass it in a private variable. You can see this here in the $ControllerProvider source code. (The controllers variable in this code is a variable private to $ControllerProvider.)

My solution to this issue is to create a generic helper service called registerController for registering controllers. This service exposes both the controller and the controller constructor when registering a controller. This allows the controller to be used both in the normal fashion and dynamically.

Here is code I wrote for a registerController service that does this:

var appServices = angular.module('app.services', []);

// Define a registerController service that creates a new controller
// in the usual way.  In addition, the service registers the
// controller's constructor as a service.  This allows the controller
// to be set dynamically within a template.
appServices.config(['$controllerProvider', '$injector', '$provide',
  function ($controllerProvider, $injector, $provide) {
    $provide.factory('registerController',
      function registerControllerFactory() {
        // Params:
        //   constructor: controller constructor function, optionally
        //     in the annotated array form.
        return function registerController(name, constructor) {
            // Register the controller constructor as a service.
            $provide.factory(name + 'Factory', function () {
                return constructor;
            });
            // Register the controller itself.
            $controllerProvider.register(name, constructor);
        };
    });
}]);

Here is an example of using the service to register a controller:

appServices.run(['registerController',
  function (registerController) {

    registerController('testCtrl', ['$scope',
      function testCtrl($scope) {
        $scope.foo = 'bar';
    }]);

}]);

The code above registers the controller under the name testCtrl, and it also exposes the controller's constructor as a service called testCtrlFactory.

Now you can use the controller in a template either in the usual fashion--

ng-controller="testCtrl"

or dynamically--

ng-controller="templateController"

For the latter to work, you must have the following in your current scope:

$scope.templateController = testCtrlFactory
cjerdonek
  • 5,814
  • 2
  • 32
  • 26
  • Looks nice, but how do I get `testCtrlFactory`? Can you make a plunker of it? Thx. – Patrick Dec 19 '14 at 19:39
  • To use the `testCtrlFactory` service in a component, you just need to include `'testCtrlFactory'` in the requires array for the component, as you would to use any service. – cjerdonek Dec 20 '14 at 05:14
5

I believe you're having this problem because you're defining your controllers like this (just like I'm used to do):

app.controller('ControllerX', function() {
    // your controller implementation        
});

If that's the case, you cannot simply use references to ControllerX because the controller implementation (or 'Class', if you want to call it that) is not on the global scope (instead it is stored on the application $controllerProvider).

I would suggest you to use templates instead of dynamically assign controller references (or even manually create them).

Controllers

var app = angular.module('app', []);    
app.controller('Ctrl', function($scope, $controller) {
    $scope.panels = [{template: 'panel1.html'}, {template: 'panel2.html'}];        
});

app.controller("Panel1Ctrl", function($scope) {
    $scope.id = 1;
});
app.controller("Panel2Ctrl", function($scope) {
    $scope.id = 2;
});

Templates (mocks)

<!-- panel1.html -->
<script type="text/ng-template" id="panel1.html">
  <div ng-controller="Panel1Ctrl">
    Content of panel {{id}}
  </div>
</script>

<!-- panel2.html -->
<script type="text/ng-template" id="panel2.html">
  <div ng-controller="Panel2Ctrl">
    Content of panel {{id}}
  </div>
</script>

View

<div ng-controller="Ctrl">
    <div ng-repeat="panel in panels">
        <div ng-include src="panel.template"></div>        
    </div>
</div>

jsFiddle: http://jsfiddle.net/Xn4H8/

bmleite
  • 26,850
  • 4
  • 71
  • 46
  • Hi bmleite. thanks for your response! Actually I declare controllers like so: `function Main( $scope , $filter ) { $scope.sidepanels = [ Alerts , Subscriptions ]; }`. Also `
    ` misses the point of what I want, which is to ***dynamically*** assign the controller within the `ng-repeat`.
    – Jakob Jingleheimer Dec 19 '12 at 17:58
  • btw, is there a benefit to defining controllers with the method in your answer? I just did it the other way because that's how the tutorial did it. – Jakob Jingleheimer Dec 19 '12 at 18:02
  • 2
    In that case you have the implementations directly accessible from the global scope (`window`). For me that's bad, it means that any third-party script can access (and change?) it. Regarding the **dynamically** assign controller, by **dynamically** assigning a template you're also 'dynamically' assigning the controller, just on a more structured way (i.e. by changing the template from 'alert.html' to 'subscription.html' you're also changing the Controller from `AlertCtrl` to `SubscriptionCtrl`) – bmleite Dec 19 '12 at 18:23
  • Not as elegant IMO as specifying controllers directly in ng-repeat, but it works, with a caveat. Controllers in my code have an onLoad method, spec'd as the onload attribute of the ng-include tag. I want it to fire when the tab loads, but it doesn't, because it's defined in the controller's scope, which only exists inside the div in the ng-include. You could make the onLoad methods globals (bad as doing that with the controllers), or add them to the tab specs, with the same problem as SET's answer. Stumped. Plunk of my original is [here](http://plnkr.co/edit/49SdneT2S465HBjXYRkw?p=preview). – enigment Jan 11 '13 at 19:08
  • Oh! I didn't see that the first time! It is actually possible to access the panel object inside the template. This solved my problem. – mlunoe Feb 26 '13 at 11:09
  • how do you access panel.template within the panel controller? – kilianc Feb 10 '14 at 10:05
  • @bmleite I wrote up and posted a solution that does let you assign controllers dynamically -- without putting the controllers on the global scope and without needing to create intermediate templates. – cjerdonek Jul 12 '14 at 22:47
5

Another way is to not use ng-repeat, but a directive to compile them into existence.

HTML

<mysections></mysections>

Directive

angular.module('app.directives', [])
    .directive('mysections', ['$compile', function(compile){
        return {
            restrict: 'E',
            link: function(scope, element, attrs) {
                for(var i=0; i<panels.length; i++) {
                    var template = '<section><div ng-include="path/to/file.html" ng-controller="'+panels[i]+'"></div></section>';
                    var cTemplate = compile(template)(scope);

                    element.append(cTemplate);
                }
            }
        }
    }]);
Brenton
  • 317
  • 2
  • 9
-1

Ok I think the simplest solution here is to define the controller explicitly on the template of your file. Let's say u have an array:

$scope.widgets = [
      {templateUrl: 'templates/widgets/aWidget.html'},
      {templateUrl: 'templates/widgets/bWidget.html'},
];

Then on your html file:

<div ng-repeat="widget in widgets">
    <div ng-include="widget.templateUrl"></div>
</div>

And the solution aWidget.html:

<div ng-controller="aWidgetCtrl">
   aWidget
</div>

bWidget.html:

<div ng-controller="bWidgetCtrl">
   bWidget
</div>

Simple as that! You just define the controller name in your template. Since you define the controllers as bmleite said:

app.controller('ControllerX', function() {
// your controller implementation        
});

then this is the best workaround I could come up with. The only issue here is if u have like 50 controllers, u'll have to define them explicitly on each template, but I guess u had to do this anyway since you have an ng-repeat with controller set by hand.

Alex Arvanitidis
  • 4,403
  • 6
  • 26
  • 36