Angular.js tabs directive with dynamic loading of partial templates and controllers

The Angular.js framework strongly encourages modular application design, particularly when it comes to separating application logic from DOM manipulation. One of the greatest “it-just-works” components of Angular.js has got to be the ngRoute module, which allows extremely straightforward mapping of URLs to partial templates that load into a region of the main application template marked up with an ngView directive. For instance, a couple of simple routings might be defined as follows:

	function($routeProvider) {
			when("/home", {
				controller: "Home",
				templateUrl: "partials/home.html"
			when("/products", {
				controller: "Products",
				templateUrl: "partials/products.html"

This would cause the respective partial templates (specified by the templateUrl property) to be loaded into a <div ng-view> in the main template, with the specified controllers attached. Unfortunately, there can only be one ng-view in the main template, so routing can only load partial templates into a single part of the main template.

In the app I’m currently working on, the single ng-view is used to load partial templates into the “detail view” of a two-pane list-detail view, in which the list view is a sidebar menu and the detail view takes up the majority of the screen and shows the “detail” corresponding to the current sidebar selection:


This works well with one key exception: one of the detail views needs to include a number of other detail views in a tabbed area like this:


If we’re to make this work, it would obviously be highly desirable to re-use the same partial template HTML and controller JavaScript regardless of whether the detail view is loaded into the full screen view or the tabbed view of another detail view. With only one supported ng-view, this isn’t currently possible if we use the ngRoute module. Before filling this gap in functionality, it’s worth noting that the AngularUI Router framework already supports this exact type of functionality. Unfortunately, as of writing, the framework still comes with a disclaimer that the API is in flux and that it shouldn’t be used in production unless you’re willing to keep track of the changelog. Similarly, a much more sophisticated router is planned for AngularJS 2.0, and will be backwards compatible with AngularJS 1.3. But, for the time being, there are no “stable” modules that seem to enable this kind of functionality.

Angular AJAX Tabs

Starting with the tabs example on the AngularJS homepage, support for controller and template attributes can easily be added to the pane element. All of the changes to the directive are then concentrated in the link function, which is worth dissecting further:

link: function(scope, element, attrs, tabsCtrl) {
	var templateCtrl, templateScope;

	if (attrs.template && attrs.controller) {
		scope.load = function() {
			$http.get(attrs.template, {cache: $templateCache})
			.then(function(response) {
				templateScope = scope.$new();
				templateScope.isTabbedPane = true;
				templateCtrl = $controller(attrs.controller, {$scope: templateScope});
				element.html(;'$ngControllerController', templateCtrl);
				element.children().data('$ngControllerController', templateCtrl);


The link function first checks for the template and controller attributes on the element. If they don’t exist, it falls back to the standard tabs behaviour of inlining the HTML into the pane and adding the pane to the panes array in the tab controller’s scope. If both attributes do exist, a load function is added to the passed in scope. (The load function will later be called from the select function in the directive.)

The load function first uses $http.get to retrieve the partial template from the URL specified in the template attribute, adding it to the templateCache if it’s not already there. Once the template’s been retrieved, a new scope is created for the template and a single isTabbedPane property is added.

A new controller is then instantiated based on the controller attribute string, with the new scope injected in.

The HTML from the template is then set as the innerHTML of the pane element using the .html() function from Angular’s built in jqLite library. Similarly, the jqLite .data() function is then used to associate the instantiated controller with all of the pane’s child elements under the “$ngControllerController” key. $ngControllerController is poorly documented and there is certainly some confusion as to its purpose, but from a quick look through the AngularJS source code, it looks like it’s used by the internal jqLiteController function to return the default controller for an element. (jqLiteController provides the functionality underpinning the public controller() method on angular.element)

Finally, the template is compiled with the new template scope and the pane is added to the array in the tab controller’s scope as it would be had we not specified a controller and template.

Elsewhere, there’s also one minor change to the tabs directive, which simply triggers the pane’s load method when the select method is called at the time of a tab switch. The full select method in the tabs directive controller scope is then as follows:

$ = function(pane) {
	angular.forEach(panes, function(pane) {
    	pane.selected = false;
	if (pane.load !== undefined) {
	pane.selected = true;

With the full module in place, ng-repeats and some structured metadata can be used to generate both the sidebar menu and the tabs themselves:

var tabbedPaneMetaData = [{
	"name": "Pane 1",
	"path": "pane-1",
	"partial": "pane-1.html",
	"controller": "Pane1",
	"includedInTabView": true
}, {
	"name": "Pane 2",
	"path": "pane-2",
	"partial": "pane-2.html",
	"controller": "Pane2",
	"includedInTabView": true
}, {
	"name": "Pane 3",
	"path": "pane-3",
	"partial": "pane-3.html",
	"controller": "Pane3",
	"includedInTabView": false

In the tabbed pane, the following HTML could be added to the tabbed detail view controller:

	<pane ng-repeat="pane in panes | filter:{includedInTabView:true}" 

Et voila, (relatively) straightforward configuration of ng-view partial templates and controllers as both full ng-view citizens and as participants in a tabbed view within another ng-view. The full Angular AJAX Tabs module source code can be found on GitHub with a barebones demo of the full functionality live on Plnkr.

Finally, if you’re already familiar with the concepts behind AngularJS but want to learn more about directives, Josh Kurz has written an excellent book, Mastering AngularJS Directives, that does the deep dive on writing directives, including many of the aspects covered briefly in this post.