ChatGPT解决这个技术问题 Extra ChatGPT

How to dynamically change header based on AngularJS partial view?

I am using ng-view to include AngularJS partial views, and I want to update the page title and h1 header tags based on the included view. These are out of scope of the partial view controllers though, and so I can't figure out how to bind them to data set in the controllers.

If it was ASP.NET MVC you could use @ViewBag to do this, but I don't know the equivalent in AngularJS. I've searched about shared services, events etc but still can't get it working. Any way to modify my example so it works would be much appreciated.

My HTML:

<html data-ng-app="myModule">
<head>
<!-- include js files -->
<title><!-- should changed when ng-view changes --></title>
</head>
<body>
<h1><!-- should changed when ng-view changes --></h1>

<div data-ng-view></div>

</body>
</html>

My JavaScript:

var myModule = angular.module('myModule', []);
myModule.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/test1', {templateUrl: 'test1.html', controller: Test1Ctrl}).
        when('/test2', {templateUrl: 'test2.html', controller: Test2Ctrl}).
        otherwise({redirectTo: '/test1'});
}]);

function Test1Ctrl($scope, $http) { $scope.header = "Test 1"; 
                                  /* ^ how can I put this in title and h1 */ }
function Test2Ctrl($scope, $http) { $scope.header = "Test 2"; }
This comment maybe late but I want to add. cssfacts.com/simple-dynamic-meta-tags-in-angularjs This can be useful for set dynamic metas. You will just change your $rootScope meta variable.

t
tronman

I just discovered a nice way to set your page title if you're using routing:

JavaScript:

var myApp = angular.module('myApp', ['ngResource'])

myApp.config(
    ['$routeProvider', function($routeProvider) {
        $routeProvider.when('/', {
            title: 'Home',
            templateUrl: '/Assets/Views/Home.html',
            controller: 'HomeController'
        });
        $routeProvider.when('/Product/:id', {
            title: 'Product',
            templateUrl: '/Assets/Views/Product.html',
            controller: 'ProductController'
        });
    }]);

myApp.run(['$rootScope', function($rootScope) {
    $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
        $rootScope.title = current.$$route.title;
    });
}]);

HTML:

<!DOCTYPE html>
<html ng-app="myApp">
<head>
    <title ng-bind="'myApp &mdash; ' + title">myApp</title>
...

Edit: using the ng-bind attribute instead of curlies {{}} so they don't show on load


right, but your example doesn't show how to change the title on $routeChangeSuccess parameterized by $scope variables, which @tosh's example using a Page service does. So you can set title = "Blog" but not title = '{{"Blog post " + post.title}}'.
@felix you can access title like current.title also
$rootScope.title = current.$route.title; without doble $$
I just upgraded my Angular version several versions (1.0.5 to 1.2.7) and this broke me for in my code. I was using current.$route in the old code and it was working. With the upgrade, the double $ on route is needed. current.$$route
In the answser when can see '/Product/:id'. Is there any way to have the :id value with this method ? I tried title: function(params){return params.id;} but doesn't work... Maybe using resolve ?
T
Tosh

You could define controller at the <html> level.

 <html ng-app="app" ng-controller="titleCtrl">
   <head>
     <title>{{ Page.title() }}</title>
 ...

You create service: Page and modify from controllers.

myModule.factory('Page', function() {
   var title = 'default';
   return {
     title: function() { return title; },
     setTitle: function(newTitle) { title = newTitle }
   };
});

Inject Page and Call 'Page.setTitle()' from controllers.

Here is the concrete example: http://plnkr.co/edit/0e7T6l


uhmm... I'm not sure if placing a service straight into the $scope is considered nice in the AngularJS architecture. Maybe it could be better to put in $scope a Controller function, and then let this function query the service.
This example was terrific. I have one followup though, on initial load you can see the {{ Page.title() }} text in the title (very quickly). I don't think you can use ng-cloak since it's not in the body. Any suggestions to avoid this?
@ArthurFrankel Just use ng-bind (e.g. ng-bind="Page.title()")
or we can specify controller in title tag, no need for global controller on html header: {{ Page.title() }}
I personally prefer to set the title on the $rootScope instead of creating an additional controller.
b
broc.seib

Note that you can also set the title directly with javascript, i.e.,

$window.document.title = someTitleYouCreated;

This does not have data binding, but it suffices when putting ng-app in the <html> tag is problematic. (For example, using JSP templates where <head> is defined in exactly one place, yet you have more than one app.)


This was the only way to get it to work on Internet Explorer for me, the other methods worked on other browsers though
As Maarten mentioned this is the only approach that works in ie7 and ie8
Amazing how people can't step back and see how easily this thing can be done without scopes and factories
Unbelievable. This was far simpler than all the shenanigans others were mentioning. Thanks!
Using plain 'window' is fine -- that's directly acting on the DOM. '$window' is an angular thing, and you have to inject it to use it. Either way will work.
T
Tisho

Declaring ng-app on the html element provides root scope for both the head and body.

Therefore in your controller inject $rootScope and set a header property on this:

function Test1Ctrl($rootScope, $scope, $http) { $rootScope.header = "Test 1"; }

function Test2Ctrl($rootScope, $scope, $http) { $rootScope.header = "Test 2"; }

and in your page:

<title ng-bind="header"></title>

best answer on my opinion. To have a controller on ng-app level as described in accepted answer is useless in this case.
i love how lightweight this solution is, and it avoids using $$ properties
The accepted answer adds unnecessary complication and risks. This version makes it as simple as setting a variable.
If you're dead set on using $rootScope I'd at least extract this out to a service so you don't have $rootScope in your controller.
I want to use this solution but I am curious what the advantages of using it are over document.title = "App";
M
Martin Atkins

The module angularjs-viewhead shows a mechanism to set the title on a per-view basis using only a custom directive.

It can either be applied to an existing view element whose content is already the view title:

<h2 view-title>About This Site</h2>

...or it can be used as a standalone element, in which case the element will be invisible in the rendered document and will only be used to set the view title:

<view-title>About This Site</view-title>

The content of this directive is made available in the root scope as viewTitle, so it can be used on the title element just like any other variable:

<title ng-bind-template="{{viewTitle}} - My Site">My Site</title>

It can also be used in any other spot that can "see" the root scope. For example:

<h1>{{viewTitle}}</h1>

This solution allows the title to be set via the same mechanism that is used to control the rest of the presentation: AngularJS templates. This avoids the need to clutter controllers with this presentational logic. The controller needs to make available any data that will be used to inform the title, but the template makes the final determination on how to present it, and can use expression interpolation and filters to bind to scope data as normal.

(Disclaimer: I am the author of this module, but I'm referencing it here only in the hope that it will help someone else to solve this problem.)


Can't believe this solution hasn't been upvoted more. Most of the other ones are really bad design choices.
Agreed, this should be the top solution. I like this a lot better than declaring a controller at the page level for setting the title. FYI: using this with Angular v1.3.2 and angular-route-segment v1.3.3 and it's working like a charm.
I endorse this solution ;)
I wrote a bit more about angularjs-viewhead and another related idea here on my blog: apparently.me.uk/angularjs-view-specific-sidebars
If reusing the same view at top-level and at sub-level view, one can still use view-title with a ng-if, e.g.:

Details for {{...}}

Details for {{...}}

M
Mr Hash

Here is an adapted solution that works for me which doesn't require injection of $rootScope into controllers for setting resource specific page titles.

In the master template:

<html data-ng-app="myApp">
    <head>
    <title data-ng-bind="page.title"></title>
    ...

In the routing config:

$routeProvider.when('/products', {
    title: 'Products',
    templateUrl: '/partials/products.list.html',
    controller: 'ProductsController'
});

$routeProvider.when('/products/:id', {
    templateUrl: '/partials/products.detail.html',
    controller: 'ProductController'
});

And in the run block:

myApp.run(['$rootScope', function($rootScope) {
    $rootScope.page = {
        setTitle: function(title) {
            this.title = title + ' | Site Name';
        }
    }

    $rootScope.$on('$routeChangeSuccess', function(event, current, previous) {
        $rootScope.page.setTitle(current.$$route.title || 'Default Title');
    });
}]);

Finally in the controller:

function ProductController($scope) {
    //Load product or use resolve in routing
    $scope.page.setTitle($scope.product.name);
}

The title set in ProductController ($scope.page.setTitle) is being overridden by $rootScope.$on('$routeChangeSuccess'. Setting a default title in $rootScope.$on('$routeChangeStart' is safer in this respect.
@mr-hash: here is small adjustment I suggest, perfect for existing angular projects with many routes, but without titles. It'll generate title from controller's name, if no title is defined on the route: $rootScope.page.setTitle(current.$$route.title || current.$$route.controller.replace('Ctrl', ''));
remember to sanitize output like this: this.title = title.replace('<', '&lt;').replace('>', '&gt;').replace(' & ', ' &amp; ') + ' | Site Name';
I got undefined error so I changed the last bit to: $rootScope.page.title = current.$$route ? current.$$route.title + ' | Site Name' : 'Site Name';
D
Deminetix

jkoreska's solution is perfect if you know the titles before hand, but you may need to set the title based on data you get from a resource etc.

My solution requires a single service. Since the rootScope is the base of all DOM elements, we don't need to put a controller on the html element like someone mentioned

Page.js

app.service('Page', function($rootScope){
    return {
        setTitle: function(title){
            $rootScope.title = title;
        }
    }
});

index.jade

doctype html
html(ng-app='app')
head
    title(ng-bind='title')
// ...

All controllers that need to change title

app.controller('SomeController', function(Page){
    Page.setTitle("Some Title");
});

small problem, when you refresh a page, in your tab name you see '{{ title }}' and after page was rendered, you see 'Some Title' only. solution with factory does not have that behavior
instead {{title}} use ng-bind='title'
Agree with @Faradox... using ng-bind prevents the pre-interpolated syntax from displaying before the title actually evaluates. +100
A
Alex Soroka

A clean way that allow dynamically setting title or meta description. In example I use ui-router but you can use ngRoute in same way.

var myApp = angular.module('myApp', ['ui.router'])

myApp.config(
    ['$stateProvider', function($stateProvider) {
        $stateProvider.state('product', {
            url: '/product/{id}',
            templateUrl: 'views/product.html',
            resolve: {
                meta: ['$rootScope', '$stateParams', function ($rootScope, $stateParams) {
                    var title = "Product " + $stateParams.id,
                        description = "Product " + $stateParams.id;
                    $rootScope.meta = {title: title, description: description};
                }]

                // Or using server side title and description
                meta: ['$rootScope', '$stateParams', '$http', function ($rootScope, $stateParams, $http) {
                    return $http({method: 'GET', url: 'api/product/ + $stateParams.id'})
                        .then (function (product) {
                            $rootScope.meta = {title: product.title, description: product.description};
                        });
                }]

            }
            controller: 'ProductController'
        });
    }]);

HTML:

<!DOCTYPE html>
<html ng-app="myApp">
<head>
    <title ng-bind="meta.title + ' | My App'">myApp</title>
...

N
Nathan Kot

Alternatively, if you are using ui-router:

index.html

<!DOCTYPE html>
<html ng-app="myApp">
<head>
    <title ng-bind="$state.current.data.title || 'App'">App</title>

Routing

$stateProvider
  .state('home', {
      url: '/',
      templateUrl: 'views/home.html',
      data: {
        title: 'Welcome Home.'
      }
  }

I can't get this to work.. I've got ui-router updating URL and content based on my state and I get no errors or warnings, but I can't seem to access any part of the state config object through $state.current.[...]. What version of ui-router did you use to do this?
My "Runtime Config" edit to the answer solves the problem I mentioned in my comment above. :) I'm open to ideas if there's a better way to do this though.
this does not work for me and 'title' is not found in the API docs - is this still supported?
E
Ehsan88

Custom event-based solution

Here is another approach that hasn't been mentioned by the others here (as of this writing).

You can use custom events like so:

// your index.html template
<html ng-app="app">
<head>
<title ng-bind="pageTitle">My App</title>

// your main app controller that is declared on the <html> element
app.controller('AppController', function($scope) {
    $scope.$on('title-updated', function(newTitle) {
        $scope.pageTitle = newTitle;
    });
});

// some controller somewhere deep inside your app
mySubmodule.controller('SomeController', function($scope, dynamicService) {
    $scope.$emit('title-updated', dynamicService.title);
});

This approach has the advantage of not requiring extra services to be written and then injected into every controller that needs to set the title, and also doesn't (ab)use the $rootScope. It also allows you to set a dynamic title (as in the code example), which is not possible using custom data attributes on the router's config object (as far as I know at least).


J
JeremyWeir

For scenarios that you don't have an ngApp that contains the title tag, just inject a service to controllers that need to set the window title.

var app = angular.module('MyApp', []);

app.controller('MyController', function($scope, SomeService, Title){
    var serviceData = SomeService.get();
    Title.set("Title of the page about " + serviceData.firstname);
});

app.factory('SomeService', function ($window) {
    return {
        get: function(){
            return { firstname : "Joe" };
        }
    };
});

app.factory('Title', function ($window) {
    return {
        set: function(val){
            $window.document.title = val;
        }
    };
});

Working example... http://jsfiddle.net/8m379/1/


A
Ashish

If you don't have control over title element (like asp.net web form) here is some thing you can use

var app = angular.module("myApp")
    .config(function ($routeProvider) {
                $routeProvider.when('/', {
                                            title: 'My Page Title',
                                            controller: 'MyController',
                                            templateUrl: 'view/myView.html'
                                        })
                            .otherwise({ redirectTo: '/' });
    })
    .run(function ($rootScope) {
        $rootScope.$on("$routeChangeSuccess", function (event, currentRoute, previousRoute) {
            document.title = currentRoute.title;
        });
    });

u
user1338062

Simple and dirty way using $rootScope:

<html ng-app="project">
<head>
<title ng-bind="title">Placeholder title</title>

In your controllers, when you have the data necessary to create the title, do:

$rootScope.title = 'Page X'

M
MikeyB

None of these answers seemed intuitive enough, so I created a small directive to do this. This way allows you to declare the title in the page, where one would normally do it, and allows it to be dynamic as well.

angular.module('myModule').directive('pageTitle', function() {
    return {
        restrict: 'EA',
        link: function($scope, $element) {
            var el = $element[0];
            el.hidden = true; // So the text not actually visible on the page

            var text = function() {
                return el.innerHTML;
            };
            var setTitle = function(title) {
                document.title = title;
            };
            $scope.$watch(text, setTitle);
        }
    };
});

You'll need to of course change the module name to match yours.

To use it, just throw this in your view, much as you would do for a regular <title> tag:

<page-title>{{titleText}}</page-title>

You can also just include plain text if you don't need it to by dynamic:

<page-title>Subpage X</page-title>

Alternatively, you can use an attribute, to make it more IE-friendly:

<div page-title>Title: {{titleText}}</div>

You can put whatever text you want in the tag of course, including Angular code. In this example, it will look for $scope.titleText in whichever controller the custom-title tag is currently in.

Just make sure you don't have multiple page-title tags on your page, or they'll clobber each other.

Plunker example here http://plnkr.co/edit/nK63te7BSbCxLeZ2ADHV. You'll have to download the zip and run it locally in order to see the title change.


I came up with something similar. By far the most intuitive to use, and doesn't require putting a controller on html. In my directive I also inject an optional pageTitlePrefix constant.
t
the_mishra

Simplistic solution for angular-ui-router :

HTML :

<html ng-app="myApp">
  <head>
     <title ng-bind="title"></title>
     .....
     .....  
  </head>
</html>

App.js > myApp.config block

$stateProvider
    .state("home", {
        title: "My app title this will be binded in html title",
        url: "/home",
        templateUrl: "/home.html",
        controller: "homeCtrl"
    })

App.js>myApp.run

myApp.run(['$rootScope','$state', function($rootScope,$state) {
   $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
    $rootScope.title = $state.current.title;
    console.log($state);
   });
}]);

C
Community

Here's a different way to do title changes. Maybe not as scalable as a factory function (which could conceivably handle unlimited pages) but it was easier for me to understand:

In my index.html I started like this:

    <!DOCTYPE html>
      <html ng-app="app">
        <head>
          <title ng-bind-template="{{title}}">Generic Title That You'll Never See</title>

Then I made a partial called "nav.html":

<div ng-init="$root.title = 'Welcome'">
    <ul class="unstyled">
        <li><a href="#/login" ng-click="$root.title = 'Login'">Login</a></li>
        <li><a href="#/home" ng-click="$root.title = 'Home'">Home</a></li>
        <li><a href="#/admin" ng-click="$root.title = 'Admin'">Admin</a></li>
        <li><a href="#/critters" ng-click="$root.title = 'Crispy'">Critters</a></li>
    </ul>
</div>

Then I went back to "index.html" and added the nav.html using ng-include and the ng-view for my partials:

<body class="ng-cloak" ng-controller="MainCtrl">
    <div ng-include="'partials/nav.html'"></div>
    <div>
        <div ng-view></div>
    </div>

Notice that ng-cloak? It doesn't have anything to do with this answer but it hides the page until it's done loading, a nice touch :) Learn how here: Angularjs - ng-cloak/ng-show elements blink

Here's the basic module. I put it in a file called "app.js":

(function () {
    'use strict';
    var app = angular.module("app", ["ngResource"]);

    app.config(function ($routeProvider) {
        // configure routes
        $routeProvider.when("/", {
            templateUrl: "partials/home.html",
            controller:"MainCtrl"
        })
            .when("/home", {
            templateUrl: "partials/home.html",
            controller:"MainCtrl"
        })
            .when("/login", {
            templateUrl:"partials/login.html",
            controller:"LoginCtrl"
        })
            .when("/admin", {
            templateUrl:"partials/admin.html",
            controller:"AdminCtrl"
        })
            .when("/critters", {
            templateUrl:"partials/critters.html",
            controller:"CritterCtrl"
        })
            .when("/critters/:id", {
            templateUrl:"partials/critter-detail.html",
            controller:"CritterDetailCtrl"
        })
            .otherwise({redirectTo:"/home"});
    });

}());

If you look toward the end of the module, you'll see that I have a critter-detail page based on :id. It's a partial that is used from the Crispy Critters page. [Corny, I know - maybe it's a site that celebrates all kinds of chicken nuggets ;) Anyway, you could update the title when a user clicks on any link, so in my main Crispy Critters page that leads to the critter-detail page, that's where the $root.title update would go, just like you saw in the nav.html above:

<a href="#/critters/1" ng-click="$root.title = 'Critter 1'">Critter 1</a>
<a href="#/critters/2" ng-click="$root.title = 'Critter 2'">Critter 2</a>
<a href="#/critters/3" ng-click="$root.title = 'Critter 3'">Critter 3</a>

Sorry so windy but I prefer a post that gives enough detail to get it up and running. Note that the example page in the AngularJS docs is out of date and shows a 0.9 version of ng-bind-template. You can see that it's not that much different.

Afterthought: you know this but it's here for anyone else; at the bottom of the index.html, one must include the app.js with the module:

        <!-- APP -->
        <script type="text/javascript" src="js/app.js"></script>
    </body>
</html>

My opinion, don't use this. You are mixing data(information) in views(presentation). Later on it will be very difficult to find title sources scattered all over your HTML links that may be present at various places in the view..
Because the title only gets updated upon actually clicking a link, this doesn't set the title correctly when the user first lands on a page, or when the user refreshes.
T
Tom Söderlund

When I had to solve this, I couldn't place the ng-app on the page's html tag, so I solved it with a service:

angular.module('myapp.common').factory('pageInfo', function ($document) {

    // Public API
    return {
        // Set page <title> tag. Both parameters are optional.
        setTitle: function (title, hideTextLogo) {
            var defaultTitle = "My App - and my app's cool tagline";
            var newTitle = (title ? title : defaultTitle) + (hideTextLogo ? '' : ' - My App')
            $document[0].title = newTitle;
        }
    };

});

T
Thomas

Custom Event Based solution inspired from Michael Bromley

I wasn't able to make it work with $scope, so I tried with rootScope, maybe a bit more dirty... (especially if you make a refresh on the page that do not register the event)

But I really like the idea of how things are loosely coupled.

I'm using angularjs 1.6.9

index.run.js

angular
.module('myApp')
.run(runBlock);

function runBlock($rootScope, ...)
{
  $rootScope.$on('title-updated', function(event, newTitle) {
    $rootScope.pageTitle = 'MyApp | ' + newTitle;
  });
}

anyController.controller.js

angular
.module('myApp')
.controller('MainController', MainController);

function MainController($rootScope, ...)
{
  //simple way :
  $rootScope.$emit('title-updated', 'my new title');

  // with data from rest call
  TroncQueteurResource.get({id:tronc_queteur_id}).$promise.then(function(tronc_queteur){
  vm.current.tronc_queteur = tronc_queteur;

  $rootScope.$emit('title-updated', moment().format('YYYY-MM-DD') + ' - Tronc '+vm.current.tronc_queteur.id+' - ' +
                                             vm.current.tronc_queteur.point_quete.name + ' - '+
                                             vm.current.tronc_queteur.queteur.first_name +' '+vm.current.tronc_queteur.queteur.last_name
  );
 });

 ....}

index.html

<!doctype html>
<html ng-app="myApp">
  <head>
    <meta charset="utf-8">
    <title ng-bind="pageTitle">My App</title>

It's working for me :)


K
Kode

While others may have better methods, I was able to use $rootScope in my controllers, as each of my views/templates has a distinct controller. You will need to inject the $rootScope in each controller. While this may not be ideal, it is functioning for me, so I thought I should pass it along. If you inspect the page, it adds the ng-binding to the title tag.

Example Controller:

myapp.controller('loginPage', ['$scope', '$rootScope', function ($scope, $rootScope) {

// Dynamic Page Title and Description
$rootScope.pageTitle = 'Login to Vote';
$rootScope.pageDescription = 'This page requires you to login';
}]);

Example Index.html header:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="{{pageDescription}}">
<meta name="author" content="">
<link rel="shortcut icon" href="../../assets/ico/favicon.ico">
<base href="/">
<title>{{pageTitle}}</title>

You can also set the pageTitle and pageDescription to dynamic values, such as returning data from a REST call:

    $scope.article = restCallSingleArticle.get({ articleID: $routeParams.articleID }, function() {
    // Dynamic Page Title and Description
    $rootScope.pageTitle = $scope.article.articletitle;
    $rootScope.pageDescription = $scope.article.articledescription;
});

Again, others may have better ideas on how to approach this, but since I am using a pre-rendering, my needs are being met.


C
Community

Thanks to tosh shimayama for his solution.
I thought it was not so clean to put a service straight into the $scope, so here's my slight variation on that: http://plnkr.co/edit/QJbuZZnZEDOBcYrJXWWs

The controller (that in original answer seemed to me a little bit too dumb) creates an ActionBar object, and this one is stuffed into $scope. The object is responsible for actually querying the service. It also hides from the $scope the call to set the template URL, which instead is available to other controllers to set the URL.


C
Community

Mr Hash had the best answer so far, but the solution below makes it ideal (for me) by adding the following benefits:

Adds no watches, which can slow things down

Actually automates what I might have done in the controller, yet

Still gives me access from the controller if I still want it.

No extra injecting

In the router:

  .when '/proposals',
    title: 'Proposals',
    templateUrl: 'proposals/index.html'
    controller: 'ProposalListCtrl'
    resolve:
      pageTitle: [ '$rootScope', '$route', ($rootScope, $route) ->
        $rootScope.page.setTitle($route.current.params.filter + ' ' + $route.current.title)
      ]

In the run block:

.run(['$rootScope', ($rootScope) ->
  $rootScope.page =
    prefix: ''
    body: ' | ' + 'Online Group Consensus Tool'
    brand: ' | ' + 'Spokenvote'
    setTitle: (prefix, body) ->
      @prefix = if prefix then ' ' + prefix.charAt(0).toUpperCase() + prefix.substring(1) else @prifix
      @body = if body then ' | ' + body.charAt(0).toUpperCase() + body.substring(1) else @body
      @title = @prefix + @body + @brand
])

g
geolyth

The better and dynamic solution I have found is to use $watch to trace the variable changes and then update the title.