This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Routing

Build applications with multiple screens, browser URLs, nested layouts, data loading, and guarded transitions.

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.

1 - State-based routing in AngularTS applications

Learn how AngularTS implements state-based routing with states, views, resolves, transitions, and URL matching configured through module declarations.

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-view element 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:

TokenTypePurpose
$stateStateServiceNavigate (go), inspect current state (is, includes, current)
$transitionsTransitionsServiceRegister lifecycle hooks (onBefore, onStart, onSuccess, …)
$stateRegistryAdvanced registryRegister 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>

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>
  • 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

Define state hierarchies, resolve data, register states, and navigate with $state.

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' },
  },
];

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.

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:

3 - Routing transitions and lifecycle hooks

Understand the AngularTS transition lifecycle and use hooks like onBefore, onStart, onSuccess, and onError for auth guards, analytics, and redirects.

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:

PathDescription
fromAll currently active nodes, from root to the current leaf state
toAll nodes that will be active after the transition
exitingNodes that are active now but will not be active after (in reverse order, deepest first)
retainedNodes that are active both before and after (unchanged)
enteringNodes 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
  • true to 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 valueEffect
undefined / trueTransition continues normally
falseTransition is cancelled (aborted)
TargetStateTransition 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 PromiseTransition fails with the rejection reason

Note: onSuccess and onError hooks 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:

ResultDescription
SuccessAll hooks passed, the to-state is now active. transition.success === true.
ErrorA hook threw, returned a rejected promise, or a resolve failed. transition.success === false, transition.error() contains the reason.
IgnoredThe transition targets the same state with the same parameters that are currently active. Treated as successful navigation.
SupersededA 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 onStart global 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

Use route maps to make $state navigation, route params, resolves, and ng-state examples auditable by TypeScript.

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:

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 typed params(), params('to'), and params('from').
  • ng.RouterModule<TRoutes> lets angular.createModule<TRoutes>(...) and router(...) carry route-name checking into module declarations.
  • ng.RoutesOf<typeof tree> derives a route map from a literal router(...) tree without exposing internal state, transition, or view records.
  • Built-in literal URL parameter types and custom $router.paramTypes value types are inferred from route URL patterns.
  • Editor tooling completes and validates literal ng-state route names and ng-state-params keys in inline templates and templateUrl files. It also validates simple dynamic route-name unions when TypeScript can prove them.

Executable checks:

  • src/router/state/state-declaration-types.spec.ts verifies typed route params, resolve maps, $state.go(...), $state.href(...), and $transition$.params(...).
  • state-links.html demonstrates the quoted literal ng-state template shape.

5 - URL matching and configuration in AngularTS router

Configure URL parameters, typed path and query params, hash vs HTML5 mode, base href, and custom parameter types in the AngularTS state-based 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.paramTypes before 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:

  1. Rules are sorted by a primary sort order (rules created from state declarations all share the same group).
  2. Within a group, the match with the highest weight wins. Weight is computed by counting matched segments, typed parameters, and specificity of regexps.
  3. A state URL like /users/{id:int} outweighs /users/:id for the path /users/42 because 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');
      });
  },
]);