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"; }
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 — ' + title">myApp</title>
...
Edit: using the ng-bind
attribute instead of curlies {{}}
so they don't show on load
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
$rootScope
instead of creating an additional controller.
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.)
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>
document.title = "App"
;
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.)
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);
}
$rootScope.page.setTitle(current.$$route.title || current.$$route.controller.replace('Ctrl', ''));
this.title = title.replace('<', '<').replace('>', '>').replace(' & ', ' & ') + ' | Site Name';
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");
});
{{title}}
use ng-bind='title'
ng-bind
prevents the pre-interpolated syntax from displaying before the title actually evaluates. +100
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>
...
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.'
}
}
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?
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).
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/
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;
});
});
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'
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.
html
. In my directive I also inject an optional pageTitlePrefix
constant.
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);
});
}]);
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>
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;
}
};
});
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 :)
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.
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.
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
])
The better and dynamic solution I have found is to use $watch to trace the variable changes and then update the title.
Success story sharing
title = "Blog"
but nottitle = '{{"Blog post " + post.title}}'
.current.title
alsocurrent.$route
in the old code and it was working. With the upgrade, the double $ on route is needed.current.$$route
'/Product/:id'
. Is there any way to have the:id
value with this method ? I triedtitle: function(params){return params.id;}
but doesn't work... Maybe usingresolve
?