39

I'm trying to implement a plugin system in angularjs that would allow users to configure which "widgets" they will see on a certain page. Each widget is defined by a controller and a template(url). Is it possible to create a directive that instantiates a controller, invokes it with a template and transcludes the resulting content?

The goal is something like this:

<div class="widget" ng-repeat="widget in widgets">
    <widget controller="widget.controller" templateUrl="widget.templateUrl"></widget>
</div>
LorenVS
  • 12,597
  • 10
  • 47
  • 54
  • 1
    This is in essence what `ngView` does; though your use case is a little simpler, you may find its [source code](https://github.com/angular/angular.js/blob/master/src/ng/directive/ngView.js) helpful. Basically, you fetch, append to the DOM, and then `$compile` your template and then assign the controller: `element.children().data('$ngControllerController', controller);`. If I have time later today I'll post a more complete response. – Josh David Miller May 02 '13 at 17:24

1 Answers1

74

There are two ways to do this; one uses the helper directives already available (like ngInclude and ngController) and the second is manual; the manual version might be faster, but I cannot be sure.

The Easy Way:

The easy method is to simple create a new element with ngController and ngInclude attributes, append it to the directive's element, and then $compile it:

var html = '<div ng-controller="'+ctrl+'" ng-include="'+tpl+'"></div>';
element.append(html);
$compile( element.contents() )( scope );

The Manual Way:

The manual way is to do what these directives would themselves do in turn; this logic is very similar to what ngView does (though without the complexity). We fetch the template, storing it in $templateCache, and then append it to the DOM. We create a new child scope and instantiate the provided controller with it and assign that controller to the element. Finally, we $compile it:

$http.get( tpl, { cache: $templateCache } )
.then( function( response ) {
  templateScope = scope.$new();
  templateCtrl = $controller( ctrl, { $scope: templateScope } );
  element.html( response.data );
  element.children().data('$ngControllerController', templateCtrl);
  $compile( element.contents() )( templateScope );
});

(Note that there is no garbage collection here, which you would need to implement if the widgets change)

Here is a Plunker demonstrating both methods: http://plnkr.co/edit/C7x9C5JgUuT1yk0mBUmE?p=preview

Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • This looks like a great solution, it's clear and logical, the problem with the 'easy way' is that the ng-include is not getting evaluated, it does load the controller :) – perrohunter Jun 10 '14 at 00:23
  • 1
    This line is great: `$http.get( tpl, { cache: $templateCache } ) .then( function( response ) {...)`. Thanks! – SimplGy Jun 11 '14 at 17:02
  • This does not work for me unless I call `scope.$apply()` – Simone Aug 01 '14 at 20:24
  • 1
    @Simone That should only be necessary if you leave the AngularJS lifecycle at some point, which the code above does not do. See the Plunker. What have you changed from the above? Can you post a plunker of your own showing the problem? – Josh David Miller Aug 01 '14 at 20:32
  • Well the "easy way" does not show in which context it is called – Simone Aug 01 '14 at 21:33
  • @Simone True, but directives are the *only* acceptable place for DOM manipulation to occur - anywhere else is misusing AngularJS. So it should never be an issue, but I'm happy to help if you provide more information. – Josh David Miller Aug 01 '14 at 21:42
  • In my case I was trying to populate a Google Maps InfoWindow with the HTML generated by compiling an angular template and populating it with values coming from a scope. I was basically trying to use angular as a templating language, which worked well as soon as I called `$apply` on the scope. – Simone Aug 02 '14 at 01:44
  • 5
    This is awesome. However, I don't understand what associating the controller with the `'$ngControllerController'` .data() key does. I see that it's supposed to associate the controller with the element's children (or vice versa), but looking through the Angular source, I can't see how that happens and, as far as I can tell, the Plnkr still works as intended with that line commented out. Can anyone explain what this line's doing? – Rich Pollock Oct 13 '14 at 17:07
  • @RichPollock Very late answer here. I had the same question and had a quick look through the source. I found that it's set to enable the jqLite `controller` method to function correctly (see `angular.element`). I didn't dig too much deeper than that though. https://github.com/angular/angular.js/blob/master/src/jqLite.js#L439 – Jason Larke Oct 01 '15 at 02:04
  • 1
    Any ideas on what sort of garbage collection is required with the second solution? We're using this method to render a context panel so are swapping the template and controller depending on the property being edited. – Ben Foster Oct 29 '15 at 17:02