Routing connects a browser URL to a screen in your application. Begin with the routing overview, then learn states, URLs, transitions, and route maps in that order.
This is the multi-page printable view of this section. Click here to print.
Routing
- 1: State-based routing in AngularTS applications
- 2: States
- 3: Routing transitions and lifecycle hooks
- 4: Route maps
- 5: URL matching and configuration in AngularTS router
1 - State-based routing in AngularTS applications
AngularTS ships a full-featured router ported from UI-Router. Unlike simple URL-based routers, it models your application as a state machine: each screen or workflow step is a named state, and navigation means transitioning from one state to another. URLs are a way to enter a state, not the primary concept.
Router naming, inference, and public-type placement follow the shared API ergonomics contract. Throughout the router documentation, a router state is one named navigation node. A route tree is the module-owned hierarchy made from router states. Machine and workflow states are execution modes and are not router states.
Key concepts
The router is built from five interlocking primitives:
- States — named nodes in the application state tree. Each state can have a URL, template, component, and resolved data.
- Views — the rendered output of a state, inserted into a
ng-viewelement in the DOM. - Resolves — asynchronous data-fetching functions that run before a state is entered.
- Transitions — the lifecycle of moving between states with hooks you can intercept.
- URL matching — an optional layer that maps browser URLs onto states and keeps them in sync.
How routing integrates with the module system
The router exposes these injectable services:
| Token | Type | Purpose |
|---|---|---|
$state | StateService | Navigate (go), inspect current state (is, includes, current) |
$transitions | TransitionsService | Register lifecycle hooks (onBefore, onStart, onSuccess, …) |
$stateRegistry | Advanced registry | Register and deregister state declarations at runtime |
For ordinary app authoring, $state is usually sufficient. $transitions and
$stateRegistry are classified as advanced runtime APIs for policy wiring,
late-bound navigation policies, and runtime diagnostics.
Setting up the router
Register your states
Call module.router(declaration) with one
RouterModuleDeclaration
or a readonly forest of declarations. Every route must have a unique name. A
single typed forest keeps registration and route-name inference in one source
of truth.
const routes = [
{
name: 'home',
url: '/home',
template: '<h1>Home</h1>',
},
{
name: 'contacts',
url: '/contacts',
templateUrl: 'contacts.html',
controller: 'ContactsCtrl',
},
{
name: 'contacts.detail',
url: '/:contactId',
resolve: {
contact: [
'$transition$',
'ContactService',
function ($transition$, ContactService) {
return ContactService.get($transition$.params().contactId);
},
],
},
templateUrl: 'contact-detail.html',
controller: 'ContactDetailCtrl',
},
] as const satisfies readonly ng.RouterModuleDeclaration[];
const app = angular.createModule('app', []).router(routes);
type AppRoutes = ng.RoutesOf<typeof routes>;
router() has no separate public input or provider type. Its generic
TDeclaration is inferred from the literal above; it is not a type users must
name. The router is part of the built-in runtime, so the module does not need an
ng.router dependency.
Add ng-view to your layout
Place ng-view where you want the active state’s template to render. An unnamed
ng-view receives the default view.
<!DOCTYPE html>
<html ng-app="app">
<body>
<nav>
<a ng-state="'home'" data-state-active="active">Home</a>
<a ng-state="'contacts'" data-state-active>Contacts</a>
</nav>
<!-- Active state template renders here -->
<div ng-view></div>
</body>
</html>
Navigate between states
Use $state.go() in controllers or services to perform programmatic navigation.
angular.getModule('app').controller('ContactsCtrl', [
'$state',
function ($state) {
this.viewContact = function (contactId) {
$state.go('contacts.detail', { contactId: contactId });
};
},
]);
The ng-view directive
ng-view is a viewport directive that renders the template and controller for
the currently active state. When a transition completes, the view is swapped in.
Multiple named views can coexist:
<div ng-view></div>
<!-- Named views -->
<div ng-view="header"></div>
<div ng-view="content"></div>
<div ng-view="sidebar"></div>
A state targets named views through its views property:
name: 'dashboard',
views: {
'header': { template: '<app-header></app-header>' },
'content': { templateUrl: 'dashboard.html', controller: 'DashboardCtrl' },
'sidebar': { component: 'DashboardSidebar' }
}
});
The ng-view directive emits $viewContentLoading before the DOM is rendered
and $viewContentLoaded after. It also supports autoscroll and onload
attributes.
Router-wide scroll and focus behavior belongs in typed module config. This keeps accessibility and scroll restoration consistent across the whole route tree without repeating settings on every route:
angular.createModule('demo', []).config({
$router: {
scroll: { top: 0 },
focus: '[data-route-focus]',
},
});
Use scroll: true or scroll: "top" to reset to the top after every successful
navigation, scroll: "preserve" to leave the current position alone, or
scroll: { selector: "#content" } to scroll a specific element into view. Use
focus: true for the default focus target ([autofocus],
[data-router-focus], main, then h1), a selector string, or
{ selector, preventScroll } for explicit focus behavior.
Executable sample: scroll-focus.html
When $router.viewTransitions is not disabled and the browser exposes
document.startViewTransition(), connected routed views commit inside the View
Transitions API so CSS can animate the outgoing and incoming route content as
one navigation. Detached, ignored, redirected, loading-resume, or explicitly
disabled transitions keep the normal synchronous view commit path.
Executable demo:
router-view-transition-test.html
Router directives
ng-state
ng-state is the router link primitive. Its value is always an AngularTS
expression: quote a static route name and use an identifier or property path
for a dynamic route name. A bare identifier is never treated as an implicit
route-name string, and ng-state does not use interpolation.
<a ng-state="'home'">Home</a>
<a ng-state="$ctrl.nextRoute">Next</a>
<a
ng-state="'contacts.detail'"
ng-state-params="{ contactId: contact.id }"
data-state-active
>
Contact {{ contact.name }}
</a>
<a
ng-state="'users.edit'"
ng-state-params="{ id: user.id }"
ng-state-opts="{ inherit: false, reload: true }"
data-state-active
data-state-exact
>
Edit user
</a>
Active link modifiers
data-state-active: track active state (and descendant states).data-state-exact: switch to exact-match behavior.
data-state-active="activeClass" supports both direct and object-based usage in
scoped contexts through inherited route context.
ng-state manages data-state-current="true|false" so CSS and scripts can read
current-link state directly.
Explore further
States
Define state hierarchies, configure resolves, and navigate with $state.go().
Transitions
Intercept the transition lifecycle with hooks for auth guards, analytics, and more.
URL matching
Configure parameterized URLs, typed parameters, hash mode, and base href.
Resolve
Fetch data asynchronously before a state is entered, with eager and lazy policies.
2 - States
A state represents a place in an AngularTS application: a page, a nested layout,
a modal, or a step in a workflow. States are declared as plain objects and
registered through module.router() before bootstrap or $stateRegistry at
runtime.
Exact state and router contracts live in TypeDoc:
Route resolves are authored through the resolve property on
StateDeclaration. The object and array helper containers are internal typing
details and are intentionally not part of the public ng namespace.
Register States
Register most states as one module-owned route tree or forest through
module.router().
angular
.createModule('demo', [])
.router({
name: 'home',
url: '/home',
component: 'homePage',
})
.router({
name: 'contacts',
url: '/contacts',
templateUrl: 'contacts/list.html',
controller: 'ContactsListCtrl',
controllerAs: 'vm',
});
Register states at runtime when a feature is loaded after bootstrap.
angular.getModule('demo').run(["$stateRegistry", ($stateRegistry) => {
$stateRegistry.register({
name: 'settings',
url: '/settings',
component: 'settingsPage',
});
}]);
Runtime registration requires the parent state to exist first. If the parent is missing, the state is queued until the parent is registered.
Nest States
Use dot notation or an explicit parent property to create a hierarchy.
angular
.createModule('demo', [])
.router({
name: 'contacts',
url: '/contacts',
template: '<div ng-view></div>',
})
.router({
name: 'contacts.list',
url: '/list',
templateUrl: 'contacts/list.html',
})
.router({
name: 'contacts.detail',
url: '/:id',
templateUrl: 'contacts/detail.html',
});
Child states inherit the parent URL prefix. A transition to contacts.detail
with { id: 42 } produces /contacts/42.
A parent state must provide a ng-view outlet where child views can render.
Use Abstract States
Abstract states cannot be activated directly. Use them to share a URL prefix, resolves, metadata, or layout with child states.
angular
.createModule('demo', [])
.router({
name: 'admin',
url: '/admin',
abstract: true,
template: '<admin-layout ng-view></admin-layout>',
resolve: {
currentUser: ['AuthService', (AuthService) => AuthService.currentUser()],
},
data: { requiresAuth: true },
})
.router({
name: 'admin.dashboard',
url: '/dashboard',
component: 'adminDashboard',
});
Navigating to admin.dashboard enters both admin and admin.dashboard.
Declare Parameters
URL parameters are parsed from path and query segments.
angular.createModule('demo', []).router({
name: 'product',
url: '/products/:category?page&sort',
component: 'productList',
});
Non-URL parameters belong in the params block.
angular.createModule('demo', []).router({
name: 'search',
url: '/search?q',
params: {
q: { value: '', squash: true },
filters: { value: null, type: 'any' },
},
component: 'searchPage',
});
Use parameter declarations for defaults, typed values, dynamic params, array params, squashing, inheritance, and raw URL values.
Resolve Data
Resolves fetch or compute data before a state renders. The router waits for required resolves before entering the state.
angular.createModule('demo', []).router({
name: 'contacts.detail',
url: '/:contactId',
resolve: {
contact: [
'$transition$',
'ContactService',
($transition$, ContactService) =>
ContactService.get($transition$.params().contactId),
],
contactHistory: [
'contact',
'HistoryService',
(contact, HistoryService) => HistoryService.forContact(contact.id),
],
},
templateUrl: 'contact-detail.html',
controller: [
'$scope',
'contact',
'contactHistory',
function ($scope, contact, contactHistory) {
$scope.contact = contact;
$scope.history = contactHistory;
},
],
});
Use array-style resolves when you need explicit tokens, dependency metadata, or resolve policies.
resolve: [
{
token: 'contact',
deps: ['$transition$', 'ContactService'],
resolveFn: ($transition$, ContactService) =>
ContactService.get($transition$.params().contactId),
policy: { when: 'EAGER', async: 'WAIT' },
},
];
Navigate
Use $state.go() for normal application navigation. It supports absolute
states, parent-relative states, sibling-relative states, params, and transition
options.
$state.go('contacts.detail', { contactId: 42 });
$state.go('^');
$state.go('^.list');
$state.go('.detail', { contactId: 42 });
$state.go('home', {}, { location: 'replace', reload: true });
$state.go() returns a TransitionPromise. When navigation starts immediately,
the active Transition is available as .transition; lazy route loading may
defer creation of that transition until after the promise has been returned.
Inspect And Link
Generate links without navigating:
const url = $state.href('contacts.detail', { contactId: 42 });
const absUrl = $state.href(
'contacts.detail',
{ contactId: 42 },
{ absolute: true },
);
Check active states:
$state.matches('contacts.detail', undefined, { exact: true });
$state.matches('contacts.detail', { contactId: 42 }, { exact: true });
$state.matches('contacts');
$state.matches('contacts.**');
$state.matches('*.detail.*.*');
Route Policies
Prefer policy.navigation for auth/authorization and auth-like policy checks.
policy.transition.canExit handles transition decisions, while
policy.transition.dirty centralizes unsaved-change prompts for state leaves.
angular.createModule('editor', []).router({
name: 'edit',
url: '/edit',
component: 'editorPage',
policy: {
transition: {
dirty: {
when: ({from}) => from.data.dirty,
prompt: 'Discard unsaved changes?',
redirectTo: 'discard',
},
},
},
});
In this example, leaving edit prompts for unsaved changes and can redirect to
discard when declined. list uses canExit with a transition param to skip
the transition in a confirmation flow.
Retry and Fallback
Use retry to control how the router re-runs transiently failing work before
the transition continues, and use fallbackTo to route to a recovery state when
the final attempt fails.
angular.createModule('demo', []).router({
name: 'report',
component: 'reportPage',
policy: {
transition: {
loading: 'loading',
retry: ({attempt}) => attempt < 2,
fallbackTo: 'fallback',
},
},
resolve: {report: () => reportApi.load()},
});
transient retries its resolve up to two times (attempt is 0-based in the
context), then enters the target only if it eventually resolves. stable shows
fallback policy using a static and object target form.
When policy-driven boundaries are declarative, transitions do not require manual
$transitions.onBefore/onStart/onError handlers for common retry/fallback
behavior.
Use $router config when most routes should share the same transition policy:
angular.createModule('demo', []).config({
$router: {
loading: 'loading',
retry: 2,
fallbackTo: 'fallback',
errorBoundary: 'error',
viewTransitions: true,
},
});
Route-level policy.transition values override these router-wide defaults for
that route subtree. Set loading: false or retry: false on a route when that
route should opt out of the default behavior.
Route-level loading/error decisions can be expressed in policy.transition too.
Use error to select the boundary for a recoverable transition failure.
angular
.createModule('editorDemo', [])
.router({
name: 'profile',
url: '/profile',
template: '<profile/>',
policy: {
transition: {
loading: true,
errorBoundary: 'error',
},
},
})
.router({
name: 'error',
url: '/error',
template: '<p>Unable to load this state.</p>',
});
loading is an executable decision point that currently accepts a boolean, a
state target, or an injectable policy.
Loading and error boundaries are UX-level transition concerns. They are
evaluated after the inherited security decision is made, so
authentication/authorization still happens in $security and route policy
decides what loading/error flow the app shows on the router side.
Retention
Use policy.retention for route branches that should deactivate while inactive
instead of losing DOM, scope, and component state on every navigation.
angular.createModule('retentionDemo', []).router({
name: 'workspace',
abstract: true,
component: 'workspacePage',
policy: {
retention: {mode: 'keep-alive', max: 2, evict: 'lru'},
},
children: [
{name: 'editor', component: 'editorTab'},
{name: 'preview', component: 'previewTab'},
],
});
In this branch, workspace.tabA, workspace.tabB, and workspace.tabC inherit
the same retention policy. Leaving a tab deactivates its route tree. Returning
to the same retained key reactivates it with the previous scope and DOM state.
Because max is 2, entering a third distinct retained tab evicts one cached
entry and destroys its retained scope.
Use $router.retention when the entire application or module should share a
default retention policy:
angular.createModule('workspaceApp', []).config({
$router: {
retention: {
mode: 'keep-alive',
max: 5,
pause: 'schedulers',
evict: 'lru',
},
},
});
Route-level policy.retention overrides the router-wide default for that route
subtree. A route can opt out by declaring policy.retention.mode = 'destroy'.
Retention keys default to state name plus route/query params. Use a string key
to share a retained instance across params, or an injectable key policy when the
cache identity needs app-specific route context.
pause: "schedulers" tells retained route scopes to pause scheduler-bound work
while inactive and resume it on reactivation. App-owned model schedulers remain
app-context owned; retention applies to route/root-owned work.
Reload the current state or an ancestor subtree through go():
$state.go($state.current, $state.params, { reload: true, inherit: false });
$state.go($state.current, $state.params, {
reload: 'contacts',
inherit: false,
});
Executable examples:
Related
3 - Routing transitions and lifecycle hooks
Every navigation in AngularTS is a
Transition—a structured object
that describes moving from one state (or set of states) to another. Transitions
carry the from-state, the to-state, all parameter values, the tree of
entering/exiting/retained states, and the resolve context. You hook into their
lifecycle to implement auth guards, loading indicators, analytics, scroll
resets, and more.
What is a transition
When you call $state.go('contacts.detail', { contactId: 42 }), the router
creates a
Transition[ instance. Internally it computes a ](../../../typedoc/classes/Transition.html)TreeChanges
object with five paths:
| Path | Description |
|---|---|
from | All currently active nodes, from root to the current leaf state |
to | All nodes that will be active after the transition |
exiting | Nodes that are active now but will not be active after (in reverse order, deepest first) |
retained | Nodes that are active both before and after (unchanged) |
entering | Nodes that are not active now but will be active after (parent first) |
The
Transition[ exposes these paths through ](../../../typedoc/classes/Transition.html)trans.exiting(), trans.retained(), and trans.entering(), each returning an array of StateDeclaration objects. Use trans.treeChanges(pathname)for the rawPathNode[]
arrays.
Each transition has a numeric id and a promise that resolves to the
StateDeclaration for the
to-state on success, or rejects with an error object on failure.
Transition lifecycle
The router runs hooks in a fixed sequence of phases:
onCreate (synchronous)
Fires during transition construction before the transition is returned. Used internally for view config setup and global state updates. Not available for application hooks.
onBefore (synchronous)
Fires before any state is exited or entered. This is the right place for synchronous guards that should prevent the transition from even starting.
onStart (async)
Fires as the transition starts running—after onBefore is settled but before
any states are exited. Eager resolves are fetched here.
onExit (async, deepest first)
Fires for each state being exited, starting with the deepest and moving toward the root.
onRetain (async)
Fires for each state that is being retained (neither entered nor exited).
onEnter (async, shallowest first)
Fires for each state being entered, starting with the parent and moving toward
the leaf. Lazy resolves for entering states are fetched before each state’s
onEnter.
onFinish (async)
Fires after all onEnter hooks are done. This is the last chance to cancel or
redirect before the transition is committed.
onSuccess / onError (synchronous, after commit)
Fires after the transition is fully committed. onSuccess hooks run when the
promise resolves; onError hooks run when it rejects. Return values are ignored
at this stage.
Registering hooks
All hook registration methods are on the $transitions service (injected as
$transitions or typed as
ng.TransitionsService). Each
method returns a deregistration function.
$transitions.onBefore(
matchCriteria: HookMatchCriteria,
callback: (transition: Transition) => HookResult,
options?: HookRegOptions
): () => void // deregister function
Hook match criteria
HookMatchCriteria is an
object with optional keys to, from, exiting, retained, and entering.
Each value is:
- A state name string or glob (
'contacts.**') - A function
(state, transition) => boolean trueto match any state (the default when the key is omitted)
$transitions.onStart({}, callback);
// Matches transitions going to any child of 'admin'
$transitions.onBefore({ to: 'admin.**' }, callback);
// Matches transitions where a specific state is being exited
$transitions.onExit({ exiting: 'contacts.detail' }, callback);
// Matches using a function predicate
$transitions.onStart(
{
to: function (state) {
return state.policy?.navigation?.authenticated === true;
},
},
callback,
);
Hook registration options
priority: 10, // higher priority runs first (default: 0)
bind: myObject, // `this` inside callback
invokeLimit: 1 // auto-deregister after N invocations
});
Hook return values (HookResult)
The return value of a hook controls the transition:
| Return value | Effect |
|---|---|
undefined / true | Transition continues normally |
false | Transition is cancelled (aborted) |
TargetState | Transition is redirected to the new target |
Promise<false> | Transition waits for the promise; cancels if it resolves to false |
Promise<TargetState> | Transition waits for the promise; redirects when it resolves |
Rejected Promise | Transition fails with the rejection reason |
Note:
onSuccessandonErrorhooks run after the transition is committed. Their return values are ignored—you cannot cancel or redirect from these hooks.
Redirecting from a hook
Return a TargetState created with
$state.target():
// Always redirect 'home' to 'home.dashboard' as a default substate
return $state.target('home.dashboard');
});
The router internally creates a new transition to the redirect target and chains
the promises. If the original transition was triggered by a URL change, the
redirect uses location: 'replace' so the original URL is removed from history.
Cancelling a transition
Return false from any hook that runs before onSuccess:
if (appIsLocked) {
return false; // abort
}
});
A cancelled transition is rejected with an internal cancellation error type. The
browser URL is reset to the previous location by the built-in updateUrl hook.
Common hook patterns
Authentication guard
This pattern intercepts any transition to a state with a navigation policy and redirects unauthenticated users to the login page.
.run(['$transitions', '$state', 'AuthService', function ($transitions, $state, AuthService) {
$transitions.onBefore(
{
to: function (state) {
return state.policy && state.policy.navigation;
}
},
function (transition) {
if (!AuthService.isAuthenticated()) {
// Redirect to login, passing the intended destination
return $state.target('login', {
redirectTo: transition.to().name
});
}
}
);
}]);
Mark states that require authentication:
name: 'admin.users',
url: '/users',
component: 'AdminUsers',
policy: {
navigation: {
authenticated: true
}
}
});
Loading indicator
Show a spinner while any transition is in progress:
.run(['$transitions', '$rootScope', function ($transitions, $rootScope) {
$transitions.onStart({}, function () {
$rootScope.isLoading = true;
});
$transitions.onSuccess({}, function () {
$rootScope.isLoading = false;
});
$transitions.onError({}, function () {
$rootScope.isLoading = false;
});
}]);
Analytics tracking
Fire a page-view event after every successful navigation:
.run(['$transitions', 'AnalyticsService', function ($transitions, AnalyticsService) {
$transitions.onSuccess({}, function (transition) {
var toState = transition.to();
AnalyticsService.pageView({
page: toState.name,
url: window.location.pathname,
params: transition.params()
});
});
}]);
Scroll reset
Scroll to the top of the page on each navigation:
.run(['$transitions', function ($transitions) {
$transitions.onSuccess({}, function () {
window.scrollTo(0, 0);
});
}]);
Async guard with redirect on failure
Allow users to authenticate mid-transition:
var AuthService = transition.injector().get('AuthService');
if (!AuthService.isAuthenticated()) {
// Return a promise; transition waits for it to settle
return AuthService.authenticate().catch(function () {
return $state.target('guest');
});
}
});
Transition outcomes
$state.go() is an asynchronous navigation boundary, so it uses normal Promise
control flow rather than returning an { ok, status } object. Branch by
awaiting the
TransitionPromise:
fulfillment means navigation completed or was ignored, while rejection carries
the router failure. When present, the attached
Transition[ records ](../../../typedoc/classes/Transition.html)successanderror()
as evidence after settlement.
A transition can finish in one of four states:
| Result | Description |
|---|---|
| Success | All hooks passed, the to-state is now active. transition.success === true. |
| Error | A hook threw, returned a rejected promise, or a resolve failed. transition.success === false, transition.error() contains the reason. |
| Ignored | The transition targets the same state with the same parameters that are currently active. Treated as successful navigation. |
| Superseded | A newer transition started before this one finished. The older transition is abandoned; the newer one continues. |
Redirected transitions are not an error from the caller’s perspective:
$state.go() transparently chains to the redirect target’s promise.
The router does not expose a persistence snapshot. Route declarations describe navigation, and retention policy manages live views; neither is a durable machine or workflow snapshot.
Accessing the transition object
The active transition is stored on $router.transition. Inside any hook, the
Transition instance is passed as
the first argument.
console.log('Transitioned to:', transition.to().name);
console.log('From:', transition.from().name);
console.log('Params:', transition.params());
console.log('Entering:', transition.entering().map(s => s.name));
console.log('Exiting:', transition.exiting().map(s => s.name));
console.log('ID:', transition.id);
});
transition.redirectedFrom() returns the previous transition in a redirect
chain. transition.originalTransition() walks the entire chain back to the
first transition.
Per-transition hooks
Hooks can also be registered on a specific
Transition instance rather than
globally on $transitions. These hooks only affect that single transition:
trans.onSuccess({}, function () {
console.log('This specific transition succeeded.');
});
Warning: Per-transition hooks must be registered before the transition has finished running. Register them synchronously in the same call stack as the navigation, or in an
onStartglobal hook.
Dynamic resolvables in hooks
An onBefore hook can add additional resolve data to the current transition
using transition.addResolvable(). The added resolvable is available to hooks
and views that run after it:
transition.addResolvable({
token: 'requestId',
resolveFn: function () { return generateRequestId(); }
});
});
Transition hook ordering: priority and phase
Within a phase (e.g., all onStart hooks), hooks are invoked in descending
priority order. The default priority is 0. Built-in hooks like URL update and
view activation use priorities in the range 9000–10000, so application hooks
with the default priority run before them.
For onExit, hooks are invoked deepest-first (children before parents). For
onEnter, hooks are invoked shallowest-first (parents before children). This
ordering is fixed regardless of priority.
4 - Route maps
Route maps are optional TypeScript contracts for applications that want route names, params, and resolves checked at author time. They do not change the runtime router; they describe the route contract that the runtime already enforces.
RouterModuleDeclaration
is the only public route-registration shape. router() accepts either one
declaration or a readonly array of declarations. TDeclaration in its generated
signature captures the exact argument literal for inference; it is not another
public type. There is no router<TRouteMap>(...) assertion overload and no
public state provider. Put an explicit route map on
createModule<TRouteMap>(...) when inference is not the desired boundary.
Use StateService<TRoutes> at
injection boundaries where code calls $state.go(...) or $state.href(...).
type AdminRoutes = {
'admin.users': {
params: {
page?: number;
query?: string;
};
resolves: {
users: { id: string }[];
total: number;
};
};
'admin.profile': {
params: {
userId: string;
};
};
};
declare const $state: ng.StateService<AdminRoutes>;
const usersParams: ng.ParamsOf<AdminRoutes, 'admin.users'> = {
page: 1,
query: 'active',
};
const usersResolves: ng.ResolvesOf<AdminRoutes, 'admin.users'> = {
users: [{ id: '1' }],
total: 1,
};
$state.go('admin.users', usersParams);
$state.go('admin.users');
$state.href('admin.profile', { userId: '42' });
With a route map enabled, TypeScript rejects unknown route names, missing required params, omitted required params, extra params, and invalid resolve maps.
// @ts-expect-error unknown route
$state.go('admin.missing', {});
// @ts-expect-error userId is required
$state.href('admin.profile', {});
// @ts-expect-error profile params cannot be omitted
$state.go('admin.profile');
// @ts-expect-error page is not a profile param
$state.go('admin.profile', { page: 1 });
Derive a route map from router(...)
For module-owned route trees, keep the tree literal as the source of truth and derive the public route map from it.
const adminTree = {
name: 'admin',
children: [
{
name: 'users',
url: '/users/{page:int}',
params: { page: { type: 'int' } },
resolve: { users: () => [{ id: '1' }] },
},
],
} as const satisfies ng.RouterModuleDeclaration;
angular.createModule('admin', []).router(adminTree);
type AdminRoutes = ng.RoutesOf<typeof adminTree>;
declare const $state: ng.StateService<AdminRoutes>;
$state.go('admin.users', { page: 1 });
RoutesOf infers dot-composed child
names, absolute child names, explicit params keys, built-in literal URL
parameter value types, custom $router.paramTypes value types, and object-style
resolve return values.
Pass the same custom param type map used by $router.paramTypes as the second
generic argument. decode return types are used first; if a type has no
decode, an is(value): value is T predicate is used as the value type.
Large modules can pin the route contract directly on the module. This keeps later route trees and lazy namespaces route-name aware without exposing provider APIs.
type AdminRoutes = ng.RoutesOf<typeof adminTree>;
angular
.createModule<AdminRoutes>('admin', [])
.router(adminTree)
.lazyState('admin.**', () => import('./admin.routes'));
When the tree is declared inline, router(...) infers a
ng.RouterModule<ng.RoutesOf<typeof tree>>
return type automatically. Typed router modules check the router(...) tree
itself, keep route names checked across chained router(...) calls and
lazyState(...) prefixes, and narrow object-style params and resolve
declarations to the route contract. Array-style resolves remain available when a
route needs explicit resolve metadata such as token, deps, or eager.
If a route map is supplied with angular.createModule<Routes>(...), that explicit
module contract is preserved after router(...). This lets a feature module
declare one route tree while the route map also includes lazy boundaries or
states registered later in the same module.
Use that explicit module contract when states must be registered individually:
type AppRoutes = {
home: {};
settings: {};
};
angular
.createModule<AppRoutes>('app', [])
.router({ name: 'home', url: '/', component: 'homePage' })
.router({
name: 'settings',
url: '/settings',
component: 'settingsPage',
});
The route map is declared once on createModule(). Each router() call is then
checked against it. Prefer a single inferred tree or forest when the states are
available together, because that avoids repeating route names in a handwritten
map.
Typed lazy prefixes accept known route names, parent route prefixes, and the
explicit ".**" lazy namespace form. Unknown lazy namespaces are rejected when
the module has a route map.
Typed router trees accept known route names and parent prefixes. This lets a
module declare an abstract or shell parent such as admin while still rejecting
unknown concrete children such as admin.audit when they are not in the module
route map.
type Uuid = string & { readonly __uuid: unique symbol };
const paramTypes = {
uuid: {
pattern: /[0-9a-f-]{36}/i,
encode(value: Uuid) {
return value;
},
decode(value: string): Uuid {
return value as Uuid;
},
},
};
const appTree = {
name: 'item',
url: '/item/{id:uuid}',
} as const;
angular.createModule('items', []).config({ $router: { paramTypes } }).router(appTree);
type ItemRoutes = ng.RoutesOf<typeof appTree, typeof paramTypes>;
declare const $state: ng.StateService<ItemRoutes>;
$state.go('item', { id: 'b4f6eb70-3d2b-42df-8f14-4c7e8e88c208' as Uuid });
Use
ng.Transition<TRoutes, { to: ToName; from: FromName }>
when code receives an injected $transition$. params() and params('to')
read the destination route params; params('from') can be narrowed when the
source route is known.
declare const $transition$: ng.Transition<
AdminRoutes,
{ to: 'admin.profile'; from: 'admin.users' }
>;
const toParams = $transition$.params();
const fromParams = $transition$.params('from');
toParams.userId.toUpperCase();
fromParams.page?.toFixed();
The same route map should be the source of truth for resolves and routed component inputs. At runtime, routed components already receive resolve values by matching binding names. The route map makes that contract visible to TypeScript without asking application code to touch internal view records.
type AdminUsersRoute = AdminRoutes['admin.users'];
type AdminUsersResolves = ng.ResolvesOf<AdminRoutes, 'admin.users'>;
const usersComponent = {
bindings: {
users: '<',
total: '<',
},
};
const usersRouteContract: AdminUsersResolves = {
users: [{ id: '1' }],
total: 1,
};
const checkedUsersRouteContract =
usersRouteContract satisfies AdminUsersRoute['resolves'];
void usersComponent;
void checkedUsersRouteContract;
Component contracts at the route boundary
Route maps should be mirrored by app-owned component contracts when you want typed, non-internal binding assertions.
type AdminUsersResolves = ng.ResolvesOf<AdminRoutes, 'admin.users'>;
type AdminUsersBindings = {
users: AdminUsersResolves['users'];
total: AdminUsersResolves['total'];
};
function buildUsersViewModel(
routeResolves: AdminUsersResolves,
): AdminUsersBindings {
return {
users: routeResolves.users,
total: routeResolves.total,
};
}
The example keeps route resolves and component inputs in one named contract so
admin.users consumers can evolve in lockstep. This stays explicit and does not
require internal router records.
Lazy route map composition
Lazy route declarations are composed per module rather than centralized in one monolithic map.
type AppCoreRoutes = {
'app.home': {
params: {};
};
};
type LazyAdminRoutes = {
'admin.users': {
params: { filter?: string };
resolves: {
users: { id: string }[];
};
};
};
type AppRoutes = AppCoreRoutes & LazyAdminRoutes;
declare const $state: ng.StateService<AppRoutes>;
const adminHref = $state.href('admin.users', { filter: 'active' });
void adminHref;
This pattern keeps typed navigation available at call sites without forcing a root module to own every lazy route declaration.
Generated docs and namespace shape
The generated public surface exposes route maps as helper contracts:
ng.RouteContractng.RouteMapng.RoutesOf<typeof tree>ng.ParamsOf<TRoutes, TName>ng.ResolvesOf<TRoutes, TName>ng.RouterModule<TRoutes>ng.StateService<TRoutes>ng.Transition<TRoutes, { to: ToName; from: FromName }>
These types are public authoring helpers only. They are not aliases for built state records, transition internals, retained-view cache entries, or view service records. If a route map needs to include a lazy-loaded module, compose the public route-map types at the TypeScript boundary rather than importing router internals.
ng-state always evaluates its value as an expression. Use a quoted literal
route name when the target is static; this keeps examples and editor tooling
aligned with the same route-map contract. An unquoted identifier is a scope
lookup, not an implicit route-name string. Do not wrap ng-state expressions in
interpolation braces.
<a ng-state="'admin.profile'" ng-state-params="{ userId: user.id }">
Profile
</a>
When a template route is static, mirror that literal route name in the component or docs-example TypeScript boundary. This gives TypeScript the same route-map contract without adding a template compiler.
type StaticNgStateLiteral<
TRoutes extends ng.RouteMap,
TName extends Extract<keyof TRoutes, string>,
> = {
state: TName;
params: ng.ParamsOf<TRoutes, TName>;
};
const profileLink: StaticNgStateLiteral<AdminRoutes, 'admin.profile'> = {
state: 'admin.profile',
params: { userId: '42' },
};
Use an identifier or property-path expression only when the route name is
genuinely dynamic. The AngularTS VS Code tooling checks simple TypeScript
literal unions such as $ctrl.route: 'admin.profile' | 'admin.users'. A broad
string cannot prove a finite target set, so use
StateService<TRoutes> in the code
that computes that value when it needs author-time validation.
Current boundary:
$state.go(...),$state.href(...), params, and resolve maps have executable public type coverage.ng.Transition<TRoutes, { to: ToName; from: FromName }>gives injected$transition$code typedparams(),params('to'), andparams('from').ng.RouterModule<TRoutes>letsangular.createModule<TRoutes>(...)androuter(...)carry route-name checking into module declarations.ng.RoutesOf<typeof tree>derives a route map from a literalrouter(...)tree without exposing internal state, transition, or view records.- Built-in literal URL parameter types and custom
$router.paramTypesvalue types are inferred from route URL patterns. - Editor tooling completes and validates literal
ng-stateroute names andng-state-paramskeys in inline templates andtemplateUrlfiles. It also validates simple dynamic route-name unions when TypeScript can prove them.
Executable checks:
src/router/state/state-declaration-types.spec.tsverifies typed route params, resolve maps,$state.go(...),$state.href(...), and$transition$.params(...).state-links.htmldemonstrates the quoted literalng-statetemplate shape.
5 - URL matching and configuration in AngularTS router
The AngularTS router matches the browser’s URL against registered state declarations and activates the best matching state. URL matching is an optional layer on top of the state machine—states can be navigated to programmatically without any URL involvement—but most applications use URLs to make deep-linking and browser history work correctly.
How URL matching works
When the browser URL changes (or on initial load), the router syncs registered
URL rules in priority order and finds the best match using a weighted scoring
system. The winning rule’s handler calls $state.go() with the matched state
and extracted parameter values.
URL rules are created automatically for every state that has a url property.
Rules with the same sort order are ranked by weight so the most specific match
wins.
URL parameters
Parameters are declared in the state’s url string. The router uses a
UrlMatcher compiled from this pattern to test URLs and extract values.
Path parameters
Use a colon prefix for named path segments:
name: 'user',
url: '/users/:userId',
component: 'UserProfile'
});
// Matches: /users/42 → { userId: '42' }
Use curly braces for parameters with inline type annotations or custom regexps:
name: 'product',
url: '/products/{productId:int}',
component: 'ProductDetail'
});
// Matches: /products/123 → { productId: 123 } (integer, not string)
// Does NOT match: /products/abc
Custom regexp:
name: 'article',
url: '/articles/{slug:[a-z0-9-]+}',
component: 'Article'
});
Query parameters
Append a ? followed by parameter names. Multiple query params are separated by
&:
name: 'search',
url: '/search?q&page',
component: 'SearchResults'
});
// Matches: /search?q=angularts&page=2 → { q: 'angularts', page: '2' }
Typed query params:
name: 'messages',
url: '/messages?{before:date}&{after:date}',
component: 'MessageList'
});
Mixed path and query params:
name: 'mailbox',
url: '/messages/:mailboxId?{before:date}&{after:date}',
component: 'Mailbox'
});
Optional parameters
Give a parameter a default value in the params block and set squash: true to
make it optional. When the URL is visited without the parameter, the default
value is used. When navigating with the default value, the parameter is omitted
from the URL:
name: 'userList',
url: '/users/:page',
params: {
page: {
value: '1', // default value
squash: true // remove from URL when value equals default
}
},
component: 'UserList'
});
// /users/ → { page: '1' } (squashed)
// /users/3 → { page: '3' }
Built-in parameter types
The router ships the following built-in parameter types. Specify the type inline
in the URL pattern or in the params block:
string
Default for path parameters. Encodes and decodes as a plain string. Slashes
within the value are encoded as ~2F in AngularTS’s patched path type to avoid
ambiguity in Angular 1’s $location.
url: '/items/:name';
// { name: 'foo bar' } → /items/foo%20bar
int
Parses URL segments as integers using parseInt. The pattern is /\d+/.
url: '/users/{id:int}';
// /users/42 → { id: 42 } (number, not string)
bool
Represents boolean values. Encodes true as "1" and false as "0". The
pattern matches 1 or 0.
url: '/settings/{darkMode:bool}';
// /settings/1 → { darkMode: true }
json
Encodes arbitrary objects as a JSON string in the URL (URL-encoded). Useful for complex filter objects.
url: '/search?{filters:json}';
// { filters: { status: 'active', role: 'admin' } }
// → /search?filters=%7B%22status%22%3A%22active%22%7D
date
Encodes Date objects as YYYY-MM-DD strings. Parses the string back to a
Date on the way in.
url: '/events?{startDate:date}&{endDate:date}';
// { startDate: new Date('2024-01-15') }
// → /events?startDate=2024-01-15
hash
The internal parameter type used for the # (hash/anchor) portion of the URL.
Has inherit: false so the hash is not carried forward to child state
transitions.
URL configuration with $router
$router config controls global URL matching behavior.
Case sensitivity
URL matching is case-sensitive by default. Allow case-insensitive matching:
angular.getModule('app').config({
$router: {
caseInsensitive: true,
},
});
Strict mode (trailing slashes)
By default, /users/ and /users are distinct. Disable strict mode to treat
trailing slashes as equivalent:
angular.getModule('app').config({
$router: {
strict: false, // /users/ matches /users
},
});
Default squash policy
Control the global default for how parameters with default values appear in URLs:
angular.getModule('app').config({
$router: {
// 'false' (default): include the default value in the URL
// 'true': omit default value from URL
// '~': replace default value with '~' in the URL
defaultSquash: true,
},
});
Custom parameter types
Register a custom ParamType before
using it in state URL patterns. The type must implement encode, decode,
is, equals, and optionally pattern:
angular.createModule('app', []).config({
$router: {
paramTypes: {
intarray: {
encode: (values) => values.join('-'),
decode: (value) => value.split('-').map(Number),
is: (value) => Array.isArray(value) && value.every(Number.isInteger),
equals: (a, b) =>
a.length === b.length && a.every((x, i) => x === b[i]),
pattern: /[0-9]+(?:-[0-9]+)*/,
},
},
},
});
Use the custom type in a state URL:
angular.getModule('app').router({
name: 'report',
url: '/reports/{ids:intarray}',
component: 'Report',
});
// $state.go('report', { ids: [10, 20, 30] }) → /reports/10-20-30
// /reports/10-20-30 → { ids: [10, 20, 30] }
Executable sample: param-types.html
Note: Register custom types in
$router.paramTypesbefore any state that uses them.
Hash mode vs HTML5 mode
AngularTS inherits Angular 1’s $location modes. Configure the mode with
module.config({ $location: ... }):
Hash mode (default)
URLs use a hash fragment: http://example.com/app/#/contacts/42. No server
configuration is needed; the hash segment is never sent to the server.
angular.getModule('app').config({
$location: {
html5Mode: false,
hashPrefix: '!', // optional: use #!/ instead of #/
},
});
Generated hrefs are prefixed with # (plus the hash prefix) in hash mode.
HTML5 mode (pushState)
URLs use real paths: http://example.com/contacts/42. The server must return
the app’s index.html for all routes.
angular.getModule('app').config({
$location: {
html5Mode: {
enabled: true,
requireBase: true, // <base href="..."> must be present in <head>
},
},
});
In HTML5 mode, generated hrefs use the base path rather than a hash.
Base href
In HTML5 mode, the base href is read from the <base> tag in the document
<head>:
<head>
<base href="/myapp/" />
</head>
If no <base> tag is present, the runtime falls back to
window.location.pathname. The base href is used when constructing absolute
URLs via $state.href(..., { absolute: true }) and when pushing new history
entries.
Reading the current URL
Use $location to read URL components:
$location.getPath(); // "/contacts/42"
$location.getSearch(); // { tab: 'notes' }
$location.getHash(); // "section1"
$location.getUrl(); // "/contacts/42?tab=notes#section1"
Updating the URL
Use $location.setUrl(newUrl) to replace the current URL. The router then syncs
the matching state:
$location.setUrl('/contacts/99?tab=history');
URL rule priority and matching weight
When multiple rules could match the same URL, the router scores each match and picks the winner:
- Rules are sorted by a primary sort order (rules created from state declarations all share the same group).
- Within a group, the match with the highest weight wins. Weight is computed by counting matched segments, typed parameters, and specificity of regexps.
- A state URL like
/users/{id:int}outweighs/users/:idfor the path/users/42because the typed parameter provides a stricter match.
Exact path matches score higher than prefix matches. Query parameters do not affect path scoring but must all be present if declared without defaults.
Listening for URL changes
Listen for $locationChangeSuccess when code needs URL-level notifications:
const deregister = $rootScope.on('$locationChangeSuccess', () => {
console.log('URL changed to:', $location.getUrl());
});
// Stop listening
deregister();
When loading states asynchronously, register them before enabling navigation
links or calling $state.go() for URL-driven destinations:
angular.getModule('app').run([
'$stateRegistry',
'$state',
function ($stateRegistry, $state) {
fetch('/api/states')
.then((r) => r.json())
.then(function (states) {
states.forEach((s) => $stateRegistry.register(s));
$state.go('home');
});
},
]);