Angular UI-Router How to create a "layout" state?
Asked Answered
C

6

39

Given an HTML file like so:

<html>
<header ui-view="header"></header>
<div class="main" ui-view></div>
<footer ui-view="footer"></footer>
</html>

How would one create a layout state that fills the "header" with a header template, the footer with a footer template, and then allow child states to fill the empty ui-view?

I suppose the empty ui-view could also be named something like ui-view="main".

Catanddog answered 28/2, 2014 at 20:10 Comment(0)
P
32

One way is to make a global 'root' state. So, each and every state will be it's child. Like root.pages, root.pages.edit, root.settings, etc. Then you can provide default templates for header and footer.

Another way, different approach, that I use personally, is to use ng-include for header and footer.

<body ng-controller="AppCtrl">
  <div id="header" ng-include="'header.tpl.html'"></div>
  <div class="main_container" ui-view>
  </div>
</body>

Btw. I use seperate controller in header.tpl.html, that is a child of main AppCtrl.:

<div id="header" ng-controller="HeaderCtrl"> ....
Paris answered 1/3, 2014 at 14:29 Comment(5)
That's a really good idea. The reason why a "layout" or "root" state is useful is when inside your app, your "headers" or "footers" may change. Such as between the internal account management vs the outside presentation site. Although in this root state, is it an abstract state, and I'm guessing that it does not have a url property right? (I also like the AppCtrl, I've been using the app.run(function(){}) as a root controller.Catanddog
Yes, root state could be an abstract state. Though, abstract state should have it's URL. This can be used to prepend an url for children states. But for root state it can be kept as '/'. Here is a detailed info about abstract states in ui-router: github.com/angular-ui/ui-router/wiki/… As for controller, you can make your main App controller as the controller of that root state. It will work.Paris
I would have a Home state url being '/'. So I don't think my abstract state can have a url in this case.Catanddog
You have no idea how many pages I read concerning nested views, named views/pages, ui-router tutorials, etc... before your post made me realize that a simple ng-include would do just fine. Thanks, you effectively put a pillow between my head and the wall it was banging against.Proleg
I am currently doing what @MaximDemkin has suggested above. However it creates an abstract state of the header and footer. Any idea how to make header and footer have concrete state?Artwork
A
79

try this, practically your header and footer are static templates but you can add the controllers in case you need to add some dynamic functionality to it, the header and the footer will be included by default since the route is '', so try that out:

 app.config(['$stateProvider', function($stateProvider){
        $stateProvider
        .state('root',{
          url: '',
          abstract: true,
          views: {
            'header': {
              templateUrl: 'header.html',
              controller: 'HeaderCtrl'
            },
            'footer':{
              templateUrl: 'footer.html',
              controller: 'FooterCtrl'
            }
          }
        })
        .state('root.home', {
          url: '/',
          views: {
            'container@': {
              templateUrl: 'homePage.html'
            }
          }
        })
        .state('root.other', {
          url: '/other',
          views: {
            'container@': {
              templateUrl: 'other.html'
            }
          }
        });    

    }]);

Edit: to me the best place to set the views should be in the index.html and something like this code:

<header>
    <div ui-view="header"></div>
</header>
<div ui-view="container">
</div>
<footer id="mainFooter" ui-view="footer">
</footer>
Apoloniaapolune answered 2/6, 2014 at 15:6 Comment(11)
I can't seem to get homePage.html to render. It grabs the HTML but isn't injecting into UI View. I'm getting no errors. Thoughts?Warble
take a look to the named views in the ui-router documentation: github.com/angular-ui/ui-router/wiki/Multiple-Named-ViewsApoloniaapolune
Thanks! Helped so much. container@ had to be on the blank ui-view in the html and now it worksWarble
Could you please provide the HTML for this code too? Thanks!Snuggery
@BrunoPeres you just have to use the ui-view, it's quite simple nothing fancy! cheers!Apoloniaapolune
Why use @ in views object? I mean why 'container@' and not just 'container' ?Sidman
@Tsalikidis take a look to multiple named views here github.com/angular-ui/ui-router/wiki/Multiple-Named-ViewsApoloniaapolune
This is the best answer. It solves the problem in a clean way.Bats
This is f#cking amazing!! Thanks so much, it inspired my solution which I will post below in case it helps someone.Protero
How to link to other states? For example, I want to redirect '/other'. I tried <a ui-sref='root.other'>other</a>, but doesn't seem to work.Heliograph
This is awesome answer @jack.the.ripper you did fantastic work.......... keep doing...................Same
P
32

One way is to make a global 'root' state. So, each and every state will be it's child. Like root.pages, root.pages.edit, root.settings, etc. Then you can provide default templates for header and footer.

Another way, different approach, that I use personally, is to use ng-include for header and footer.

<body ng-controller="AppCtrl">
  <div id="header" ng-include="'header.tpl.html'"></div>
  <div class="main_container" ui-view>
  </div>
</body>

Btw. I use seperate controller in header.tpl.html, that is a child of main AppCtrl.:

<div id="header" ng-controller="HeaderCtrl"> ....
Paris answered 1/3, 2014 at 14:29 Comment(5)
That's a really good idea. The reason why a "layout" or "root" state is useful is when inside your app, your "headers" or "footers" may change. Such as between the internal account management vs the outside presentation site. Although in this root state, is it an abstract state, and I'm guessing that it does not have a url property right? (I also like the AppCtrl, I've been using the app.run(function(){}) as a root controller.Catanddog
Yes, root state could be an abstract state. Though, abstract state should have it's URL. This can be used to prepend an url for children states. But for root state it can be kept as '/'. Here is a detailed info about abstract states in ui-router: github.com/angular-ui/ui-router/wiki/… As for controller, you can make your main App controller as the controller of that root state. It will work.Paris
I would have a Home state url being '/'. So I don't think my abstract state can have a url in this case.Catanddog
You have no idea how many pages I read concerning nested views, named views/pages, ui-router tutorials, etc... before your post made me realize that a simple ng-include would do just fine. Thanks, you effectively put a pillow between my head and the wall it was banging against.Proleg
I am currently doing what @MaximDemkin has suggested above. However it creates an abstract state of the header and footer. Any idea how to make header and footer have concrete state?Artwork
D
21

There is actually a very easy way to do that.

1. Create a layout state

$stateProvider
  .state('master', {
    abstract: true,
    views: {
      layout: {
        templateUrl: '/layouts/master.html',
      }
    }
  });

2. Use unnamed view state inside the layout

<!-- layouts/master.html -->
<div ui-view></div>

3. Create a view state

$stateProvider
  .state('home', {
    url: '/',
    templateUrl: '/views/home.html',
    parent: 'master',
  });

4. Use named layout state as root state

<!-- home.html -->
<body ui-view="layout"></body>
Disobedience answered 25/11, 2015 at 9:42 Comment(5)
On a slightly related note, I'm leaning towards this approach to be able to share controller code between mobile and desktop apps, and to serve either a desktop or mobile template, in case anyone wondering similar things stumbles upon this comment.Selfconfessed
@Selfconfessed You should try to share code trough services and not trough controllers. But I think it's fine for template helpers, etc.Disobedience
Yeah, that's what I meant. Different templates for mobile and desktop, but same controllers, since 95% of the controller functionality is shared.Selfconfessed
Best answer here, using it as a templated minimizes the need for writing extra directives.Yusuk
@MK confused! would you please provide a Codepen/Fiddle?Woke
P
4

Based on the answer by Jack.the.ripper, I created this solution.

Casus: I have a slight variation that I actually want 2 layouts. One Public and one Private. In Meteor with Blaze and Iron Router there was a nice solution that you could just define which master template will be used for a certain route. This I have now been able to set up thanks to Jack!

NOTE: Code will be in Jade and Coffee and it's a Meteor + Angular project. Use http://js2.coffee/ to convert to Javascript.

# the ROUTER part
#
angular.module( 'myApp' ).config(

  ( $urlRouterProvider, $stateProvider, $locationProvider ) ->
    $locationProvider.html5Mode true

    $stateProvider
      .state( 'private',
        url: ''
        abstract: true
        views:
          'app':
            templateUrl: 'client/views/layouts/privateLayout.html'
      )

      .state( 'public',
        url: ''
        abstract: true
        views:
          'app':
            templateUrl: 'client/views/layouts/publicLayout.html'
      )

      .state( 'private.home',
        url: '/'
        views:
          "container@private":
            templateUrl: 'client/views/home/home.html'
      )

      .state( 'public.login',
        url: '/login'
        views:
          "container@public":
            templateUrl: 'client/views/session/login.html'
      )
      $urlRouterProvider.otherwise '/'
 )

This is the index file where the app view is defined that will utilise the abstract state defined in the router.

head
meta(name="viewport" content="width=device-width, initial-scale=1")
base(href="/")

body(layout="column")
   div(ui-view="app" layout="column" flex)

Then the Private Layout with its container view.

div(layout="column" flex)
  div(ng-controller="AppCtrl" layout="row" flex)

  //- some private Layout stuff happening here....
  md-content(flex layout-padding)
     div(ui-view="container" layout="column")

and finally the Public Layout with its container view

div.public-layout(layout="column" flex)
   div(ui-view="container" layout="column" flex)

With this setup I can set the login page to use the abstract public layout by stating in the view of this route that it should uses views container@public, meaning from Public view, use the view Container. In this view load the login.html.

Same goes for the home page, this one loads the container@private meaning the Container view of the Private view.

This seems to work like a charm.

Thanks very much Jack, and also the author of the answer on Angular UI Router - Nested States with multiple layouts which helped me to get towards the final solution.

Cheers

Protero answered 25/8, 2015 at 5:22 Comment(0)
D
1

Similar to jack.the.ripper's way, you could also do it the way it works for me.

app.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('root', {
      /* The Root State */
      views: {
        '': {
          abstract: true,
          templateUrl: 'master.html',
          controller: 'mainController'
        },
        'header@root': {
          templateUrl: 'header.html',
          controller: 'headerController',
        },
        'footer@root': {
          templateUrl: 'footer.html',
          controller: 'footerController'
        },
    },
   })
    .state('root.index', {
      /* The Index State */
      url: '/',
      views: {
        'content': {
          templateUrl: 'index.html',
          controller: 'indexController'
        }
      },
    .state('root.other', {
      /* The Other State */
      url: '/',
      views: {
        'content': {
          templateUrl: 'other.html',
          controller: 'otherController'
        }
      },
    });
});

In our index.html we will only have a <ui-view></ui-view>

The master.html will look like

<div ui-view="header"></div>
<div ui-view="content"></div>
<div ui-view="footer"></div>

Why I chose this approach, is I don't have to create a separate global controller, and my mainController will be the global controller.

Deeannadeeanne answered 6/12, 2016 at 12:46 Comment(0)
F
0

Instead of using routes at all for the header and footer, I would use Angular components now that they are available in 1.5x.

It is way more flexible and you don't have to deal with root.whatever or ngInclude. I go into it in more detail here: https://mcmap.net/q/408951/-master-page-concept-in-angularjs, but essentially you:

1. Create Component

(function () {
'use strict';
angular
    .module('layout')
    .component('layoutHeader', {
        templateUrl: 'layout/header.html',
        bindings: {},
        controller: Controller
    });

Controller.$inject = [];
function Controller() {
    var ctrl = this;

    initialize();

    ////////////////////

    function initialize(){

    }

}
}());

2. Add it to your index.html page or similar

<layout-header></layout-header>

3. Same for footer

<layout-footer></layout-footer>

4. The result in the body is

<layout-header></layout-header>
<main ui-view></main>
<layout-footer></layout-footer>
Floribunda answered 12/12, 2016 at 2:25 Comment(1)
components are not really the same as using ui-router. ui-router will allow you to pick templates/controller configurations based off url. Using Named Views as above will allow for swapping templates based on $stateGave

© 2022 - 2024 — McMap. All rights reserved.