Replacing ngInclude With Component Directives In AngularJS
For the last few years, I've been using the ngInclude directive in order to render nested layouts in AngularJS. And, it's been working out quite well. But, last night, Igor Minar - AngularJS team member and wicked smart fellow - challenged me with the idea that ngInclude could be entirely replaced with "component directives." To date, the vast majority of my directives have been behavioral; so, the idea of using component directives is a bit foreign. As such, I felt that I needed to immediately experiment with the concept of replacing nested layouts with AngularJS components.
Run this demo in my JavaScript Demos project on GitHub.
In the past, if I had to render a view that had nested subviews, my AngularJS view code would have looked something like this:
<div ng-switch="subview">
<div ng-switch-when="a" ng-include=" 'a.htm' "></div>
<div ng-switch-when="b" ng-include=" 'b.htm' "></div>
<div ng-switch-when="c" ng-include=" 'c.htm' "></div>
</div>
As you can see, I have a "subview" $scope value that I am switching on and using it to include an HTML page. The included subview would then look something like this, often times, having its own switch for nested views:
<!-- a.htm. -->
<div ng-controller="ViewAController">
Some more stuff here.
<!-- Nested layout here. -->
<div ng-switch="subview">
<div ng-switch-when="x" ng-include=" 'x.htm' "></div>
<div ng-switch-when="y" ng-include=" 'y.htm' "></div>
<div ng-switch-when="z" ng-include=" 'z.htm' "></div>
</div>
</div>
And like I said, this approach has been quite successful for me. But, after I demonstrated that using ngRepeat with ngInclude can be a significant performance hit in AngularJS, Igor laid down his challenge:
To experiment with this idea, I wanted to create a small demo that had two different "layout" components. Then, within each layout component, I would render a sub-component for a particular list (friends and enemies).
In order to do this, I have to change the way that I think about my page hierarchy. Rather than thinking about it in terms of nested views, each of which invokes a controller using the ngController directive, I have start thinking about it in terms of nested components, each of which binds a particular controller to a particular view.
Let's take a look at the main index page that I created in this demo:
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title>{{ windowTitle }}</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body
ng-class="{ primary: ( layout == 'primary' ), secondary: ( layout == 'secondary' ) }"
ng-switch="layout">
<!--
Each of these ngSwitchWhen directives will conditionally render a COMPONENT
directive. The component directive will take care of rendering a template and a
controller as well as any other nested components that are relevant.
-->
<div ng-switch-when="primary" primary-layout>
<!-- Content provided by component directive. -->
</div>
<div ng-switch-when="secondary" secondary-layout>
<!-- Content provided by component directive. -->
</div>
<p class="legal">
<strong>Note</strong>: This is a new approach for me, so take with grain of salt.
</p>
<!--
CAUTION: Commingling ngSwitchWhen directives next static content has only been
available since AngularJS 1.2 (stable). In prior versions, the ngSwitch container
is completely emptied when switching rendering templates.
Load application scripts.
-->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.6.min.js"></script>
<script type="text/javascript" src="./app.controller.js"></script>
<!-- Enemies list component. -->
<script type="text/javascript" src="./enemies-list/enemies-list.controller.js"></script>
<script type="text/javascript" src="./enemies-list/enemies-list.directive.js"></script>
<!-- Friends list component. -->
<script type="text/javascript" src="./friends-list/friends-list.controller.js"></script>
<script type="text/javascript" src="./friends-list/friends-list.directive.js"></script>
<!-- Primary Layout component. -->
<script type="text/javascript" src="./primary-layout/primary-layout.controller.js"></script>
<script type="text/javascript" src="./primary-layout/primary-layout.directive.js"></script>
<!-- Secondary layout component. -->
<script type="text/javascript" src="./secondary-layout/secondary-layout.controller.js"></script>
<script type="text/javascript" src="./secondary-layout/secondary-layout.directive.js"></script>
</body>
</html>
As you can see, I'm still using the ngSwitch and ngSwitchWhen directives to conditionally render parts of the page; but, instead of using the ngInclude directive, as I normally would have, I'm using a "component directive" alongside each ngSwitchWhen directive. Each of these "layout components" then binds a particular View to a particular Controller. Let's look at the "primary-layout" directive:
angular.module( "Demo" ).directive(
"primaryLayout",
function() {
// Return the directive configuration.
return({
controller: "PrimaryLayoutController",
link: link,
restrict: "A",
templateUrl: "primary-layout/primary-layout.htm"
});
// I bind the JavaScript events to the scope.
function link( scope, element, attributes ) {
console.log( "Primary layout directive linking." );
}
}
);
As you can see, this will "include" the View, "primary-layout/primary-layout.htm" and then bind it to the Controller, "PrimaryLayoutController". This is pretty much what I was doing with the ngInclude approach; only the "component" approach has the added benefit of exposing a linking function where I can manage the JavaScript events, on the view, that need to be piped into the scope (or the Controller for those of you who love controllerAs).
And, just quickly, let's look at the primary-layout.htm view so you can see how I am rendering a sub-component:
<h1>
Replacing ngInclude With Component Directives In AngularJS
</h1>
<p>
This is the <strong>primary</strong> layout.
<!--
NOTE: For simplicity of the demo (since we're not using URL-based routing), this
method is inherited from the app-controller.
-->
<a ng-click="showSecondaryLayout()">Show secondary layout</a>
</p>
<!--
From this component (primary layout), we're going to "include" another component
to look at how nested layouts can be achieved without ngInclude.
-->
<div friends-list>
<!-- Content provided by component directive. -->
</div>
As you can see, this component - PrimaryLayout - turns around and "includes" another component, FriendsList. And, the FriendsList "component" does exactly the same thing as the PrimaryLayout component - it binds a given view to a given controller:
angular.module( "Demo" ).directive(
"friendsList",
function() {
// Return the directive configuration.
return({
controller: "FriendsListController",
link: link,
restrict: "A",
templateUrl: "friends-list/friends-list.htm"
});
// I bind the JavaScript events to the scope.
function link( scope, element, attributes ) {
console.log( "Friends list directive linking." );
}
}
);
It took me a little while to wrap my head around this; but, after seeing it come to life, I have to say that I think it's actually kind of cool. If I'm really behing honest with myself, most of the views that I included with ngInclude also required an additional directive to perform actions like keyboard-shortcuts and deferred linking. As such, the fact that "component directives" combine a View, a Controller, and a Linking function makes them very appealing.
AngularJS! After 3 years, I'm still learning stuff! And still so much more to learn. I love this framework and this community.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben,
Nice post (as always). I've been using this approach for quite while, and it seems to work fine for me.
I've got a question, though; In your primaryLayout directive you're pointing to PrimaryLayoutController which I suppose you are registering in primary-layout.controller.js file. Why don't you use a function in the directive closure and point the directive controller to that function? It seems to me that the whole logic of the primaryLayout directive should be encapsulated within the directive definition.
Is there a use case where you would like to reuse the PrimaryLayoutController ?
Cheers
Pavlos
@Pavlos,
First, yes, the Controller is defined in the primary-layout.controller.js. And you ask a good question about organization. For starters, I'm used to putting controllers in their own file. Typically, those controllers are included via the ng-controller directive. But, in this case, obviously, they are being used by the directive.
I think I am keeping them in a separate file to stay consistent with how I did it before. Then, the directives are very focused on two things: Compiling and Linking. And controllers are very focused on Controllers.
I wish I knew more about testing; but, I don't yet. That said, I believe that keeping them in separate files makes the individual behaviors more testable. For example, I can now [theoretically] test the Controller independently of the Directive.
But, again, I'm still learning this kind of approach. Until now, I've only ever used ngInclude and ngController to hook views and controllers together.
@Ben
I believe that encapsulating controllers within directives is an approach that will make transition to future versions of angular (and future specs i.e. web components) less painful. Considering that web components is upon us, and that angular 2.0 will deprecate controllers and scope, it's safer to think of directives as "self-contained" components.
With regards to testing, what benefits does testing the controller independently of the Directive can bring? I think you can still test the Directive by getting a reference to it's controller (el.controller(name) according to the docs https://code.angularjs.org/1.2.18/docs/api/ng/function/angular.element#jquery-jqlite-extras)
@Pavlos,
I don't think I'm too worried as to whether or not multiple files makes things less self-contained. After all, my HTML file for the component will always be a separate file anyway. Plus, I think by breaking the files apart, it will force you keep a cleaner separation of responsibilities in your mind.
Now, I'm not saying they should be in totally different places in the file structure. As I've been reading more about AngularJS "best practices", I do really like the idea of organizing things based on feature. So, while I have a Controller file, a Directive file, and a Template file, they would all be in the same folder:
/my-component/
. . . my-component.controller.js
. . . my-component.directive.js
. . . my-component.htm
So, different files, but still one cohesive unit of code.
As far as AngularJS 2.0, as far as I know, that won't be ready for like another 18-months; and all the team members keep saying, "Just write the best 1.3 code you can now, don't worry about a migration story."
And , for testing, unfortunately, I can't really speak to that one way or another - it's a serious week point for me. My next ebook I have is on Jasmine and Unit Testing; so hopefully I can actually get better at that stuff :D
@Ben,
Sorry Ben, I think I didn't make myself clear. I totally agree with you about having multiple files, and in fact I always do it this way.
I'm worried more about how the controller function is defined component.controller.js file. I mean, do you register it using module.controller()?
I personally would expose it using the revealing module patter , for example. This way I'm not putting the controller into the angular context. What I mean is, I don't want anyone to reuse the controller ( e.g. ng-controller) just because it happens to have a piece of functionality that can suite a specific piece of view. This gives as the ability to use controllers as shared services, which is somewhat violating the SoC.
I don't know If it makes sense (btw I'm really crap in explaining ); but I would really appreciate if you could give me your thought on that.
@Pavlos,
Ah, I see what you're saying. Good point; and, in fact, up until a few weeks ago, I didn't even realize that you *could* define directive controllers using the normal Controller provider. Actually, I didn't even realize that directive controllers could use dependency-injection:
www.bennadel.com/blog/2709-directive-controllers-can-use-dependency-injection-in-angularjs.htm
It wasn't until someone pointed it out in the comments of that post, that a controller is a controller is a controller. After that, I became enamored with the idea that it could be defined using the Controller provider.
So, I guess the short answer is, I do it because it's "fun" and "new" :D
@Ben,
Cheers!
This looks great except I'm having a problem where each directive is loading its template regardless of whether it's showing or not. If you're wanting to switch between a number of views, then this slows the rendering down significantly.
So the solution to my issue is easy with 1.3, use $templateRequest, a handy caching template getting and then compile it in a link function. Becomes as snappy as using ng-include again.
Cool, again a very nice explanation.
I'm in the same transition as you were, replacing includes with directives.
One clear drawback of includes is you can't pass parameters to them, with directives you can.
The advantage of includes is that they're simple to understand, the DISadvantage of directives is that they are NOT simple to understand.
Directives are key to unlocking the power of Angular, however It's really hard to get a good grasp of it and even then it's so complex that I'm never able to code anything off the top of my head.
I really expect this to improve a lot in Angular 2.0.
Ben, excellent post as always. Question, is there a way to handle the back-button page navigation after a ng-switch execution? In other words; how do you prevent the navigation from going to a back page and make it go back to the previous switched view?
Fir the first 50% of your video I was thinking "OK this is great but I should still use ngInclude for the elaborate screen that I'm making now"....
...until I realized that you are exactly right, directives are much cleaner.
I removed all the ngIncludes btw.
Since the post
www.bennadel.com/blog/2738-using-ngrepeat-with-nginclude-hurts-performance-in-angularjs.htm
which you refer to was about performance, did you repeat your measurements to see how things change? And in such case, could you elaborate on the outcome?
I have a similar problem. I use a directive named nxt-include which works like ng-include
app.controls.directive('nxtInclude', ['$compile', '$controller', function($compile, $controller) {
return {
restrict: 'A',
scope: { init: '=' },
templateUrl: function(elem, attrs) { return attrs.nxtInclude; },
link: function (scope, elem, attrs) {
if (scope.init) {
for (var key in scope.init) {
scope[key] = scope.init[key];
}
}
elem.data('$Controller', $controller(attrs.ctrl, { $scope: scope }));
}
};
}]); and this
<div nxt-include="modules/laboratorySettings/devices/lsTestsPanel.html" ctrl="nxtLsTestsPanelCtrl"></div> - lsTestsPanel.html - load a table
In lsTestsPanel.html i use another directive actions="actions" which load me some table actions like reload, cart, etc, but the value of actions is null because the directive load the value before the controller initialize it .
if i use ng-include instead of directive all this process works fine.
What is the problem in first situation?