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:
app.config(["$routeProvider",
function($routeProvider) {
$routeProvider.
when("/home", {
controller: "Home",
templateUrl: "partials/home.html"
}).
when("/products", {
controller: "Products",
templateUrl: "partials/products.html"
}).otherwise({
redirectTo:"/home"
})
}
])
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:
![two-pane-view](/wp-content/uploads/2014/10/two-pane-view.png)
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:
![two-pane-view-with-tabbed-main-view](/wp-content/uploads/2014/10/two-pane-view-with-tabbed-main-view.png)
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(response.data);
element.data('$ngControllerController', templateCtrl);
element.children().data('$ngControllerController', templateCtrl);
$compile(element.contents())(templateScope);
});
};
}
tabsCtrl.addPane(scope);
}
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:
$scope.select = function(pane) {
angular.forEach(panes, function(pane) {
pane.selected = false;
});
if (pane.load !== undefined) {
pane.load();
}
pane.selected = true;
};
With the full module in place, ng-repeat
s 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:
<tabs>
<pane ng-repeat="pane in panes | filter:{includedInTabView:true}"
tab-title="{{pane.name}}"
tabcontroller="{{pane.controller}}"
template="{{pane.partial}}">
</pane>
</tabs>
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.