Complex nesting of partials and templates
Asked Answered
A

6

506

My question involves how to go about dealing with complex nesting of templates (also called partials) in an AngularJS application.

The best way to describe my situation is with an image I created:

AngularJS Page Diagram

As you can see this has the potential to be a fairly complex application with lots of nested models.

The application is single-page, so it loads an index.html that contains a div element in the DOM with the ng-view attribute.

For circle 1, You see that there is a Primary navigation that loads the appropriate templates into the ng-view. I'm doing this by passing $routeParams to the main app module. Here is an example of what's in my app:

angular.module('myApp', []).
    config(['$routeProvider', function($routeProvider) {
        $routeProvider.                     
            when("/job/:jobId/zones/:zoneId", { controller: JobDetailController, templateUrl: 'assets/job_list_app/templates/zone_edit.html' }).
            when("/job/:jobId/initial_inspection", { controller: JobDetailController, templateUrl: 'assets/job_list_app/templates/initial_inspection.html' }).
            when("/job/:jobId/zones/:zoneId/rooms/:roomId", { controller: JobDetailController, templateUrl: 'assets/job_list_app/templates/room_edit.html' })       

    }]);

In circle 2, the template that is loaded into the ng-view has an additional sub-navigation. This sub-nav then needs to load templates into the area below it - but since ng-view is already being used, I'm not sure how to go about doing this.

I know that I can include additional templates within the 1st template, but these templates are all going to be pretty complex. I would like to keep all the templates separate in order to make the application easier to update and not have a dependency on the parent template having to be loaded in order to access its children.

In circle 3, you can see things get even more complex. There is the potential that the sub-navigation templates will have a 2nd sub-navigation that will need to load its own templates as well into the area in circle 4

How does one go about structuring an AngularJS app to deal with such complex nesting of templates while keeping them all separate from one another?

Agnusago answered 12/10, 2012 at 16:49 Comment(4)
If you're still following this thread, I added a link to a new AngularUI project to address this and a third demo that is powered by subroutes without needing a directive to my answer.Henigman
what about this one? bennadel.com/blog/…Shortterm
prettiest question I've seen in a long time :)Sauternes
Would be great to add links to the alternatives directly in the question here. github.com/angular-ui/ui-router github.com/artch/angular-route-segment github.com/dotJEM/angular-routingCozza
N
172

Well, since you can currently only have one ngView directive... I use nested directive controls. This allows you to set up templating and inherit (or isolate) scopes among them. Outside of that I use ng-switch or even just ng-show to choose which controls I'm displaying based on what's coming in from $routeParams.

EDIT Here's some example pseudo-code to give you an idea of what I'm talking about. With a nested sub navigation.

Here's the main app page

<!-- primary nav -->
<a href="#/page/1">Page 1</a>
<a href="#/page/2">Page 2</a>
<a href="#/page/3">Page 3</a>

<!-- display the view -->
<div ng-view>
</div>

Directive for the sub navigation

app.directive('mySubNav', function(){
    return {
        restrict: 'E',
        scope: {
           current: '=current'
        },
        templateUrl: 'mySubNav.html',
        controller: function($scope) {
        }
    };
});

template for the sub navigation

<a href="#/page/1/sub/1">Sub Item 1</a>
<a href="#/page/1/sub/2">Sub Item 2</a>
<a href="#/page/1/sub/3">Sub Item 3</a>

template for a main page (from primary nav)

<my-sub-nav current="sub"></my-sub-nav>

<ng-switch on="sub">
  <div ng-switch-when="1">
      <my-sub-area1></my-sub-area>
  </div>
  <div ng-switch-when="2">
      <my-sub-area2></my-sub-area>
  </div>
  <div ng-switch-when="3">
      <my-sub-area3></my-sub-area>
  </div>
</ng-switch>

Controller for a main page. (from the primary nav)

app.controller('page1Ctrl', function($scope, $routeParams) {
     $scope.sub = $routeParams.sub;
});

Directive for a Sub Area

app.directive('mySubArea1', function(){
    return {
        restrict: 'E',
        templateUrl: 'mySubArea1.html',
        controller: function($scope) {
            //controller for your sub area.
        }
    };
});
Neisa answered 12/10, 2012 at 17:21 Comment(5)
I like ProLoser's solution better, we do something like that on our app and it has worked fine. The problem with blesh's solution is the controller code going into the directives. Usually the controller that that you specify in a directive is one which works closely with the directive for ex: ngModelCtrl which works closely with the input and other directives. In your case putting the controller code within a directive would be a code smell, it is actually an independent controller.Neumark
@blesh, nice ! It seems really good explained but as a good JS programmer and beginner in AngularJS I coulnt catch that well... Me and the community would really enjoy a JSFiddle link to a working sample using that approach. Diggin it around would be easy to understand ! :) ThanksBloodcurdling
I think this solution should be a first answer for any questions kind of 'nested view didn't work'. Just because it's much more close to the Angular ideology instead of using ui-router and etc. Thanks.Clavus
So the answer is directives?Horowitz
to highlight the sub-nav items you could use this: coder1.com/articles/angularjs-managing-active-nav-elements is this a good way to do it?Springclean
H
199

UPDATE: Check out AngularUI's new project to address this problem


For subsections it's as easy as leveraging strings in ng-include:

<ul id="subNav">
  <li><a ng-click="subPage='section1/subpage1.htm'">Sub Page 1</a></li>
  <li><a ng-click="subPage='section1/subpage2.htm'">Sub Page 2</a></li>
  <li><a ng-click="subPage='section1/subpage3.htm'">Sub Page 3</a></li>
</ul>
<ng-include src="subPage"></ng-include>

Or you can create an object in case you have links to sub pages all over the place:

$scope.pages = { page1: 'section1/subpage1.htm', ... };
<ul id="subNav">
  <li><a ng-click="subPage='page1'">Sub Page 1</a></li>
  <li><a ng-click="subPage='page2'">Sub Page 2</a></li>
  <li><a ng-click="subPage='page3'">Sub Page 3</a></li>
</ul>
<ng-include src="pages[subPage]"></ng-include>

Or you can even use $routeParams

$routeProvider.when('/home', ...);
$routeProvider.when('/home/:tab', ...);
$scope.params = $routeParams;
<ul id="subNav">
  <li><a href="#/home/tab1">Sub Page 1</a></li>
  <li><a href="#/home/tab2">Sub Page 2</a></li>
  <li><a href="#/home/tab3">Sub Page 3</a></li>
</ul>
<ng-include src=" '/home/' + tab + '.html' "></ng-include>

You can also put an ng-controller at the top-most level of each partial

Henigman answered 15/10, 2012 at 21:3 Comment(8)
I like your solution better. i am a newb to Angular and this seems much more understandable from how i see the web until today. who knows, may be blesh uses more of the angular framework to do it, but it seems like you nailed it with fewer lines of code in a more sensible way. thanks!Sangfroid
@ProLooser maybe you meant this link github.com/angular-ui/ui-router ...the one you pasted is brokenElbert
I'm having the same design issue as the OP. Has anyone tried AngularUI state mechanism ? I'm quite reluctant to using yet another 3rd party library and I'd rather stick with AngularJS 'way of doing things'. But on the other hand, the routing system seems to be its Achilles' heel... @PhillipKregg, what did you end up using to solve this scenario ?Latrishalatry
So with this method, do you end up sharing the scope between the parent template and the sub-template?Oversubscribe
You can specify <div ng-include="'/home/' + tab + '.html'" ng-controller="SubCtrl"></div> in order to use a separate controller/scope for the sub-template. Or just specify the ngController directive anywhere within your sub-template(s) to use a different controller for each partial.Oversubscribe
Would you choose the nested routing project or the strings + ng-include? Just curious, i'm probably just going to use the ng-include thing until the project is stable.Darton
@DerekAdair the project is fairly stable (despite version number) and in use in production in a few places. It prevents unnecessary controller reloading, and is a much better alternative to my suggested solution.Henigman
I tried the variable approach in src='' and it seems to try to load the page w/o interpolating the variable first, then will load the full page. Caused routing loops for me. I had to generate the URL in the controller completely.Flavouring
N
172

Well, since you can currently only have one ngView directive... I use nested directive controls. This allows you to set up templating and inherit (or isolate) scopes among them. Outside of that I use ng-switch or even just ng-show to choose which controls I'm displaying based on what's coming in from $routeParams.

EDIT Here's some example pseudo-code to give you an idea of what I'm talking about. With a nested sub navigation.

Here's the main app page

<!-- primary nav -->
<a href="#/page/1">Page 1</a>
<a href="#/page/2">Page 2</a>
<a href="#/page/3">Page 3</a>

<!-- display the view -->
<div ng-view>
</div>

Directive for the sub navigation

app.directive('mySubNav', function(){
    return {
        restrict: 'E',
        scope: {
           current: '=current'
        },
        templateUrl: 'mySubNav.html',
        controller: function($scope) {
        }
    };
});

template for the sub navigation

<a href="#/page/1/sub/1">Sub Item 1</a>
<a href="#/page/1/sub/2">Sub Item 2</a>
<a href="#/page/1/sub/3">Sub Item 3</a>

template for a main page (from primary nav)

<my-sub-nav current="sub"></my-sub-nav>

<ng-switch on="sub">
  <div ng-switch-when="1">
      <my-sub-area1></my-sub-area>
  </div>
  <div ng-switch-when="2">
      <my-sub-area2></my-sub-area>
  </div>
  <div ng-switch-when="3">
      <my-sub-area3></my-sub-area>
  </div>
</ng-switch>

Controller for a main page. (from the primary nav)

app.controller('page1Ctrl', function($scope, $routeParams) {
     $scope.sub = $routeParams.sub;
});

Directive for a Sub Area

app.directive('mySubArea1', function(){
    return {
        restrict: 'E',
        templateUrl: 'mySubArea1.html',
        controller: function($scope) {
            //controller for your sub area.
        }
    };
});
Neisa answered 12/10, 2012 at 17:21 Comment(5)
I like ProLoser's solution better, we do something like that on our app and it has worked fine. The problem with blesh's solution is the controller code going into the directives. Usually the controller that that you specify in a directive is one which works closely with the directive for ex: ngModelCtrl which works closely with the input and other directives. In your case putting the controller code within a directive would be a code smell, it is actually an independent controller.Neumark
@blesh, nice ! It seems really good explained but as a good JS programmer and beginner in AngularJS I coulnt catch that well... Me and the community would really enjoy a JSFiddle link to a working sample using that approach. Diggin it around would be easy to understand ! :) ThanksBloodcurdling
I think this solution should be a first answer for any questions kind of 'nested view didn't work'. Just because it's much more close to the Angular ideology instead of using ui-router and etc. Thanks.Clavus
So the answer is directives?Horowitz
to highlight the sub-nav items you could use this: coder1.com/articles/angularjs-managing-active-nav-elements is this a good way to do it?Springclean
B
27

You may checkout this library for the same purpose also:

http://angular-route-segment.com

It looks like what you are looking for, and it is much simpler to use than ui-router. From the demo site:

JS:

$routeSegmentProvider.

when('/section1',          's1.home').
when('/section1/:id',      's1.itemInfo.overview').
when('/section2',          's2').

segment('s1', {
    templateUrl: 'templates/section1.html',
    controller: MainCtrl}).
within().
    segment('home', {
        templateUrl: 'templates/section1/home.html'}).
    segment('itemInfo', {
        templateUrl: 'templates/section1/item.html',
        controller: Section1ItemCtrl,
        dependencies: ['id']}).
    within().
        segment('overview', {
            templateUrl: 'templates/section1/item/overview.html'}).

Top-level HTML:

<ul>
    <li ng-class="{active: $routeSegment.startsWith('s1')}">
        <a href="/section1">Section 1</a>
    </li>
    <li ng-class="{active: $routeSegment.startsWith('s2')}">
        <a href="/section2">Section 2</a>
    </li>
</ul>
<div id="contents" app-view-segment="0"></div>

Nested HTML:

<h4>Section 1</h4>
Section 1 contents.
<div app-view-segment="1"></div>
Behold answered 15/8, 2013 at 10:5 Comment(1)
Artem, your's is the best solution I've found to date! I like the fact that you extend angular route rather than replace it as angular ui does.Unmade
P
17

I too was struggling with nested views in Angular.

Once I got a hold of ui-router I knew I was never going back to angular default routing functionality.

Here is an example application that uses multiple levels of views nesting

app.config(function ($stateProvider, $urlRouterProvider,$httpProvider) {
// navigate to view1 view by default
$urlRouterProvider.otherwise("/view1");

$stateProvider
    .state('view1', {
        url: '/view1',
        templateUrl: 'partials/view1.html',
        controller: 'view1.MainController'
    })
    .state('view1.nestedViews', {
        url: '/view1',
        views: {
            'childView1': { templateUrl: 'partials/view1.childView1.html' , controller: 'childView1Ctrl'},
            'childView2': { templateUrl: 'partials/view1.childView2.html', controller: 'childView2Ctrl' },
            'childView3': { templateUrl: 'partials/view1.childView3.html', controller: 'childView3Ctrl' }
        }
    })

    .state('view2', {
        url: '/view2',
    })

    .state('view3', {
        url: '/view3',
    })

    .state('view4', {
        url: '/view4',
    });
});

As it can be seen there are 4 main views (view1,view2,view3,view4) and view1 has 3 child views.

Pugh answered 3/9, 2014 at 11:51 Comment(2)
What is the purpose of injecting $httpProvider? I don't see it used anywhere.Archegonium
@Archegonium app.config doesn't end in this piece of code and may be $httpProvider is used somewhere elseAcker
P
4

You may use ng-include to avoid using nested ng-views.

http://docs.angularjs.org/api/ng/directive/ngInclude
http://plnkr.co/edit/ngdoc:example-example39@snapshot?p=preview

My index page I use ng-view. Then on my sub pages which I need to have nested frames. I use ng-include. The demo shows a dropdown. I replaced mine with a link ng-click. In the function I would put $scope.template = $scope.templates[0]; or $scope.template = $scope.templates[1];

$scope.clickToSomePage= function(){
  $scope.template = $scope.templates[0];
};
Phyllous answered 27/3, 2014 at 17:19 Comment(0)
C
2

Angular ui-router supports nested views. I haven't used it yet but looks very promising.

http://angular-ui.github.io/ui-router/

Christianchristiana answered 23/4, 2014 at 15:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.