103

I am migrating my AngularJS based app to use ui-router instead of the built in routing. I have it configured as shown below

.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
$stateProvider
    .state('home', {
        url: '/home',
        templateUrl : 'views/home.html',
        data : { pageTitle: 'Home' }

    })
    .state('about', {
        url: '/about',
        templateUrl : 'views/about.html',
        data : { pageTitle: 'About' }
    })
     });

How can I use the pageTitle variable to dynamically set the title of the page? Using the built in routing, I could do

$rootScope.$on("$routeChangeSuccess", function(currentRoute, previousRoute){
    $rootScope.pageTitle = $route.current.data.pageTitle;
  });

and then bind the variable in HTML as shown below

<title ng-bind="$root.pageTitle"></title>

Is there a similar event that I can hook into using ui-router? I noticed that there are 'onEnter' and 'onExit' functions but they seem to be tied to each state and will require me to repeat code to set the $rootScope variable for each state.

15 Answers15

109

Use $stateChangeSuccess.

You can put it in a directive:

app.directive('updateTitle', ['$rootScope', '$timeout',
  function($rootScope, $timeout) {
    return {
      link: function(scope, element) {

        var listener = function(event, toState) {

          var title = 'Default Title';
          if (toState.data && toState.data.pageTitle) title = toState.data.pageTitle;

          $timeout(function() {
            element.text(title);
          }, 0, false);
        };

        $rootScope.$on('$stateChangeSuccess', listener);
      }
    };
  }
]);

And:

<title update-title></title>

Demo: http://run.plnkr.co/8tqvzlCw62Tl7t4j/#/home

Code: http://plnkr.co/edit/XO6RyBPURQFPodoFdYgX?p=preview

Even with $stateChangeSuccess the $timeout has been needed for the history to be correct, at least when I've tested myself.


Edit: Nov 24, 2014 - Declarative approach:

app.directive('title', ['$rootScope', '$timeout',
  function($rootScope, $timeout) {
    return {
      link: function() {

        var listener = function(event, toState) {

          $timeout(function() {
            $rootScope.title = (toState.data && toState.data.pageTitle) 
            ? toState.data.pageTitle 
            : 'Default title';
          });
        };

        $rootScope.$on('$stateChangeSuccess', listener);
      }
    };
  }
]);

And:

<title>{{title}}</title>

Demo: http://run.plnkr.co/d4s3qBikieq8egX7/#/credits

Code: http://plnkr.co/edit/NpzQsxYGofswWQUBGthR?p=preview

Timothy
  • 1,198
  • 3
  • 10
  • 30
tasseKATT
  • 38,470
  • 8
  • 84
  • 65
  • Super great. This couldnt get any easier – Armeen Moon Aug 01 '14 at 03:25
  • This won't work if you load the state through its full URL because the `$stateChangeStart` event doesn't fire in that case. – M.K. Safi Aug 23 '14 at 14:25
  • 3
    This example also doesn't work properly with history (at least in Chrome 37). If you go between various states, then look at your history, the history item's title will be the previous page's value. If you go page1 -> page2 -> page3, then look at history, page2's url will be matched with page1's title. – jkjustjoshing Sep 02 '14 at 14:50
  • 2
    Actually, that's not quite accurace. The page title changes before the URL hash changes, so the browser thinks the new title is for the old page. The back button history is then 1 page off. Wrapping the `element.text(title)` call in a $timeout worked for me. Editing the original post. – jkjustjoshing Sep 02 '14 at 15:19
  • 3
    This won't work if the title needs to be dynamic based on some url paramaters. – Kushagra Gour Sep 08 '14 at 09:05
  • 10
    @KushagraGour If the title needs to be dynamic based on the $stateParams, you could use a function in `resolve` to generate it, then access the "resolved" value during the $stateChangeSuccess event with: `$state.$current.locals.resolve.$$values.NAME_OF_RESOLVE_FUNCTION`. – Claus Conrad Oct 17 '14 at 09:01
  • AFAIK you can use `$stateChangeSuccess` to get rid of the $timeout. If you are working with `angular-translate` that works nice with $filter as well. Above example will fire unnecessary $digest by using $timeout. If you want to use it, add 'false' as a 3rd parameter to prevent that behaviour. – Mike Grabowski Nov 11 '14 at 15:21
  • I haven't tried this in several months, I will take a look later tonight and edit accordingly. Thank you :) – tasseKATT Nov 11 '14 at 16:23
  • Just tried the latest version, but couldn't get it to work. I removed the directive and put the $timeout function in a .run function in my app initialisation. It now works! – Nick Randell Dec 11 '14 at 11:05
  • What's the point of using `$timeout` if you don't provide the milliseconds parameter? – CodyBugstein Jan 19 '15 at 12:08
  • I added a check to see if $rootscope.title was already set, if so, the title directive should not override the value, in order to retain some flexibility for setting the title from within the controller as well. – Qiong Wu Mar 27 '15 at 11:10
92

There is a another way of doing this by combining most of the answers here already. I know this is already answered but I wanted to show the way I dynamically change page titles with ui-router.

If you take a look at ui-router sample app, they use the Angular .run block to add the $state variable to $rootScope.

// It's very handy to add references to $state and $stateParams to the $rootScope
// so that you can access them from any scope within your applications.
// For example, <li ng-class="{ active: $state.includes('contacts.list') }"> 
// will set the <li> to active whenever 'contacts.list' or one of its 
// decendents is active.

.run([ '$rootScope', '$state', '$stateParams',
function ($rootScope, $state, $stateParams) {
  $rootScope.$state = $state;
  $rootScope.$stateParams = $stateParams;
}])

With this defined, you can then easily dynamically update your page title with what you have posted but modified to use the defined state:

Setup the state the same way:

.state('home', {
    url: '/home',
    templateUrl : 'views/home.html',
    data : { pageTitle: 'Home' }
})

But edit the html a bit...

<title ng-bind="$state.current.data.pageTitle"></title>

I can't say this is any better than the answers before, but it was easier for me to understand and implement.

starball
  • 20,030
  • 7
  • 43
  • 238
cwbutler
  • 1,260
  • 9
  • 11
17

The angular-ui-router-title plugin makes it easy to update the page title to a static or dynamic value based on the current state. It correctly works with browser history, too.

Stepan Riha
  • 1,736
  • 14
  • 12
  • This seems to be the best solution going forward. I have noticed several inconsistencies with the browser history using some of the other solutions on this page. –  May 11 '15 at 01:40
  • angular-ui-router-title does seems to be the best solution. Most of all it's hassle free! Thanks Stepan. – Dário May 20 '15 at 11:31
  • It's a very tiny source file. – Tyler Collier Sep 01 '15 at 03:04
15

$stateChangeSuccess is now deprecated in UI-Router 1.x and disabled by default. You'll now need to use the new $transition service.

A solution isn't too difficult once you understand how $transition works. I got some help from @troig in understanding it all. Here's what I came up with for updating the title.

Put this in your Angular 1.6 application. Note that I'm using ECMAScript 6 syntax; if you are not, you'll need e.g. to change let to var.

.run(function($transitions, $window) {
    $transitions.onSuccess({}, (transition) => {
        let title = transition.to().title;
        if (title) {
            if (title instanceof Function) {
                title = title.call(transition.to(), transition.params());
            }
            $window.document.title = title;
        }
    });

Then just add a title string to your state:

$stateProvider.state({
    name: "foo",
    url: "/foo",
    template: "<foo-widget layout='row'/>",
    title: "Foo Page""
});

That will make the words "Foo Page" show up in the title. (If a state has no title, the page title will not be updated. It would be a simple thing to update the code above to provide a default title if a state does not indicate one.)

The code also allows you to use a function for title. The this used to call the function will be the state itself, and the one argument will be the state parameters, like this example:

$stateProvider.state({
    name: "bar",
    url: "/bar/{code}",
    template: "<bar-widget code='{{code}}' layout='row'/>",
    title: function(params) {
        return `Bar Code ${params.code}`;
    }
});

For the URL path /bar/code/123 that would show "Bar Code 123" as the page title. Note that I'm using ECMAScript 6 syntax to format the string and extract params.code.

It would be nice if someone who had the time would put something like this into a directive and publish it for everyone to use.

Community
  • 1
  • 1
Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • Use `data` object for custom keys. `title` doesn't exist on `StateDeclaration` interface. – Gaui Dec 21 '17 at 14:16
5

Attaching $state to $rootscope to use anywhere in the app.

app.run(['$rootScope', '$state', '$stateParams',
    function ($rootScope,   $state,   $stateParams) {

        // It's very handy to add references to $state and $stateParams to the $rootScope
        // so that you can access them from any scope within your applications.For example,
        // <li ng-class="{ active: $state.includes('contacts.list') }"> will set the <li>
        // to active whenever 'contacts.list' or one of its decendents is active.
        $rootScope.$state = $state;
        $rootScope.$stateParams = $stateParams;
    }
  ]
)
<title ng-bind="$state.current.name + ' - ui-router'">about - ui-router</title>
simbu
  • 51
  • 1
  • 1
5

I found this way really easy:

  .state('app.staff.client', {
    url: '/client/mine',
    title: 'My Clients'})

and then in my HTML like this:

<h3>{{ $state.current.title }}</h3>
Mike Rouse
  • 1,278
  • 18
  • 34
5

Just update window.document.title:

.state('login', {
   url: '/login',
   templateUrl: "/Login",
   controller: "loginCtrl",
   onEnter: function($window){$window.document.title = "App Login"; }
})

That way 'ng-app' does not need to move up to the HTML tag and can stay on the body or lower.

getsetbro
  • 1,798
  • 1
  • 22
  • 33
3

I'm using ngMeta, which works well for not only setting page title but descriptions as well. It lets you set a specific title/description for each state, defaults for when a title/description is not specified, as well as default title suffixes (i.e., ' | MySiteName') and author value.

$stateProvider
  .state('home', {
    url: '/',
    templateUrl: 'views/home.html',
    controller: 'HomeController',
    meta: {
      'title': 'Home',
      'titleSuffix': ' | MySiteName',
      'description': 'This is my home page description lorem ipsum.'
    },
  })
drichar
  • 51
  • 6
2

You are actually really close with your first answer/question. Add your title as a data object:

.state('home', {
    url: '/home',
    templateUrl : 'views/home.html',
    data : { pageTitle: 'Home' }
})

In your index.html bind the data directly to the page title:

<title data-ng-bind="$state.current.data.pageTitle + ' - Optional text'">Failsafe text</title>
Tristan
  • 41
  • 3
1

Why not just:

$window.document.title = 'Title';

UPDATE: Full Directive Code

var DIRECTIVE = 'yourPageTitle';

yourPageTitle.$inject = ['$window'];
function yourPageTitle($window: ng.IWindowService): ng.IDirective {

    return {
        link: (scope, element, attrs) => {

            attrs.$observe(DIRECTIVE, (value: string) => {

                $window.document.title = value;
            });
        }
    }
}

directive(DIRECTIVE, yourPageTitle);

Then in every page you would just include this directive:

<section
    your-page-title="{{'somePage' | translate}}">
Martin
  • 15,820
  • 4
  • 47
  • 56
  • could potentially be very hard to find out why/how the title is changing for anyone who inherits the code base – ton.yeung Dec 06 '15 at 17:10
  • Why would that be hard to find out? This snipped should be triggered from a directive, say your-page-titile="{{ 'pageTitle' | translate }}. This directive would be included in the first element of every page. Nice and declarative clear. – Martin Dec 08 '15 at 17:32
  • oh, with the edit, i see what you mean now. what i meant was that the one liner could potentially be put anywhere, $rootscope, directive, etc... – ton.yeung Dec 08 '15 at 19:29
1

I ended up with this combination of Martin's and tasseKATT's answers - simple and without any template related stuff:

$rootScope.$on("$stateChangeSuccess", function (event, toState) {
   $timeout(function () { // Needed to ensure the title is changed *after* the url so that history entries are correct.
     $window.document.title = toState.name; 
   });
});
Rob
  • 5,353
  • 33
  • 34
  • if there isn't any template related stuff, how would a new dev know how the title was being changed without asking how it was being changed? – ton.yeung Dec 06 '15 at 16:50
  • if you use $window.document.title $timeout is useless. I'm following this hackish just to get rid of $timeout and a $digest cycle :) – Whisher Mar 02 '16 at 08:16
0

If you are using ES6, this works just fine :).

class PageTitle {
    constructor($compile, $timeout) {
        this.restrict = 'A';
        this._$compile = $compile;
        this.$timeout = $timeout;
    }

    compile(element) {
        return this.link.bind(this);
    }

    link(scope, element, attrs, controller) {
        let defaultTitle = attrs.pageTitle ? attrs.pageTitle : "My Awesome Sauce Site";
        let listener = function(event, toState) {
            let title = defaultTitle;
            if (toState.data && toState.data.title) title = toState.data.title + ' | ' + title;
            $('html head title').text(title);
        };
        scope.$on('$stateChangeStart', listener);
    }
}

export function directiveFactory($compile) {
    return new PageTitle($compile);
}

directiveFactory.injections = ['$compile', '$timeout'];

export default PageTitle;
TGarrett
  • 562
  • 4
  • 15
0

Maybe you can try this directive.

https://github.com/afeiship/angular-dynamic-title

Here is the example:

html:

<title dynamic-title>Title</title>

<a href="javascript:;" ui-sref="state1">State1 page</a>
<a href="javascript:;" ui-sref="state2">State2 page</a>

javascript:

var TestModule = angular.module('TestApp', ['ui.router','nx.widget'])
    .config(function ($stateProvider, $urlRouterProvider) {
      //
      // For any unmatched url, redirect to /state1
      $urlRouterProvider.otherwise("/state1");
      //
      // Now set up the states
      $stateProvider
        .state('state1', {
          url: "/state1",
          templateUrl: "partials/state1.html",
          data:{
            pageTitle:'State1 page title11111'
          }
        })
        .state('state2', {
          url: "/state2",
          templateUrl: "partials/state2.html",data:{
            pageTitle:'State2 page title222222'
          }
        });
    })
    .controller('MainCtrl', function ($scope) {
      console.log('initial ctrl!');
    });
Fei Zheng
  • 1
  • 4
0

For Updated UI-Router 1.0.0+ versions, (https://ui-router.github.io/guide/ng1/migrate-to-1_0)

Refer to following code

app.directive('pageTitle', [
    '$rootScope',
    '$timeout',
    '$transitions',
    function($rootScope, $timeout,$transitions) {
        return {
            restrict: 'A',
            link: function() {
                var listener = function($transitions) {
                    var default_title = "DEFAULT_TITLE";
                    $timeout(function() {
                         $rootScope.page_title = ($transitions.$to().data && $transitions.$to().data.pageTitle)
                            ? default_title + ' - ' + $transitions.$to().data.pageTitle : default_title;
                     
                        
                    });
                };
                $transitions.onSuccess({ }, listener);
            }
        }
    }
])

Add following to your index.html:

<title page-title ng-bind="page_title"></title>
Palsri
  • 442
  • 4
  • 13
0

if (abp.auth.hasPermission('Center.Category.GroupItem')) {
    $stateProvider.state('groupItems', {
        title: 'GroupItems',
        url: '/groupItems',
        templateUrl: '~/App/product/views/center/groupItem/index.cshtml'
        controller: 'app.product.views.center.groupItem.index as vm'
    });
}

<title>{{$state.current.title ? $state.current.title : 'MiniShop'}}</title>
KNS
  • 1