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:

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

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

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-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:

<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.

Quality of life by age, sex and body mass index

Just a quick post on some data reported in Section 6 of NICE Clinical Guideline 43 (originally from a 2004 publication by Macran) which reports quality of life (QoL) by sex, and BMI and age brackets:1Macran S. The Relationship between Body Mass Index and Health-Related Quality of Life. 190. 2004. York, The Publications Office, Centre for Health Economics. Centre for Health Economics Discussion … Continue reading

NICE CG43 QoL Figure

The only unexpected aspect of the data is the high QoL in males with BMI greater than 40 aged 25-34 and females aged 18-24, but that’s likely driven by low sample size. Otherwise, there’s a general downward trend with age and an inverted J-shaped curve with respect to BMI, which approximately mirrors the J-shaped curves observed when plotting BMI against mortality 2Berrington de Gonzalez A, Hartge P, Cerhan JR et al. Body-mass index and mortality among 1.46 million white adults. N Engl J Med. 2010;363(23):2211–9.,3Tobias DK, Pan A, Jackson CL et al. Body-mass index and mortality among adults with incident type 2 diabetes. N Engl J Med. 2014;370(3):233–44.,4Dudina A, Cooney MT, Bacquer DD et al. Relationships between body mass index, cardiovascular mortality, and risk factors: a report from the SCORE investigators. Eur J Cardiovasc Prev Rehabil. … Continue reading and certain diabetes complications.5Sohn MW, Budiman-Mak E, Lee TA, Oh E, Stuck RM. Significant J-shaped association between body mass index (BMI) and diabetic foot ulcers. Diabetes Metab Res Rev. 2011;27(4):402–9.

References

References
1 Macran S. The Relationship between Body Mass Index and Health-Related Quality of Life. 190. 2004. York, The Publications Office, Centre for Health Economics. Centre for Health Economics Discussion Paper.
2 Berrington de Gonzalez A, Hartge P, Cerhan JR et al. Body-mass index and mortality among 1.46 million white adults. N Engl J Med. 2010;363(23):2211–9.
3 Tobias DK, Pan A, Jackson CL et al. Body-mass index and mortality among adults with incident type 2 diabetes. N Engl J Med. 2014;370(3):233–44.
4 Dudina A, Cooney MT, Bacquer DD et al. Relationships between body mass index, cardiovascular mortality, and risk factors: a report from the SCORE investigators. Eur J Cardiovasc Prev Rehabil. 2011;18(5):731-42.
5 Sohn MW, Budiman-Mak E, Lee TA, Oh E, Stuck RM. Significant J-shaped association between body mass index (BMI) and diabetic foot ulcers. Diabetes Metab Res Rev. 2011;27(4):402–9.

50ms

As had been widely anticipated, Apple announced the Apple Watch (styled WATCH in the logo mark) on Tuesday 9th. There is much to consider and much that’s still unknown about the device, but it certainly looks like it will initially make an interesting accessory to the iPhone and, in time, a useful free-standing addition to the gadget armamentarium. I’ll reserve overall judgement until it’s released, but the positioning of the Apple Watch is certainly worth considering now.

While the Apple Watch clearly does function as a timepiece, I found the name odd at first. As MG Siegler noted regarding the choice of name:

“It’s a hell of a lot more than a watch. I find the name at best lazy, at worst boring.”

But name aside, the Apple Watch wasn’t introduced as being only a watch. Horace Dediu wrote a typically insightful piece on the three “tentpoles” (key features) of the Apple Watch as listed by Tim Cook in the keynote:

  • A precise timepiece
  • A new, intimate way to communicate
  • A comprehensive health and fitness device

And to be fair, Apple did cover the non-timepiece tentpoles thoroughly in the keynote (in addition to the payment aspect, the ability to use it as a hotel door key, a remote control and the world’s smallest photo album, etc.). But I still found the emphasis on the time-telling abilities of the watch curious at first. There was repeated mention of the watch’s 50 millisecond accuracy in the Keynote. At 59:55 into the keynote, almost the first thing Tim Cook said about the watch was the following:

“We set out to make the best watch in the world. One that is precise. It’s synchronized with a universal time standard and it’s accurate within plus or minus 50 milliseconds.”

Then at 70:22, Jony Ive says:

“Apple Watch is incredibly accurate. It uses multiple technologies keeping time to plus or minus 50 milliseconds. We have worked closely with horological experts from around the world to help us understand the cultural and historical significance of timekeeping.”

Certainly in the technology world, ±50 millisecond accuracy is unimpressive to the point of actually being relatively inaccurate. NTP, which has been around since 1985, is comfortably able to synchronise a device to within a few milliseconds of UTC over a decent internet connection. And since the Apple Watch relies on the iPhone, presumably it’s actually just synchronising to the iPhone’s internal clock, which in turn is set using NTP and/or the cellular carrier and/or the GPS signal. So although Tim Cook put forth “precision timepiece” as one of the tentpoles of the Apple Watch, I was still left wondering about the emphasis on the watch’s accuracy. For a company that is usually both plainspoken and concise in their communication, it struck me as odd that they would draw attention to this unremarkable technological feat twice in ten minutes.

But thinking about it a bit more, I think the emphasis relates to where the watch (lowercase “w”) exists at the intersection of technology, fashion and timekeeping. It’s clear from the event invitees, some of Apple’s recent hires, and the extremely high quality of the devices, that the Apple Watch is competing not with existing smartwatches, but with watches full stop. While the S1 “computer on a chip” clearly does represent haute technologie, Apple is aiming first and foremost for haute horologie. Looking at the 50ms from a programmer’s perspective is completely misguided. By framing the Apple Watch in the same technical class as mechanical watches from Patek Philippe, Jaeger-LeCoultre, et al., Apple is likely trying to make it clear that, along with its exquisite aesthetics, the Apple Watch also holds its own against the precision, accuracy and meticulous engineering found in timepieces from the established Swiss watchmakers.

Now, it’s worth noting that no one in the market for a high-end watch would actually be swayed by the 50 millisecond accuracy argument. After all, a $10 Casio watch keeps time better than a Rolex and, at the high end of the watch market, people aren’t actually all that concerned with sub-second timekeeping accuracy (Patek Philippe watches might drift by, say, one second a day). But I can see Apple’s reasoning here. They’re a new entrant to the watch market and they need to demonstrate that they’re getting the basics right. By conveying that image of precision out of the gate, they’re trying to put themselves on an even footing with extremely prestigious brands in a completely new market for them.

Obviously the traditional watchmakers themselves have not reacted well to this, dismissing the Apple Watch as a fad (as opposed to the “eternity in a box” provided by traditional Swiss watchmakers). Their haughtiness is perhaps in part an early attempt to thwart Apple’s early marketing efforts and put some distance between the Apple Watch and their own products in customers’ minds. But then it’s probably also fuelled by the fact that Apple has recently made a good few strategic hires from amongst their ranks and perhaps even a sense of panic the likes of which haven’t been felt by incumbents since the launch of the original iPhone.