1 - $anchorScroll

Anchor scroll service

Description

$anchorScroll service scrolls to the element hash or (if omitter) to the current value of $location.getHash(), according to the rules speciffied in the HTML spec.

It also watches the URL hash and automatically scrolls to any matched anchor whenever it is changed by $location. This can be disabled by a setting on $anchorScrollProvider.

Additionally, you can use the yOffset property to specify a vertical scroll-offset (either fixed or dynamic).

Example

<style>
  #scrollArea {
    height: 100px;
    overflow: auto;
  }

  #bottom {
    display: block;
    margin-top: 2000px;
  }
</style>
<section ng-app="demo">
  <div id="scrollArea" ng-controller="ScrollController">
    <button class="bottom" ng-click="gotoBottom()">Go to bottom</button>
    <a id="bottom"></a> You're at the bottom!
  </div>
</section>
window.angular.module('demo', []).controller('ScrollController', [
  '$scope',
  '$location',
  '$anchorScroll',
  function ($scope, $location, $anchorScroll) {
    $scope.gotoBottom = function () {
      // set the location.hash to the id of
      // the element you wish to scroll to.
      $location.setHash('bottom');

      // call $anchorScroll()
      $anchorScroll();
    };
  },
]);

Demo

You're at the bottom!

The example below illustrates the use of a vertical scroll-offset (specified as a fixed value).

Example

<style>
  .demo2 {
    height: 100px;
    overflow: auto;
  }

  .anchor {
    border: 2px dashed DarkOrchid;
    padding: 10px 10px 400px 10px;
  }

  .fixed-header {
    background-color: rgba(0, 0, 0, 0.2);
    height: 50px;
  }

  .fixed-header > a {
    display: inline-block;
    margin: 5px 15px;
  }
</style>

<section id="demo2" class="demo2">
  <div ng-controller="headerCtrl" class="fixed-header">
    <button class="btn" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
      Go to anchor {{x}}
    </button>
  </div>
  <div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]">
    Anchor {{x}} of 5
  </div>
</section>
window.angular
  .module('demo2', [])
  .run([
    '$anchorScroll',
    function ($anchorScroll) {
      $anchorScroll.yOffset = 100; // always scroll by 50 extra pixels
    },
  ])
  .controller('headerCtrl', [
    '$anchorScroll',
    '$location',
    '$scope',
    function ($anchorScroll, $location, $scope) {
      $scope.gotoAnchor = function (x) {
        window.$locationTest = $location;
        const newHash = 'anchor' + x;
        if ($location.getHash() !== newHash) {
          // set the $location.hash to `newHash` and
          // $anchorScroll will automatically scroll to it
          $location.setHash('anchor' + x);
        } else {
          // call $anchorScroll() explicitly,
          // since $location.hash hasn't changed
          $anchorScroll();
        }
      };
    },
  ]);

window.angular.bootstrap(document.getElementById('demo2'), ['demo2']);

Demo

Anchor {{x}} of 5

For detailed method description, see $anchorScroll.

2 - $compile

Template compilation service

3 - $controller

Controller service

4 - $cookie

Provides read/write access to browser’s cookies

Description

The $cookie service offers a simple API for interacting with browser cookies in AngularTS applications. It allows you to:

  • Read existing cookies
  • Write new cookies
  • Delete cookies
  • Store and retrieve JavaScript objects (via JSON serialization)
  • Automatically URI-encode and decode values

Example

angular.module('app').controller(
  'UserCtrl',
  /** @param {ng.CookieService} $cookie */
  function ($cookie) {
    // Write cookie
    $cookie.put('session_id', 'abc123');

    // Read cookie
    const session = $cookie.get('session_id');

    // Store object
    $cookie.putObject('profile', { name: 'Alice', admin: true });

    // Read object
    const profile = $cookie.getObject('profile');

    // Remove cookie
    $cookie.remove('session_id');
  },
);

Cookie behavior can be customized globally using $cookieProvider.defaults. Cookies are limited to roughly 4 KB, including key and value, so void storing large objects; prefer identifiers or short tokens.

For detailed method description, see CookieService

5 - $eventBus

Pubsub messaging service

Description

A messaging service, backed by an instance of PubSub. This implementation is based on the original Google Closure PubSub but uses queueMicrotask instead of Window.setTimeout() for its async implementation.

$eventBus allows communication between an Angular application instance and the outside context, which can be a different module, a non-Angular application, a third-party party library, or even a WASM application. Additionally, $eventBus can be used to communicate directly with a template, using ng-channel directive.

$eventBus should not be used for communicating between Angular’s own primitives.

  • For sharing application state: use custom Services and Factories that encapsulate your business logic and manage your model.
  • For intercomponent communication: use $scope.$on(), $scope.$emit(), and $scope.$broadcast() methods.

The example below demonstrates communication between the global window context and a controller. Note: Ensure topic clean-up after the $scope is destroyed

Example

<section ng-app="demo">
  <div ng-controller="DemoCtrl as $ctrl">
    Milliseconds elapsed since the epoch <b>{{ $ctrl.ms }}</b>
  </div>
</section>

<!--We are using a regular onclick and `angular` global -->
<button
  class="btn btn-dark"
  onclick="angular.$eventBus.publish('demo', Date.now())"
>
  Publish time
</button>
window.angular.module('demo', []).controller(
  'DemoCtrl',
  class {
    static $inject = ['$eventBus', '$scope'];
    constructor($eventBus, $scope) {
      const unsubscribe = $eventBus.subscribe('demo', (val) => {
        // `this.ms = val` will not work because `this` is not a proxy
        //  to trigger change detection, we access controller as a scope property
        $scope.$ctrl.ms = val;
      });

      $scope.$on('$destroy', unsubscribe);
    }
  },
);

Demo

Milliseconds elapsed since the epoch {{ $ctrl.ms }}

For detailed method description, see PubSub


6 - $exceptionHandler

Error handling service

Description

$exceptionHandler is the central hook for uncaught exceptions inside an AngularTS application. The framework routes all errors from synchronous code, async tasks, expression evaluation, and dependency injection through this service.

By default, it rethrows exceptions that occur during AngularTS-managed execution. This fail-fast behavior ensures errors are visible immediately in development and in unit tests.

For type description, see ng.ExceptionHandler.

For service customisation, see ng.ExceptionHandlerProvider

7 - $http

HTTP client service

The $http service is AngularTS’s built-in HTTP client. Use this page for the behavioral model and common examples. For exact method signatures, parameters, return types, and interfaces, use the generated TypeDoc reference:

Usage

Call $http with a full request configuration when you need explicit control:

$http<User>({
  method: 'GET',
  url: '/api/users/42',
}).then((response) => {
  user = response.data;
});

For common HTTP verbs, use shorthand methods:

$http.get<User>('/api/users/42').then(({ data }) => {
  user = data;
});

$http.post<User>('/api/users', {
  name: 'Ada',
  email: 'ada@example.com',
});

Request Behavior

Plain objects are serialized as JSON by the default request transform. File, Blob, and FormData values are sent as-is so the browser can set the correct transport headers.

Query parameters are serialized by $httpParamSerializer. Arrays produce repeated keys, objects are JSON encoded, and null, undefined, and functions are omitted.

$http.get<Article[]>('/api/articles', {
  params: {
    category: 'news',
    tags: ['featured', 'breaking'],
  },
});

Response Behavior

Successful 2xx responses resolve with an HttpResponse<T>. Non-2xx responses, timeouts, aborts, and network errors reject with the same response shape, so error handlers can inspect status, statusText, headers, config, and xhrStatus.

$http.get<User>('/api/users/99').catch((error) => {
  if (error.xhrStatus === 'timeout') {
    message = 'Request timed out.';
  } else if (error.status === 404) {
    message = 'User not found.';
  }
});

Defaults

Configure defaults before bootstrap with $httpProvider.defaults, or mutate runtime defaults through $http.defaults.

angular.module('app', []).config(($httpProvider) => {
  $httpProvider.defaults.headers.common.Authorization = 'Bearer token';
  $httpProvider.defaults.withCredentials = true;
});

Default response transforms strip AngularJS JSON protection prefixes and parse JSON responses automatically.

Interceptors

Interceptors are registered with $httpProvider.interceptors. Request hooks run in registration order; response hooks run in reverse order.

angular.module('app', []).config(($httpProvider) => {
  $httpProvider.interceptors.push(() => ({
    request(config) {
      config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
      return config;
    },

    responseError(rejection) {
      if (rejection.status === 401) {
        window.location.href = '/login';
      }

      return Promise.reject(rejection);
    },
  }));
});

XSRF

$http reads the configured XSRF cookie and sends it in the configured XSRF header for trusted origins. Add cross-origin APIs explicitly:

angular.module('app', []).config(($httpProvider) => {
  $httpProvider.xsrfTrustedOrigins.push('https://api.example.com');
});

8 - $interpolate

HTTP service

9 - $location

Normalize and update browser URLs across HTML5 and hashbang modes.

$location parses the browser URL into path, search, hash, and state pieces. Changes made through $location update the browser address bar, and browser navigation updates the service.

Exact signatures live in TypeDoc:

Read The Current URL

$location.path();
$location.search();
$location.hash();
$location.url();
$location.absUrl();

Update The URL

$location
  .path("/settings")
  .search({ tab: "profile" })
  .hash("details");

Setter methods return $location, so updates can be chained.

$locationChangeStart is broadcast on $rootScope before the URL changes. Call event.preventDefault() from a listener to cancel the navigation.

$locationChangeSuccess is broadcast after the URL changes. In HTML5 mode, listeners may also receive the new and old history state values when the browser supports the History API.

$rootScope.$on("$locationChangeStart", (event, newUrl, oldUrl) => {
  if (shouldBlock(newUrl, oldUrl)) {
    event.preventDefault();
  }
});

$rootScope.$on("$locationChangeSuccess", (_event, newUrl) => {
  analytics.track("page_view", { url: newUrl });
});

For provider configuration, see $locationProvider.

10 - $log

Logging service

Using $log

Default implementation of LogService safely writes the message into the browser’s console (if present). The main purpose of this service is to simplify debugging and troubleshooting, while allowing the developer to modify the defaults for live environments.

Example*
angular.module('demo').controller('MyController', ($log) => {
  $log.log('log');
  $log.info('info');
  $log.warn('warn!');
  $log.error('error');
  $log.debug('debug');
});

To reveal the location of the calls to $log in the JavaScript console, you can “blackbox” the AngularTS source in your browser:

Note: Not all browsers support blackboxing.
The default is to log debug messages. You can use $logProvider.debug to change this.

For configuration and custom implementations, see $logProvider.

Decorating $log

You can also optionally override any of the $log service methods with $provide decorator. Below is a simple example that overrides default $log.error to log errors to both console and a backend endpoint.

Example*
angular.module('demo').config(($provide) => {
  $provide.decorator('$log', ($delegate, $http, $exceptionHandler) => {
    const originalError = $delegate.error;
    $delegate.error = () => {
      originalError.apply($delegate, arguments);
      const errorMessage = Array.prototype.slice.call(arguments).join(' ');
      $http.post('/api/log/error', { message: errorMessage });
    };
    return $delegate;
  });
});

Overriding console

If your application is already heavily reliant on default console logging or you are using a third-party library where logging cannot be overriden, you can still take advantage of Angular’s services by modifying the globals at runtime. Below is an example that overrides default console.error to logs errors to both console and a backend endpoint.

Example*
angular.module('demo').run(($http) => {
  /**
   * Decorate console.error to log error messages to the server.
   */
  const $delegate = window.console.error;
  window.console.error = (...args) => {
    try {
      const errorMessage = args
        .map((arg) => (arg instanceof Error ? arg.stack : JSON.stringify(arg)))
        .join(' ');
      $delegate.apply(console, args);
      $http.post('/api/log/error', { message: errorMessage });
    } catch (e) {
      console.warn(e);
    }
  };

  /**
   * Detect errors thrown outside Angular and report them to the server.
   */
  window.addEventListener('error', (e) => {
    window.console.error(e.error || e.message, e);
  });

  /**
   * Optionally: Capture unhandled promise rejections
   */
  window.addEventListener('unhandledrejection', (e) => {
    window.console.error(e.reason || e);
  });
});

Combining both $log decoration and console.log overriding provides a robust and flexible error reporting strategy that can adapt to your use-case.

* array notation and HTTP error handling omitted for brevity

11 - $machine

Reactive mode machines with declarative transitions

$machine

$machine creates a small reactive mode machine for UI and application flows. It is more than a regular service or factory because the returned machine is designed to be wrapped by AngularTS scope proxies: current and data update templates naturally, and send() batches transition work when a scope proxy is observing the machine.

Use ordinary services for IO, persistence, and shared domain logic. Use $machine when the important part is the allowed flow between modes.

Use $workflow when the flow also needs command boundaries, diagnostics, snapshots, restore, retry, or repeat.

Create a Machine

Inject $machine and assign the created machine to a controller or scope property:

app.controller('SessionCtrl', function ($machine) {
  this.session = $machine({
    initial: 'setup',
    data: {
      roomId: '',
      error: '',
    },
    transitions: {
      setup: {
        join(data, message) {
          data.roomId = message.roomId;
          return 'waiting';
        },
      },
      waiting: {
        matched(data, message) {
          data.roomId = message.roomId;
          return 'playing';
        },
        unavailable(data, reason) {
          data.error = reason;
          return 'setup';
        },
      },
    },
  });
});

Docs call current a mode. Transition handlers return a mode string to move to another mode.

Register a Named Machine

Use module.machine(name, config) when a machine should be injectable by name:

app.machine('sessionMachine', {
  initial: 'setup',
  data: {
    roomId: '',
  },
  transitions: {
    setup: {
      join(data, message) {
        data.roomId = message.roomId;
        return 'waiting';
      },
    },
  },
});

app.controller('SessionCtrl', function (sessionMachine) {
  this.session = sessionMachine;
});

You can also provide a resolvable config factory so it can read injectables at registration time:

function sessionMachineConfig(appSettings) {
  return {
    initial: appSettings.initialMode,
    data: {
      roomId: '',
      error: '',
    },
    transitions: {
      setup: {
        join(data, message) {
          data.roomId = message.roomId;

          return 'waiting';
        },
      },
    },
  };
}

sessionMachineConfig.$inject = ['appSettings'];

app.machine('sessionMachine', sessionMachineConfig);

This is useful when defaults depend on environment, user state, or persisted configuration.

Named machines are regular DI services. The injector creates one machine instance and reuses it for that injectable name.

Template API

The machine exposes a small template-friendly API:

<button ng-disabled="!$ctrl.session.matches('setup')">Random</button>

<div ng-show="$ctrl.session.matches('waiting')">Waiting for opponent...</div>

<span>{{ $ctrl.session.data.roomId }}</span>

Runtime API

session.current; // current mode string
session.data; // reactive data object
session.matches('setup'); // true when current mode is setup
session.can('join', payload); // true when current mode can run join
session.send('join', payload);
session.snapshot(); // structured clone of { current, data }
session.restore(snapshot);

send(type, payload) returns true when a transition entry exists for the current mode, its optional guard passes, and its handler runs. Missing transitions and blocked guarded transitions return false and do not throw. $machine does not record diagnostics for blocked or missing transitions; use $workflow when failures need structured evidence, retry, or recovery.

Transition return values:

  • return a mode string to change current
  • return false, undefined, an empty string, a non-string value, or no value to stay in the current mode

can(type, payload) checks the transition table and any optional guard for the current mode. It does not run the transition target. Guards should be side-effect-free because templates may call can() repeatedly.

Guarded Transitions

Use a transition descriptor when a transition has a cheap synchronous condition that templates or controllers should be able to ask about through can():

const session = $machine({
  initial: 'setup',
  data: {
    roomId: '',
    inviteCode: '',
  },
  transitions: {
    setup: {
      join: {
        guard(data, message) {
          return !!data.inviteCode && message.roomId !== '';
        },
        target(data, message) {
          data.roomId = message.roomId;

          return 'waiting';
        },
      },
    },
  },
});

session.can('join', { roomId: 'abc' }); // evaluates guard only
session.send('join', { roomId: 'abc' }); // runs target when guard passes

The original function shorthand remains valid:

join(data, message) {
  data.roomId = message.roomId;

  return 'waiting';
}

Guards and transitions are synchronous. Put async work, retries, diagnostics, and command recovery in $workflow, then use a machine transition to project the resulting mode/data into the UI.

Example: Tic Tac Toe

A machine is useful when user actions must follow a small set of legal modes. This example keeps a tic tac toe game in playing until a move produces a win or draw. Once the machine reaches xWon, oWon, or draw, there is no move transition for that mode, so further moves return false.

app.controller('GameCtrl', function ($machine) {
  const wins = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  const winnerOf = (board) => {
    for (const [a, b, c] of wins) {
      const mark = board[a];

      if (mark !== '-' && mark === board[b] && mark === board[c]) {
        return mark;
      }
    }

    return '';
  };

  this.game = $machine({
    initial: 'playing',
    data: {
      board: ['-', '-', '-', '-', '-', '-', '-', '-', '-'],
      nextPlayer: 'X',
      winner: '',
      moveCount: 0,
      lastError: '',
    },
    transitions: {
      playing: {
        move(data, { index }) {
          if (
            !Number.isInteger(index) ||
            index < 0 ||
            index >= data.board.length ||
            data.board[index] !== '-'
          ) {
            data.lastError = 'invalid_move';
            return false;
          }

          const player = data.nextPlayer;

          data.board[index] = player;
          data.moveCount += 1;
          data.lastError = '';

          const winner = winnerOf(data.board);

          if (winner) {
            data.winner = winner;
            return winner === 'X' ? 'xWon' : 'oWon';
          }

          if (data.moveCount === data.board.length) {
            return 'draw';
          }

          data.nextPlayer = player === 'X' ? 'O' : 'X';
          return 'playing';
        },
      },
    },
  });
});
<button
  ng-repeat="cell in $ctrl.game.data.board track by $index"
  ng-disabled="!$ctrl.game.matches('playing') || cell !== '-'"
  ng-click="$ctrl.game.send('move', { index: $index })"
>
  {{ cell }}
</button>

<p ng-show="$ctrl.game.matches('playing')">
  Next: {{ $ctrl.game.data.nextPlayer }}
</p>

<p ng-show="$ctrl.game.data.winner">Winner: {{ $ctrl.game.data.winner }}</p>

The transition owns both validation and projection. A valid move mutates data.board, updates whose turn is next, and returns the next mode. An invalid move records lastError and returns false, leaving current unchanged. Machine transitions do not roll back data mutations when they return false or throw.

Persist With Transition Hooks

Transition hooks are a convenient place to persist the game after each handled move. The hook runs after current has been updated, so the snapshot contains the terminal mode when the move ends the game.

app.controller('GameCtrl', function ($machine) {
  const storageKey = 'tic-tac-toe';

  this.game = $machine({
    initial: 'playing',
    data: {
      board: ['-', '-', '-', '-', '-', '-', '-', '-', '-'],
      nextPlayer: 'X',
      winner: '',
      moveCount: 0,
      lastError: '',
    },
    transitions: {
      playing: {
        move(data, { index }) {
          // Same move logic as above.
        },
      },
    },
    hooks: {
      transition({ machine }) {
        localStorage.setItem(storageKey, JSON.stringify(machine.snapshot()));
      },
    },
  });

  const saved = localStorage.getItem(storageKey);

  if (saved) {
    this.game.restore(JSON.parse(saved));
  }
});

restore() does not run hooks, so loading the saved game will not immediately write another snapshot. The next successful send() call will persist the updated machine state.

Snapshots

Use snapshot() to capture the durable machine state. A snapshot is a structured clone of { current, data }:

const snapshot = session.snapshot();

localStorage.setItem('session', JSON.stringify(snapshot));

A snapshot contains only current and data. It has no version or machine id, and it does not include transitions or hooks; those still come from the machine config. Use $workflow or a workflow supervisor when persisted recovery needs versioning, diagnostics, history, retry, or migration.

The clone uses the browser’s structuredClone() behavior. Values such as objects, arrays, Dates, Maps, Sets, typed arrays, and cycles can be cloned. Functions, DOM nodes, and other non-cloneable values will throw the native structured clone error.

JSON persistence is a narrower contract. Use JSON.stringify(snapshot) only when data is JSON-compatible, or add your own encode/decode layer before storing the snapshot.

Use restore(snapshot) to recover an existing machine:

const snapshot = JSON.parse(localStorage.getItem('session'));

session.restore(snapshot);

restore() mutates the existing data object in place so scope proxies and templates keep their identity. Keys missing from the snapshot are removed, nested plain objects are merged in place, and non-plain values like arrays are replaced with cloned snapshot values. Restore does not run transition hooks.

Hooks

Use hooks.enter and hooks.exit for mode-specific effects. Use hooks.transition for logging or cross-cutting work that should run after any handled transition, including same-mode transitions:

const session = $machine({
  initial: 'setup',
  data: {
    status: 'idle',
  },
  transitions: {
    setup: {
      join() {
        return 'waiting';
      },
    },
    waiting: {
      matched() {
        return 'playing';
      },
    },
  },
  hooks: {
    exit: {
      setup({ data }) {
        data.status = 'joining';
      },
    },
    enter: {
      waiting({ data }) {
        data.status = 'waiting';
      },
    },
    transition({ type, from, to }) {
      console.log(`${type}: ${from} -> ${to}`);
    },
  },
});

Hook context contains type, from, to, payload, data, and machine. exit runs before current changes, enter runs after current changes, and transition runs after mode-specific hooks. Missing transitions do not run hooks.

Hooks run synchronously inside the same send() batch. A hook may call machine.send() to run another valid transition; nested sends are batched with the outer transition. If a transition or hook throws, the error is rethrown, prior data/current mutations are kept, and AngularTS still schedules bound scopes for the keys touched by the attempted transition.

Scope Ownership

$machine(config) does not tie the machine to the lifetime of any one scope. The machine stores durable mode and data internally. When an AngularTS scope proxy wraps the machine, the machine registers with that proxy and uses its scheduler for reactive template updates and batched send() calls.

That means a machine can be created once, assigned to a component, survive that component being destroyed, and later be assigned to another scope.

An explicit $machine($scope, config) call is also supported when a caller wants to bind the machine to a scope immediately, but assigning the machine to a controller or scope property is usually enough. Named machines registered with module.machine() follow the same rule: they bind when a scope proxy observes them.

TypeScript

import { defineMachine } from '@angular-wave/angular.ts';

type SessionData = {
  roomId: string;
  error: string;
};

type SessionEvents = {
  join: { roomId: string };
  fail: string;
  reset: undefined;
};

const config = defineMachine<SessionData, SessionEvents>({
  initial: 'setup',
  data: {
    roomId: '',
    error: '',
  } satisfies SessionData,
  transitions: {
    setup: {
      join(data, message) {
        data.roomId = message.roomId;
        return 'waiting';
      },
    },
  },
});

const session = $machine(config);

session.send('join', { roomId: 'abc' });
session.send('reset');

Machine definitions are strict by default in TypeScript. If no event map is provided, send() has no valid event names and transition maps accept no event keys. Use defineMachine<Data, Events>() for checked event names and payloads, or opt into ng.MachineEventMap when you intentionally need dynamic event names.

12 - $parse

URL normalization for HTML5/hashbang modes

13 - $rest

Typed REST resource client

The $rest service creates typed resource clients on top of a REST backend. Use this page for the usage model and examples. Exact class members, method signatures, return types, and configuration interfaces live in TypeDoc:

Creating A Resource

Inject $rest and call it with a base URL:

const posts = $rest<Post, number>('/api/posts');

The returned RestService supports common CRUD workflows:

const all = await posts.list();
const one = await posts.get(42);
const created = await posts.create({ title: 'Hello' } as Post);
const updated = await posts.update(42, { title: 'Updated' });
const deleted = await posts.delete(42);

Entity Mapping

Pass an entity class when raw JSON should be converted into richer objects.

class Post {
  id!: number;
  title!: string;
  createdAt: Date;

  constructor(raw: any) {
    Object.assign(this, raw);
    this.createdAt = new Date(raw.created_at);
  }
}

const posts = $rest<Post, number>('/api/posts', Post);
const post = await posts.get(1);

When an entity class is supplied, response objects are passed through new entityClass(data) before they are returned.

Request Options

The third factory argument is merged into backend requests. With the default HTTP backend, use it for headers, credentials, cache options, transforms, or custom param serialization.

const posts = $rest<Post, number>('/api/posts', Post, {
  headers: { 'X-Tenant': tenantId },
  withCredentials: true,
});

Pass backend when a resource should use a custom backend. The backend receives a normalized RestRequest with expanded URLs, params, request data, collection URL, and resource id. This is the extension point for tests, local persistence, IndexedDB, the browser Cache API, or composed network/cache behavior.

For cached reads, wrap the HTTP backend with CachedRestBackend:

import {
  CachedRestBackend,
  HttpRestBackend,
} from '@angular-wave/angular.ts/services/rest';
import type {
  RestCacheStore,
  RestResponse,
} from '@angular-wave/angular.ts/services/rest';

class MapRestCacheStore implements RestCacheStore {
  private cache = new Map<string, RestResponse<unknown>>();

  async get<T>(key: string): Promise<RestResponse<T> | undefined> {
    return this.cache.get(key) as RestResponse<T> | undefined;
  }

  async set<T>(key: string, response: RestResponse<T>): Promise<void> {
    this.cache.set(key, response as RestResponse<unknown>);
  }

  async delete(key: string): Promise<void> {
    this.cache.delete(key);
  }

  async deletePrefix(prefix: string): Promise<void> {
    for (const key of this.cache.keys()) {
      if (key.startsWith(prefix)) {
        this.cache.delete(key);
      }
    }
  }
}

const posts = $rest<Post, number>('/api/posts', Post, {
  backend: new CachedRestBackend({
    network: new HttpRestBackend($http),
    cache: new MapRestCacheStore(),
    strategy: 'network-first',
  }),
});

createRestCacheKey() is used internally by CachedRestBackend; it is not part of the top-level AngularTS namespace. Cache stores receive the final key string through the RestCacheStore methods and should treat it as opaque.

Request Bodies

When the default HTTP backend is used, request data is serialized by $http. Scope proxies are deproxied before JSON serialization, so proxy helper properties such as $target, $handler, and $proxy are not sent to the server. AngularTS-generated repeat identity is stored in internal metadata, not on the object, so it is not included in write payloads either.

Explicit application-owned fields are still normal data. If your model defines a property such as $hashKey, $rest does not remove it.

URI Templates

Resource URLs support RFC 6570 URI templates. Variables wrapped in braces are expanded from the params passed to methods such as list().

const repos = $rest<Repo>('/api/{org}/repos');

const angularRepos = await repos.list({
  org: 'angular-wave',
  type: 'public',
});

The org value expands into the path. Remaining params are forwarded to $http as query parameters.

Provider Registration

Register definitions during configuration with $restProvider.rest() when a resource should be available from shared setup code.

angular.module('app', []).config(($restProvider) => {
  $restProvider.rest('posts', '/api/posts', Post);
});

Demo

The CRUD demo at /src/services/rest/rest-crud-demo.html talks to the Go demo backend through /api/tasks. It uses ng-repeat for rows, $rest for CRUD operations, and a cache strategy toggle for network-first, cache-first, and stale-while-revalidate.

14 - $rootElement

URL normalization for HTML5/hashbang modes

15 - $rootScope

Root scope service

16 - $sce

Strict Contextual Escaping service

$sceProvider and $sce Documentation

$sceProvider

The $sceProvider provider allows developers to configure the $sce service.

  • Enable/disable Strict Contextual Escaping (SCE) in a module
  • Override the default implementation with a custom delegate

Read more about Strict Contextual Escaping (SCE).


$sce

$sce is a service that provides Strict Contextual Escaping (SCE) services to AngularTS.


Strict Contextual Escaping

Overview

Strict Contextual Escaping (SCE) is a mode in which AngularTS constrains bindings to only render trusted values. Its goal is to:

  • Help you write secure-by-default code.
  • Simplify auditing for vulnerabilities such as XSS and clickjacking.

By default, AngularTS treats all values as untrusted in HTML or sensitive URL bindings. When binding untrusted values, AngularTS will:

  • Sanitize or validate them based on context.
  • Or throw an error if it cannot guarantee safety.

Example — ng-bind-html renders its value directly as HTML (its “context”). If the input is untrusted, AngularTS will sanitize or reject it.

To bypass sanitization, you must mark a value as trusted before binding it.

Note: Since version 1.2, AngularTS ships with SCE enabled by default.


Example: Binding in a Privileged Context

<input ng-model="userHtml" aria-label="User input" />
<div ng-bind-html="userHtml"></div>

If SCE is disabled, this allows arbitrary HTML injection — a serious XSS risk.

To safely render user content, you should sanitize the HTML (on the server or client) before binding it.

SCE ensures that only trusted, validated, or sanitized values are rendered.

You can mark trusted values explicitly using:

$sce.trustAs(context, value);

or shorthand methods such as:

$sce.trustAsHtml(value);
$sce.trustAsUrl(value);
$sce.trustAsResourceUrl(value);

How It Works

Directives and Angular internals bind trusted values using:

$sce.getTrusted(context, value);

Example: the ngBindHtml directive uses $sce.parseAsHtml internally:

let ngBindHtmlDirective = [
  '$sce',
  function ($sce) {
    return function (scope, element, attr) {
      scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function (value) {
        element.html(value || '');
      });
    };
  },
];

Impact on Loading Templates

SCE affects both:

  • The ng-include directive
  • The templateUrl property in directives

By default, AngularTS loads templates only from the same domain and protocol as the main document.
It uses:

$sce.getTrustedResourceUrl(url);

To allow templates from other domains, use:

  • $sceDelegateProvider.trustedResourceUrlList
  • Or $sce.trustAsResourceUrl(url)

Note: Browser CORS and Same-Origin policies still apply.


Is This Too Much Overhead?

SCE applies only to interpolation expressions.

Constant literals are automatically trusted, e.g.:

<div ng-bind-html="'<b>implicitly trusted</b>'"></div>

If the ngSanitize module is included, $sceDelegate will use $sanitize to clean untrusted HTML automatically.

AngularTS’ default $sceDelegate allows loading from your app’s domain, blocking others unless explicitly whitelisted.

This small overhead provides major security benefits and simplifies auditing.


Supported Trusted Contexts

ContextDescription
$sce.HTMLSafe HTML (used by ng-bind-html).
$sce.CSSSafe CSS. Currently unused.
$sce.MEDIA_URLSafe media URLs (auto-sanitized).
$sce.URLSafe navigable URLs.
$sce.RESOURCE_URLSafe resource URLs (used in ng-include, iframe, etc.).
$sce.JSSafe JavaScript for execution.

⚠️ Before AngularTS 1.7.0, a[href] and img[src] sanitized directly.
As of 1.7.0, these now use $sce.URL and $sce.MEDIA_URL respectively.


Resource URL List Patterns

Trusted and banned resource URL lists accept:

  • 'self' → matches same domain & protocol
  • Strings with wildcards:
    • * → matches within a single path segment
    • ** → matches across path segments (use carefully)
  • Regular expressions (RegExp)

Caution: Regex patterns are powerful but harder to maintain — use only when necessary.


You can disable SCE globally — though this is strongly discouraged.

angular.module('myAppWithSceDisabled', []).config(function ($sceProvider) {
  // Completely disable SCE. For demonstration purposes only!
  // Do not use in new projects or libraries.
  $sceProvider.enabled(false);
});

17 - $templateCache

Map object for storing templates

$templateCache is a Map object created by $templateCacheProvider.

The first time a template is used, it is loaded in the template cache for quick retrieval. You can load templates directly into the cache in a script tag by using $templateRequest or by consuming the $templateCache service directly.

Adding via the script tag:

Example

<script type="text/ng-template" id="templateId.html">
  <p>This is the content of the template</p>
</script>

Note: the script tag containing the template does not need to be included in the head of the document, but it must be a descendent of the $rootElement (e.g. element with ng-app attribute), otherwise the template will be ignored.

Adding via the $templateCache service:

Example

const myApp = angular.module('myApp', []).run(($templateCache) => {
  $templateCache.set('templateId.html', 'This is the content of the template');
});

To retrieve the template, simply use it in your component:

myApp.component('myComponent', {
  templateUrl: 'templateId.html',
});

or include it with ng-include:

<div ng-include="'templateId.html`"></div>

or get it via the $templateCache service:

myApp.controller(
  'Test',
  class {
    constructor($templateCache) {
      const tmp = $templateCache.get('templateId.html');
    }
  },
);

18 - $templateRequest

Template request service

19 - $url

URL management for state transitions

20 - $workflow

Inspectable command workflows built on reactive mode machines

$workflow

$workflow adds command boundaries, diagnostics, history, snapshots, restore, retry, and repeat on top of $machine.

Use $machine when you only need legal modes and transitions. Use $workflow when external work needs to be run, inspected, repaired, repeated, or handed to another process as JSON.

Create a Workflow

Inject $workflow and assign the workflow to a controller or scope property:

app.controller('DocsCtrl', function ($workflow) {
  this.build = $workflow({
    id: 'docs-build',
    initial: 'idle',
    data: {
      status: 'idle',
      output: '',
    },
    transitions: {
      idle: {
        start(data) {
          data.status = 'running';
          return 'running';
        },
      },
      running: {
        complete(data, output) {
          data.status = 'complete';
          data.output = output;
          return 'complete';
        },
        fail(data, reason) {
          data.status = reason;
          return 'failed';
        },
      },
    },
    commands: {
      build({ workflow, data, input }) {
        workflow.send('start');
        data.output = String(input);
        workflow.send('complete', data.output);

        return {
          ok: true,
          output: {
            file: data.output,
          },
        };
      },
    },
  });
});

workflow.current is the current mode. workflow.data is reactive data for templates. workflow.run(name, input) is the explicit boundary for work that can succeed, fail, or be retried.

Commands may be synchronous or async. run(), retry(), and repeat() always return a promise that resolves to a normalized WorkflowCommandResult.

Register a Named Workflow

Use module.workflow(name, config) when a workflow should be injectable by name:

app.workflow('docsWorkflow', {
  id: 'docs',
  initial: 'idle',
  data: {
    runs: 0,
  },
  transitions: {
    idle: {
      start(data) {
        data.runs += 1;
        return 'running';
      },
    },
  },
});

app.controller('DocsCtrl', function (docsWorkflow) {
  this.workflow = docsWorkflow;
});

You can make workflow registration resumable and environment-driven by passing a config factory:

function docsWorkflowConfig(buildConfig) {
  return {
    id: buildConfig.workflowId,
    initial: buildConfig.initialMode,
    data: {
      runs: 0,
      status: buildConfig.initialMode,
    },
    transitions: {
      idle: {
        start(data) {
          data.runs += 1;
          data.status = 'running';

          return 'running';
        },
      },
    },
    commands: {},
  };
}

docsWorkflowConfig.$inject = ['buildConfig'];

app.workflow('docsWorkflow', docsWorkflowConfig);

Named workflows are DI singletons for an injector. Observing scopes can be destroyed without destroying the workflow instance.

Command Diagnostics

Commands return a WorkflowCommandResult, or they can throw. Thrown values are converted to structured diagnostics and partial data mutations are preserved:

const result = await workflow.run('publish', 'index.html');

if (!result.ok) {
  for (const diagnostic of result.diagnostics) {
    console.warn(diagnostic.code, diagnostic.message);
  }
}

Diagnostics are safe to serialize:

const diagnosticsJson = JSON.stringify(workflow.diagnostics);

Snapshot And Restore

Snapshots are the JSON handoff format:

const snapshot = workflow.snapshot();

localStorage.setItem('docsWorkflow', JSON.stringify(snapshot));

const restored = JSON.parse(localStorage.getItem('docsWorkflow'));

workflow.restore(restored);

A snapshot contains:

{
  version: 1,
  id: 'docs-build',
  current: 'failed',
  data: {},
  diagnostics: [],
  history: []
}

restore(snapshot) requires version 1 and a matching workflow id. Restored diagnostics and history entries are normalized into the same JSON-safe shape that live commands produce. Restored history IDs are normalized to unique positive integers. Command inputs and outputs in workflow.history and snapshots are stored as JSON-safe projections. Live workflows still retry or repeat with the original input value; after restore(snapshot), retry and repeat use the serialized input from the snapshot.

Use migrateSnapshot(snapshot) to restore older persisted shapes:

const workflow = $workflow({
  id: 'docs-build',
  initial: 'idle',
  data: {
    title: '',
  },
  transitions: {},
  migrateSnapshot(snapshot) {
    return {
      version: 1,
      id: 'docs-build',
      current: snapshot.state,
      data: {
        title: snapshot.title,
      },
      diagnostics: [],
      history: [],
    };
  },
});

Retry, Repeat, And Repair

retry(commandName?) reruns the latest failed command with its original input:

const retryResult = await workflow.retry('publish');

repeat(commandName?) reruns the latest completed command with its original input:

const repeatResult = await workflow.repeat('publish');

Repair is intentionally configured as a normal command instead of a magic built-in policy:

app.controller('DocsCtrl', function ($workflow) {
  this.workflow = $workflow({
    id: 'repairable-docs',
    initial: 'idle',
    data: {
      title: '',
    },
    transitions: {
      idle: {
        validate(data) {
          return data.title ? 'complete' : 'failed';
        },
      },
      failed: {
        complete() {
          return 'complete';
        },
      },
    },
    commands: {
      validate({ workflow }) {
        workflow.send('validate');

        return workflow.matches('complete')
          ? { ok: true }
          : {
              ok: false,
              diagnostics: [
                {
                  code: 'docs.missingTitle',
                  message: 'Missing title.',
                  recoverable: true,
                },
              ],
            };
      },
      repair({ workflow, data, input }) {
        data.title = String(input);
        workflow.send('complete');

        return {
          ok: true,
        };
      },
    },
  });
});
await workflow.run('validate');
await workflow.run('repair', 'Guide');

Diagnostics and history are append-only in v1. Repair, retry, and repeat add evidence; they do not erase the commands that made recovery or replay necessary.

Production Policies

Concurrent command calls are allowed by default. Set concurrency to reject or queue overlapping runs for the same command:

const workflow = $workflow({
  id: 'publish',
  concurrency: 'queue',
  initial: 'idle',
  data: {},
  transitions: {},
  commands: {
    publish() {
      return { ok: true };
    },
  },
});

Per-run options can override the workflow default:

await workflow.run('publish', payload, { concurrency: 'reject' });

Use commandTimeout or per-run timeout to fail commands that exceed a time budget. Timeout and cancellation resolve the command result with diagnostics; commands receive an AbortSignal so async work can stop early:

const workflow = $workflow({
  id: 'publish',
  commandTimeout: 5000,
  initial: 'idle',
  data: {},
  transitions: {},
  commands: {
    async publish({ cleanup, signal }) {
      const controller = new AbortController();

      cleanup(() => controller.abort());
      signal.addEventListener('abort', () => controller.abort(), {
        once: true,
      });

      await fetch('/publish', { signal: controller.signal });

      return { ok: true };
    },
  },
});

workflow.cancel('publish');

cleanup(callback) runs after the command resolves, fails, times out, or is cancelled. Use it for timers, event listeners, observers, and request controllers created by the command.

restore(snapshot) is a hard recovery boundary. It cancels running commands, prevents queued commands from starting, and ignores late writes from cancelled command continuations. Command code should still observe signal.aborted so it can release external resources promptly.

Diagnostics and history are bounded to 1000 entries by default. Use diagnosticLimit and historyLimit to choose different bounds:

const workflow = $workflow({
  id: 'bounded',
  diagnosticLimit: 100,
  historyLimit: 100,
  initial: 'idle',
  data: {},
  transitions: {},
});

Runtime API

workflow.current;
workflow.data;
workflow.diagnostics;
workflow.history;
workflow.matches('idle');
workflow.can('start');
workflow.send('start');
workflow.run('publish', payload, { timeout: 5000 });
workflow.retry('publish', { concurrency: 'reject' });
workflow.repeat('publish');
workflow.cancel('publish');
workflow.snapshot();
workflow.restore(snapshot);

TypeScript callers can specify the expected command output type at the command boundary:

const result = await workflow.run<{ file: string }>('publish', payload);

if (result.ok) {
  result.output?.file;
}

Workflow definitions are strict by default in TypeScript. If no event or command map is provided, send(), run(), retry(), and repeat() have no valid names. Use defineWorkflow<Data, Events, Commands>() and defineCommand() for checked event names, command names, inputs, and outputs:

import { defineCommand, defineWorkflow } from '@angular-wave/angular.ts';

type DocsData = {
  output: string;
};

type DocsEvents = {
  complete: { output: string };
};

type DocsCommands = {
  publish: ng.WorkflowCommand<DocsData, string, { file: string }, DocsEvents>;
};

const config = defineWorkflow<DocsData, DocsEvents, DocsCommands>({
  id: 'docs',
  initial: 'idle',
  data: {
    output: '',
  } satisfies DocsData,
  transitions: {
    idle: {
      complete(data, payload) {
        data.output = payload.output;
        return 'complete';
      },
    },
  },
  commands: {
    publish: defineCommand<DocsData, string, { file: string }, DocsEvents>(
      ({ workflow, input }) => {
        workflow.send('complete', { output: input });

        return {
          ok: true,
          output: {
            file: input,
          },
        };
      },
    ),
  },
});

const workflow = $workflow(config);
const result = await workflow.run('publish', 'index.html');

When a command map is provided, commands is required and every declared command key must be present and callable. This keeps workflow.run('name') aligned with the runtime command table.

When a command calls another command, pass the full command map to defineCommand() so context.workflow.run() is checked too:

type DocsCommands = {
  build: ng.WorkflowCommand<
    DocsData,
    string,
    { file: string },
    DocsEvents,
    DocsCommands
  >;
  publish: ng.WorkflowCommand<
    DocsData,
    { file: string },
    { url: string },
    DocsEvents,
    DocsCommands
  >;
};

const build = defineCommand<
  DocsData,
  string,
  { file: string },
  DocsEvents,
  DocsCommands
>(({ workflow, input }) => {
  workflow.run('publish', { file: input });

  return {
    ok: true,
    output: {
      file: input,
    },
  };
});

Use ng.WorkflowCommandMap only when you intentionally need dynamic command names. Dynamic command inputs are typed as unknown; narrow the value inside the command before using it.