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

Return to the regular view of this page.

Service guides

Learn how reusable services handle models, HTTP, REST, realtime messages, routing, and browser storage.

A service is one reusable object or function that AngularTS can provide to controllers, components, directives, and other services. The act of requesting that service by name is called dependency injection.

Start with shared models, then choose the guide for the feature you need. Use the service reference when you already know a service token such as $http.

1 - Models

Use app.model() to declare shared reactive application state as an injectable service.

app.model(name, initial) declares a shared reactive model service. The injected value is the proxied model object itself. Use models for shared, declarative application state that should outlive one DOM scope tree.

Prefer the factory form:

const app = angular.createModule('demo', []);

app.model('user', () => ({
  name: 'John',
  authenticated: false,
}));

Then inject the model by name. Assigning the injected model to a controller property or $scope property makes the model readable from templates:

class HeaderController {
  static $inject = ['user'];

  constructor(public user: { name: string; authenticated: boolean }) {}
}
<section ng-controller="HeaderController as header">
  <strong>{{ header.user.name }}</strong>
  <span>{{ header.user.authenticated ? "signed in" : "guest" }}</span>
</section>

When the model changes, DOM interpolation, ng-bind, and directive expressions that read that model update automatically. Mutating the model proxy is sufficient to schedule every affected observer.

Model data belongs to the app context, not to $rootScope. Every root scope managed by the same app context resolves the same model instance. Destroying a root scope removes that root’s observations, but the model data survives while the app context remains alive.

Access From External Code

After the app has started with angular.bootstrap() or angular.injector(), use angular.getModel(name) to access a model from external code:

const user = angular.getModel<{ name: string; authenticated: boolean }>('user');

// An external callback can update the reactive model directly.
setTimeout(() => {
  user.name = 'Jane';
}, 1000);

The first lookup initializes the model if needed. Subsequent lookups and dependency injection return the same instance. Mutations schedule updates for every observing view without an explicit digest or $apply() call.

The getter only accepts models declared by loaded modules. Unknown names, ordinary service names, and access before app initialization or after app destruction throw errors. Use the Angular runtime instance that owns the app when working with multiple applications.

Models Versus Local Scope

Use models for shared domain state such as user, cart, session, and settings.

Use $scope for view-local state that only exists to support one template, such as an open menu flag, a focused row id, or transient form display state.

Use $eventBus for cross-boundary messages, $machine for finite-state transitions, and $workflow for command orchestration.

Template Reactivity

App models can be bound directly:

<p>{{ user.name }}</p>
<p ng-bind="user.profile.name"></p>
<p>{{ cart.items.length }}</p>

Controller-as bindings are first-class:

<section ng-controller="ProfileCtrl as profile">
  <input ng-model="profile.user.name" />
  <button ng-click="profile.signIn()">Sign in</button>
</section>

<section ng-controller="HeaderCtrl as header">
  <strong>{{ header.user.name }}</strong>
  <span>{{ header.user.authenticated ? "signed in" : "guest" }}</span>
</section>

Nested object reads, parent replacement, array length reads, array mutation, and property deletion follow the same scope proxy semantics as local scope state.

Model Shape

Root models must start as plain objects. Primitives, null, arrays, and class instances are rejected as root model values. Nested values follow AngularTS scope proxy behavior. Duplicate model names in one module are rejected.

Model factories should initialize state only. Put sockets, workers, timers, DOM listeners, and other side effects in services, workflows, machines, or policy-backed browser abstractions.

Model Methods

Injected models are proxy-backed objects. They expose their own data properties, the scope-proxy methods used by AngularTS reactivity, and model lifecycle methods for whole-model coordination.

Those methods exist because models are implemented with the same proxy engine as scopes. Application code should still treat a model as app-owned state, not as a DOM scope.

Model-specific methods use ordinary method names:

  • snapshot() creates a plain snapshot of model state.
  • restore(snapshot, options?) restores model state and notifies observers.
  • sync(target) synchronizes model updates with a direct target or injectable target factory.

sync() is intended for integrations such as Unity, WebSocket sessions, workers, CRDT documents, and custom stores. The external runtime may update the model imperatively, but AngularTS keeps the rest of the app declarative:

runtime event
  -> model assignment
  -> model.sync(...)
  -> websocket/store/worker/crdt target

Target factories should use normal AngularTS injectable form. Bare string service-name shortcuts are intentionally avoided for consistency:

playerModel.sync(['playerSocketSync', (playerSocketSync) => playerSocketSync]);
settingsModel.sync([
  '$cookie',
  ($cookie) => ({
    restore: () => $cookie.getObject('settings'),
    write: (snapshot) => $cookie.putObject('settings', snapshot),
  }),
]);

A managed worker already exposes a model sync target, so no callback bridge is required:

app.worker('physicsWorker', '/workers/physics.js', { restart: true });

app.run([
  'playerModel',
  'physicsWorker',
  (playerModel, physicsWorker) =>
    playerModel.sync(physicsWorker.model('player')),
]);

The worker exchanges standard ng.WorkerModelMessage envelopes. Incoming snapshots update every DOM scope that consumes the model, while model mutations are sent back with change metadata and loop-prevention origin tracking.

sync() batches synchronous model mutations, sends a plain snapshot plus modest change metadata, and uses origin metadata to prevent bidirectional sync loops. Failures should report through AngularTS exception handling by default.

Durable storage should start as an explicit sync() target. A settings model can restore from $cookie and write future snapshots back to the same storage boundary without adding persistence behavior to the model declaration itself.

A sync target can implement any combination of restore, write, receive, and dispose:

playerModel.sync([
  'playerSocketSync',
  (socketSync) => ({
    restore: () => socketSync.latest(),
    write: (snapshot, change) => socketSync.send({ snapshot, change }),
    receive: (apply) => socketSync.onMessage((snapshot) => apply(snapshot)),
    dispose: () => socketSync.close(),
  }),
]);

sync() checks the target at runtime. A target must implement at least one recognized operation, and every supplied operation must be a function. restore() results and snapshots passed to receive() must be plain objects; invalid external values are reported through the configured sync failure policy without partially updating the model. receive() must return either a disposer function or undefined.

Runtime validation establishes the model container contract, but it cannot recover erased TypeScript property types. Sync targets remain responsible for decoding and validating domain fields received from untrusted external data.

restore(snapshot, { mode: "merge" }) keeps existing model keys and writes the incoming keys. The default mode replaces the model root’s keys with the snapshot’s keys.

Executable Sample

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./user-model.js";
    </script>
  </head>
  <body ng-app="modelDemo">
    <section ng-controller="ProfileCtrl as profile">
      <label>
        Name
        <input
          ng-model="profile.user.name"
          aria-label="Name"
          data-testid="profile-name"
        />
      </label>
      <button ng-click="profile.signIn()">Sign in</button>
      <button ng-click="profile.reset()">Reset</button>
    </section>

    <section ng-controller="HeaderCtrl as header">
      <strong data-testid="header-name">{{ header.user.name }}</strong>
      <span data-testid="header-status">
        {{ header.user.authenticated ? 'signed in' : 'guest' }}
      </span>
      <span data-testid="cart-count">{{ header.cart.items.length }}</span>
    </section>
  </body>
</html>
{{ header.user.name }} {{ header.user.authenticated ? 'signed in' : 'guest' }} {{ header.cart.items.length }}

HTML:

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./user-model.js";
    </script>
  </head>
  <body ng-app="modelDemo">
    <section ng-controller="ProfileCtrl as profile">
      <label>
        Name
        <input
          ng-model="profile.user.name"
          aria-label="Name"
          data-testid="profile-name"
        />
      </label>
      <button ng-click="profile.signIn()">Sign in</button>
      <button ng-click="profile.reset()">Reset</button>
    </section>

    <section ng-controller="HeaderCtrl as header">
      <strong data-testid="header-name">{{ header.user.name }}</strong>
      <span data-testid="header-status">
        {{ header.user.authenticated ? 'signed in' : 'guest' }}
      </span>
      <span data-testid="cart-count">{{ header.cart.items.length }}</span>
    </section>
  </body>
</html>

JavaScript:

window.angular
  .createModule('modelDemo', [])
  .model('user', () => ({
    name: 'John',
    authenticated: false,
  }))
  .model('cart', () => ({
    items: ['starter'],
  }))
  .controller(
    'ProfileCtrl',
    class {
      static $inject = ['user', 'cart'];

      constructor(user, cart) {
        this.user = user;
        this.cart = cart;
      }

      signIn() {
        this.user.authenticated = true;
        this.cart.items.push('welcome');
      }

      reset() {
        this.user.name = 'John';
        this.user.authenticated = false;
        this.cart.items = ['starter'];
      }
    },
  )
  .controller(
    'HeaderCtrl',
    class {
      static $inject = ['user', 'cart'];

      constructor(user, cart) {
        this.user = user;
        this.cart = cart;
      }
    },
  );

Sync Sample

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./settings-sync.js";
    </script>
  </head>
  <body ng-app="settingsSyncDemo" ng-controller="SettingsCtrl as settings">
    <label>
      Theme
      <select
        ng-model="settings.model.theme"
        aria-label="Theme"
        data-testid="theme"
      >
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </label>

    <label>
      Compact
      <input
        type="checkbox"
        ng-model="settings.model.compact"
        aria-label="Compact"
        data-testid="compact"
      />
    </label>

    <button ng-click="settings.reset()">Reset</button>

    <p data-testid="summary">
      {{ settings.model.theme }} /
      {{ settings.model.compact ? 'compact' : 'comfortable' }}
    </p>
    <p data-testid="sync-count">{{ settings.sync.writes }}</p>
  </body>
</html>

{{ settings.model.theme }} / {{ settings.model.compact ? 'compact' : 'comfortable' }}

{{ settings.sync.writes }}

HTML:

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./settings-sync.js";
    </script>
  </head>
  <body ng-app="settingsSyncDemo" ng-controller="SettingsCtrl as settings">
    <label>
      Theme
      <select
        ng-model="settings.model.theme"
        aria-label="Theme"
        data-testid="theme"
      >
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </label>

    <label>
      Compact
      <input
        type="checkbox"
        ng-model="settings.model.compact"
        aria-label="Compact"
        data-testid="compact"
      />
    </label>

    <button ng-click="settings.reset()">Reset</button>

    <p data-testid="summary">
      {{ settings.model.theme }} /
      {{ settings.model.compact ? 'compact' : 'comfortable' }}
    </p>
    <p data-testid="sync-count">{{ settings.sync.writes }}</p>
  </body>
</html>

JavaScript:

window.angular
  .createModule('settingsSyncDemo', [])
  .model('settingsModel', () => ({
    theme: 'light',
    compact: false,
  }))
  .model('settingsSyncModel', () => ({
    writes: 0,
  }))
  .controller(
    'SettingsCtrl',
    class {
      static $inject = ['settingsModel', 'settingsSyncModel', '$cookie', '$scope'];

      constructor(settingsModel, syncModel, $cookie, $scope) {
        this.model = settingsModel;
        this.sync = syncModel;
        this.$cookie = $cookie;
        this.stopSync = settingsModel.sync([
          '$cookie',
          ($cookieService) => ({
            restore: () => $cookieService.getObject('angular_ts_settings') ?? null,
            write: (snapshot) => {
              syncModel.writes += 1;
              $cookieService.putObject('angular_ts_settings', snapshot, {
                path: '/',
              });
            },
          }),
        ]);

        $scope.on('$destroy', this.stopSync);
      }

      reset() {
        this.$cookie.remove('angular_ts_settings', { path: '/' });
        this.model.restore({
          theme: 'light',
          compact: false,
        });
      }
    },
  );

2 - Making HTTP requests with the $http service

Send requests with the $http service and configure defaults, transforms, interceptors, and XSRF behavior.

Use $http for application HTTP calls when you need request configuration, response transforms, interceptors, or integration with AngularTS services.

This guide focuses on workflows. Exact call signatures and exported interfaces live in TypeDoc:

Basic Requests

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

Use a full request config when method, URL, headers, params, timeout, response type, or upload handlers need to be assembled together.

$http({
  method: 'POST',
  url: '/api/users',
  data: { name: 'Ada' },
  headers: { 'X-Trace': traceId },
}).then(({ data }) => {
  this.user = data;
});

Query Parameters

params are appended to the URL using $httpParamSerializer.

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

The default serializer repeats array keys, JSON-encodes object values, sorts keys alphabetically, and omits null, undefined, and function values.

Request Bodies

Plain objects are JSON serialized by default. FormData, Blob, and File values are sent as native browser payloads.

const formData = new FormData();
formData.append('avatar', fileInput.files[0]);

$http.post('/api/users/42/avatar', formData, {
  uploadEventHandlers: {
    progress(event: ProgressEvent) {
      this.progress = Math.round((event.loaded / event.total) * 100);
    },
  },
});

Headers And Defaults

Set application-wide defaults during module configuration:

angular.createModule('app', []).config({
  $http: {
    defaults: {
      headers: {
        common: {
          Authorization: 'Bearer token',
        },
      },
      withCredentials: true,
    },
  },
});

Runtime defaults are available through $http.defaults:

angular.getModule('app').run([
  '$http',
  ($http) => {
    $http.defaults.headers.common['X-App-Version'] = '2.1.0';
  },
]);

Interceptors

Interceptors centralize cross-cutting request and response behavior such as auth headers, retries, logging, and redirects.

angular.createModule('app', []).config({
  $http: {
    interceptors: [
      () => ({
        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);
        },
      }),
    ],
  },
});

Error Handling

Rejected requests use the same response object shape as successful requests. Use status for HTTP errors and xhrStatus for transport outcomes such as timeouts and aborts.

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

XSRF

$http can read a configured XSRF cookie and send it in a configured request header. Cross-origin APIs must be listed as trusted origins before AngularTS sends the token to them.

angular.createModule('app', []).config({
  $http: {
    xsrfTrustedOrigins: ['https://api.example.com'],
  },
});

Next Steps

3 - Decoupled Messaging

Use $eventBus for application-wide publish/subscribe messaging and scope events for scope-tree communication.

AngularTS has two messaging models:

  • $eventBus is an application-wide EventBus instance for decoupled communication, including messages from non-Angular code.
  • Scope events ($scope.on, $scope.emit, $scope.broadcast) travel through the scope tree and are best for parent/child communication.

Exact $eventBus method signatures live in TypeDoc:

Publish and subscribe

Inject $eventBus when a publisher and subscriber should not know about each other.

class CartService {
  static $inject = ['$eventBus'];

  constructor(private $eventBus: ng.EventBusService) {}

  addItem(product: Product, quantity: number) {
    this.eventBus.publish('cart:item-added', { product, quantity });
  }
}

class HeaderController {
  static $inject = ['$eventBus', '$scope'];

  cartCount = 0;

  constructor($eventBus: ng.EventBusService, $scope: ng.Scope) {
    const unsubscribe = $eventBus.subscribe('cart:item-added', () => {
      const view = $scope['$ctrl'] as HeaderController;

      view.cartCount += 1;
    });

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

subscribe() returns an unsubscribe function. Keep that function and call it during teardown, especially from long-lived services, directive controllers, or manually bootstrapped integrations.

One-time listeners

Use subscribeOnce() for initialization handshakes where only the first event matters.

class AnalyticsBootstrap {
  static $inject = ['$eventBus'];

  constructor($eventBus: ng.EventBusService) {
    $eventBus.subscribeOnce('analytics:ready', (sdk) => {
      sdk.track('session_start');
    });
  }
}

window.onAnalyticsReady = (sdk) => {
  angular.eventBus.publish('analytics:ready', sdk);
};

Async delivery

$eventBus schedules delivery with queueMicrotask. publish() returns after scheduling the event, and subscribers run after the current call stack.

$eventBus.subscribe('order:created', (order) => {
  console.log('subscriber', order.id);
});

$eventBus.publish('order:created', { id: 42 });
console.log('publisher finished');

This makes $eventBus useful for browser callbacks, WebSocket messages, Web Worker results, and other boundaries where you want the publisher to stay independent of Angular controller timing. When a subscriber changes view state, write through proxied scope or controller state so AngularTS can observe the assignment.

Error handling

If a subscriber throws, $eventBus forwards the value unchanged to $exceptionHandler. The default handler rethrows, so the current publication stops at that listener. Do not rely on later listeners running after an exception.

$eventBus.subscribe('order:created', () => {
  throw new Error('failed listener');
});

$eventBus.subscribe('order:created', logCreatedOrder);

Subscribers that need independent failure ownership should start an operation with its own result or rejection boundary instead of using listener order as a recovery mechanism.

Control delivery with a policy

Configure one callable policy when event delivery must follow an application-wide rule. The callback is contextually typed as EventDeliveryPolicy, so ordinary configuration does not require a separate decision type.

angular.createModule('admin', []).config({
  $eventBus: {
    deliveryPolicy(context) {
      return context.topic.startsWith('internal:') ? 'drop' : 'deliver';
    },
  },
});

Return "deliver" or "drop" for the common case. Return a structured policy decision when shared policy infrastructure needs metadata such as a reason or status.

Scope events

Use scope events when the relationship is already expressed by the scope tree.

MethodDirectionUse for
$scope.broadcast(event, args)Down to descendantsParent notifying child scopes
$scope.emit(event, args)Up toward $rootScopeChild notifying parents
$scope.on(event, handler)Current scope listenerLocal event handling and cleanup
$scope.broadcast('filter:changed', { status: 'active' });

$scope.on('filter:changed', (_event, filter) => {
  this.applyFilter(filter);
});

$scope.emit('child:ready');

Scope listeners are synchronous and return a deregistration function. Pair them with $destroy when the listener can outlive the current view.

const off = $scope.on('filter:changed', handler);
$scope.on('$destroy', off);

Choosing a messaging model

ConcernPrefer $eventBusPrefer scope events
Publisher and subscriber do not share a scope ancestryYesNo
Event comes from non-Angular codeYesNo
Communication is strictly parent/childUsually noYes
Delivery should be asyncYesNo
Listener cleanup should be tied to a scopeWorks with $destroyBuilt for it

Use $eventBus for cross-boundary messages such as WebSocket events, global notifications, analytics readiness, and application-level domain events. Use scope events for local component coordination.

4 - Real-Time Communication

Stream server events with $sse, exchange bidirectional messages with $websocket or $webTransport, and offload work with ng-worker and ng-wasm.

AngularTS provides five building blocks for real-time and compute-heavy work:

  • $sse for Server-Sent Events.
  • $websocket for bidirectional WebSocket connections.
  • $webTransport for HTTP/3 WebTransport sessions with datagrams and streams.
  • ng-worker for JavaScript work in a Web Worker.
  • ng-wasm for loading WebAssembly modules.

Exact service and connection signatures live in TypeDoc:

Server-Sent Events

$sse creates a managed EventSource connection. It handles query parameters, JSON message parsing, heartbeat detection, automatic reconnection, and clean shutdown.

class NewsFeedController {
  static $inject = ["$sse", "$scope"];

  items: NewsItem[] = [];
  private connection: ng.SseConnection;

  constructor($sse: ng.SseService, $scope: ng.Scope) {
    this.connection = $sse("/api/news/stream", {
      withCredentials: true,
      params: { category: "top" },
      retryDelay: 3000,
      heartbeatTimeout: 30000,
      onMessage: (item: NewsItem) => {
        const view = $scope["$ctrl"] as NewsFeedController;

        view.items = [item, ...view.items].slice(0, 50);
      },
      onReconnect: (attempt) => {
        console.log("SSE reconnect", attempt);
      },
    });

    $scope.on("$destroy", () => this.connection.close());
  }
}

EventSource does not support arbitrary custom headers in browsers. For authenticated streams, use cookies with withCredentials or include a short-lived token in query params.

WebSockets

$websocket returns a managed WebSocket connection with reconnects, heartbeat handling, message transforms, and send support.

class ChatController {
  static $inject = ["$websocket", "$scope"];

  messages: ChatMessage[] = [];
  private socket: ng.WebSocketConnection;

  constructor($websocket: ng.WebSocketService, $scope: ng.Scope) {
    this.socket = $websocket("wss://api.example.com/chat", {
      protocols: ["v1"],
      retryDelay: 2000,
      maxRetries: 20,
      onMessage: (message: ChatMessage) => {
        const view = $scope["$ctrl"] as ChatController;

        view.messages = [...view.messages, message];
      },
      onClose: (event) => {
        console.log("WebSocket closed", event.code);
      },
    });

    $scope.on("$destroy", () => this.socket.close());
  }

  send(text: string) {
    this.socket.send({ type: "message", text });
  }
}

send() serializes values as JSON before passing them to the native WebSocket.

WebTransport

$webTransport opens a browser-native WebTransport session. Use it when an endpoint can serve HTTP/3 and the client benefits from unreliable datagrams, reliable streams, or both in the same session.

class TelemetryController {
  static $inject = ["$webTransport", "$scope"];

  events: string[] = [];
  private session: ng.WebTransportConnection;

  constructor($webTransport: ng.WebTransportService, $scope: ng.Scope) {
    this.session = $webTransport("https://localhost:4433/webtransport", {
      reconnect: true,
      retryDelay: 500,
      maxRetries: 5,
      requireUnreliable: true,
      transformDatagram: (data) => new TextDecoder().decode(data),
      onDatagram: ({ message }) => {
        const view = $scope["$ctrl"] as TelemetryController;

        view.events = [...view.events, String(message)];
      },
      onReconnect: ({ connection }) => {
        return connection.sendText(JSON.stringify({ subscribe: "telemetry" }));
      },
    });
  }

  send(value: string) {
    return this.session.sendText(value);
  }
}

The service expects the browser WebTransport API to exist and requires an https: URL with an explicit port. The test backend exposes certificate hash metadata at /webtransport/cert-hash for local browser tests.

Reconnect is opt-in at the service layer. When enabled, the WebTransportConnection object stays stable while its native transport instance is replaced. Use onReconnect as the renegotiation hook for subscriptions, authentication messages, or other session state that the server does not remember across HTTP/3 sessions.

For template-level feeds, ng-web-transport connects on load by default and evaluates lifecycle expressions. data-mode="datagram" is the default; data-mode="stream" reads server-opened unidirectional streams.

<div
  ng-web-transport="transportUrl"
  data-config="transportConfig"
  data-mode="datagram"
  data-transform="json"
  data-as="session"
  data-reconnect="true"
  on-message="events.push($message)"
  on-reconnect="reconnects = $attempt"
  on-error="error = error"
></div>

data-transform accepts bytes, text, or json. Message expressions receive $connection, $data, $message, $event, and $text for text/json modes. Reconnect is opt-in with data-reconnect="true"; tune it with data-retry-delay and data-max-retries. on-reconnect runs after the replacement session is ready and receives $attempt, $connection, error, and $url.

Web Workers

Use ng-worker when a view action should run CPU-heavy JavaScript outside the main thread.

<button
  ng-worker="./workers/compress.js"
  data-params="vm.fileBuffer"
  on-result="vm.compressed = $result"
  on-error="vm.error = error"
  trigger="click"
>
  Compress
</button>

<span
  ng-worker="./workers/sensor-reader.js"
  interval="5000"
  on-result="vm.sensorData = $result"
></span>

The directive evaluates data-params, posts the result to the worker, and exposes worker responses as $result in on-result. Worker output is never interpreted as HTML. Bind it into scope or a model and let normal directives render the result.

Register expensive or shared workers once and reference their injectable handle. data-request enables correlated request-response delivery:

app.worker("compressWorker", "./workers/compress.js", {
  restart: true,
  maxRestarts: 3,
});
<button
  ng-worker
  data-handle="compressWorker"
  data-request
  data-params="vm.fileBuffer"
  on-result="vm.compressed = $result"
  on-error="vm.error = error"
>
  Compress
</button>

The directive subscribes to a shared handle but does not terminate it when the element scope is destroyed. Workers created directly from an ng-worker script path remain element-owned and are terminated with that scope.

A worker module receives values through self.onmessage and returns results through self.postMessage.

self.onmessage = function ({ data: { limit } }) {
  const sieve = new Uint8Array(limit + 1).fill(1);
  sieve[0] = sieve[1] = 0;

  for (let i = 2; i * i <= limit; i++) {
    if (sieve[i]) {
      for (let j = i * i; j <= limit; j += i) sieve[j] = 0;
    }
  }

  const primes = [];
  for (let i = 2; i <= limit; i++) {
    if (sieve[i]) primes.push(i);
  }

  self.postMessage(primes);
};

WebAssembly

ng-wasm loads a .wasm file and exposes its WasmResource on the scope under a configurable name. Await resource.ready before reading resource.exports.

<div
  ng-wasm
  src="/wasm/image-processor.wasm"
  as="imageProcessor"
></div>
const resource = $scope.imageProcessor;
await resource.ready;
const result = resource.exports.grayscale(pixelBuffer, width, height);

WebAssembly loading is asynchronous. Use the resource status in templates and await ready before calling exports from application code.

Choosing A Transport

NeedUse
Server pushes one-way updates$sse
Client and server both send messages$websocket
HTTP/3 datagrams or streams$webTransport
CPU-heavy JavaScriptng-worker
Compiled compute moduleng-wasm

5 - Typed REST Resources

Define typed REST endpoints once and use $rest for list, get, create, update, and delete flows backed by pluggable backends.

$rest wraps a REST backend with a small typed resource client. By default it uses $http; pass a custom backend when a resource should read from another data source or compose network and cache behavior.

Exact method signatures live in TypeDoc:

Configure shared defaults

Configure defaults before the application starts when every resource should share the same request behavior.

angular.createModule('demo', []).config({
  $rest: {
    defaults: {
      timeout: 5000,
      withCredentials: true,
    },
  },
});

Create a resource at runtime

Inject $rest anywhere you need a resource client. This keeps controllers and services focused on the workflow instead of repeating request setup.

class UserRepository {
  static $inject = ['$rest'];

  private users: ng.RestService<User, number>;

  constructor($rest: ng.RestFactory) {
    this.users = $rest<User, number>('/api/users', User);
  }

  listAdmins() {
    return this.users.list({ role: 'admin' });
  }

  getUser(id: number) {
    return this.users.get(id);
  }
}

Use URI templates

$rest expands RFC 6570 templates before sending a request. Template variables are taken from the params object you pass to list() or get().

const issues = $rest<Issue>(
  '/api/repos/{owner}/{repo}/issues{?labels*}',
  Issue,
);

const openBugs = await issues.list({
  owner: 'angular-wave',
  repo: 'angular.ts',
  labels: ['bug', 'ui'],
});

Params that are not consumed by the template are forwarded to $http as query params.

Map server data to classes

Pass an entity class when the raw response needs normalization, computed properties, or methods.

class Article {
  id: number;
  title: string;
  publishedAt: Date;

  constructor(data: any) {
    this.id = data.id;
    this.title = data.title;
    this.publishedAt = new Date(data.published_at);
  }

  get isPublished() {
    return this.publishedAt.getTime() <= Date.now();
  }
}

const articles = $rest<Article, number>('/api/articles', Article);
const article = await articles.get(42);

if (article?.isPublished) {
  // article is a real Article instance.
}

If you omit the entity class, $rest returns the parsed response data as-is.

Handle writes

create() sends POST, update() sends PUT, and delete() sends DELETE. The methods intentionally stay close to HTTP semantics so errors and interceptors still flow through $http.

class ArticleController {
  static $inject = ['$rest'];

  private articles: ng.RestService<Article, number>;
  items: Article[] = [];

  constructor($rest: ng.RestFactory) {
    this.articles = $rest<Article, number>('/api/articles', Article);
  }

  async publish(draft: Partial<Article>) {
    const created = await this.articles.create(draft as Article);
    this.items.unshift(created as Article);
  }

  async rename(id: number, title: string) {
    const updated = await this.articles.update(id, { title });
    if (updated) {
      this.items = this.items.map((item) =>
        item.id === id ? (updated as Article) : item,
      );
    }
  }

  async remove(id: number) {
    await this.articles.delete(id);
    this.items = this.items.filter((item) => item.id !== id);
  }
}

get(), create(), and update() return null for an empty response. delete() resolves with no value after the backend succeeds. Backend failures reject for every operation, allowing one application error boundary or policy to handle them consistently.

$rest does not perform framework-property cleanup itself. With the default HTTP backend, $http deproxies scope payloads before JSON serialization, so proxy helpers such as _target, _handler, and _proxy do not reach the server. Generated repeat identity is stored as internal metadata rather than on your model object, so it is not included in request bodies. Explicit application-owned properties remain part of the payload.

Use a cached backend

CachedRestBackend wraps a network backend and an async cache store. The default HTTP backend remains available through HttpRestBackend, while cache storage can be memory, IndexedDB, the Cache API, or any object that implements RestCacheStore.

const backend = new CachedRestBackend({
  network: new HttpRestBackend($http),
  cache: appCache,
  policy: ({ url }) =>
    url.startsWith('/api/catalog') ? 'stale-while-revalidate' : 'network-first',
});

const articles = $rest<Article, number>('/api/articles', Article, { backend });

Supported policy results are cache-first, network-first, and stale-while-revalidate. Use the strategy option when every request should use one static result. Writes always go to the network backend first; successful writes invalidate cached collection and entity keys for the resource.

Cache keys are generated by CachedRestBackend. A RestCacheStore receives the final key string in get(), set(), delete(), and deletePrefix() and should treat that key as opaque. createRestCacheKey() is an internal REST module helper, not a top-level namespace API.

Write a custom backend

A custom backend implements RestBackend. It receives normalized requests and returns raw response data for RestService to map.

class IndexedDbRestBackend implements ng.RestBackend {
  async request<T>(request: ng.RestRequest): Promise<ng.RestResponse<T>> {
    if (request.method === 'GET') {
      return { data: (await readFromDb(request.url)) as T };
    }

    throw new Error(`Unsupported method: ${request.method}`);
  }
}

const articles = $rest<Article, number>('/api/articles', Article, {
  backend: new IndexedDbRestBackend(),
});

Use HttpRestBackend when the backend should delegate to $http, and wrap it with CachedRestBackend when reads should use one of the cache strategies.

CRUD demo

The demo at /src/services/rest/rest-crud-demo.html uses the Go demo backend through /api/tasks. It shows list(), get(), create(), update(), and delete() against a real HTTP endpoint, renders rows with ng-repeat, and includes a cache strategy toggle for network-first, cache-first, and stale-while-revalidate.

6 - Client-Side Routing

Navigate between named states, read and modify the URL, manage history, and intercept route transitions.

AngularTS has two routing layers. $location manages the browser URL and history directly. $state works at the application level with named states, parameters, resolves, and transition hooks.

Exact routing API signatures live in TypeDoc:

Work With The URL

Use $location when code needs to inspect or change the raw URL.

$location.getPath(); // "/dashboard"
$location.getSearch(); // { tab: "overview" }
$location.getHash(); // "summary"
$location.getUrl(); // "/dashboard?tab=overview#summary"
$location.absUrl(); // "https://app.example.com/dashboard?tab=overview#summary"

Setter methods return $location, so related URL changes can be chained.

$location
  .setPath('/settings/profile')
  .setSearch({ tab: 'security' })
  .setHash('billing-section');

$location.setSearchParam('page', 2);

Changes to $location are applied asynchronously. $locationChangeStart and $locationChangeSuccess are broadcast on $rootScope around navigation.

Configure URL Mode

Configure $location before the application runs.

angular.createModule('demo', []).config({
  $location: {
    html5Mode: {
      enabled: true,
      requireBase: false,
      rewriteLinks: true,
    },
    hashPrefix: '!',
  },
});

When requireBase is enabled, the application document must include a <base> tag.

Use $state.go() for normal application navigation. It accepts absolute state names, parent-relative names, and sibling-relative names.

$state.go('contacts.detail', { id: 42 });

$state.go('^.list');

$state.go('.detail', { id: 42 });

$state.go($state.current, $state.params, { reload: true });

go() returns a transition promise. Use transition options when you need to control reloads, parameter inheritance, URL updates, or relative navigation.

Use $state.href() when templates or controllers need a URL without starting navigation.

const relative = $state.href('contacts.detail', { id: 42 });
const absolute = $state.href('contacts.detail', { id: 42 }, { absolute: true });

Check Active States

Use matches() for ancestors or glob patterns and pass exact when only the current state should match.

$state.matches('contacts.detail', undefined, { exact: true });
$state.matches('contacts.detail', { id: 42 }, { exact: true });

$state.matches('contacts');
$state.matches('*.detail');

These helpers are useful for active navigation styling and conditional UI.

Register States At Runtime

$stateRegistry stores state definitions and can register states after bootstrap, which is useful for lazy-loaded feature modules.

const detail = $stateRegistry.getState('contacts.detail');
const allStates = $stateRegistry.getStates();

$stateRegistry.register({
  name: 'profile',
  url: '/profile',
  component: 'profilePage',
});

Handle Navigation Events

Listen on $rootScope for URL-level events when you need a broad guard.

angular.getModule('demo').run([
  '$rootScope',
  '$state',
  'authService',
  ($rootScope, $state, authService) => {
    $rootScope.on('$locationChangeStart', (event, newUrl) => {
      if (newUrl.includes('/admin') && !authService.isAuthenticated()) {
        event.preventDefault();
        $state.go('login', { returnUrl: newUrl });
      }
    });
  },
]);

For state-level lifecycle work, prefer transition hooks.

Example: Programmatic Navigation

class OrderController {
  static $inject = ['$state'];

  order!: Order;

  constructor(private $state: ng.StateService) {}

  viewOrder(id: number) {
    this.$state.go('orders.detail', { orderId: id });
  }

  backToList() {
    this.$state.go('^');
  }

  get orderLink(): string | null {
    return this.$state.href('orders.detail', { orderId: this.order.id });
  }
}

7 - Cookies And Browser Storage

Read and write cookies with $cookie, serialize objects, and use $window.localStorage or sessionStorage for client-side persistence.

AngularTS provides $cookie for typed, injectable cookie access and $window for direct browser storage access. Prefer injected services over globals so unit tests can replace browser APIs without patching window or document.

Exact cookie API signatures live in TypeDoc:

Read Cookies

$cookie decodes keys and values, parses document.cookie, and caches the parsed cookie map until the browser cookie string changes.

const token = $cookie.get('session_token');

const prefs = $cookie.getObject<UserPreferences>('user_prefs');

const all = $cookie.getAll();

Use get() for raw string values and getObject() only for cookies you control and know contain JSON.

Write Cookies

Use put() for strings and putObject() for JSON-serializable values.

$cookie.put('session_token', 'abc123', {
  path: '/',
  secure: true,
  samesite: 'Strict',
  expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});

$cookie.putObject(
  'user_prefs',
  { theme: 'dark', fontSize: 14 },
  {
    path: '/',
    expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
  },
);

Cookie attributes are passed through the CookieOptions object. Common options are path, domain, expires, secure, and samesite.

Remove Cookies

remove() expires the cookie by writing an old expiration date.

$cookie.remove('session_token');

$cookie.remove('session_token', {
  path: '/app',
  domain: '.example.com',
});

A cookie can only be removed when the path and domain used for removal match the values used when it was created. If a cookie was created with path: "/", pass the same path when removing it.

Set defaults once when every cookie should share the same attributes.

angular.createModule('demo', []).config({
  $cookie: {
    defaults: {
      path: '/',
      secure: true,
      samesite: 'Lax',
    },
  },
});

Per-call options are merged on top of configured defaults, so individual writes can still override a field.

Local And Session Storage

AngularTS exposes the browser window object through $window. Inject $window when a service needs localStorage or sessionStorage.

class PreferencesStorage {
  static $inject = ['$window'];

  constructor(private $window: Window & typeof globalThis) {}

  saveTheme(theme: string): void {
    this.$window.localStorage.setItem('theme', theme);
  }

  loadTheme(): string {
    return this.$window.localStorage.getItem('theme') ?? 'light';
  }

  saveSessionData(key: string, data: unknown): void {
    this.$window.sessionStorage.setItem(key, JSON.stringify(data));
  }

  loadSessionData<T>(key: string): T | null {
    const raw = this.$window.sessionStorage.getItem(key);
    if (!raw) return null;

    try {
      return JSON.parse(raw) as T;
    } catch {
      return null;
    }
  }
}
ConcernlocalStoragesessionStorage
PersistenceUntil explicitly clearedUntil the browser tab closes
ScopeShared across same-origin tabsIsolated to the current tab
Typical usePreferences and cached dataWizard state and temporary form data

Storage Events

Listen for storage changes from other tabs through $window.

angular.getModule('demo').run([
  '$window',
  '$rootScope',
  ($window, $rootScope) => {
    $window.addEventListener('storage', (event: StorageEvent) => {
      if (event.key === 'theme') {
        $rootScope.broadcast('themeChanged', event.newValue);
      }
    });
  },
]);

The browser only fires storage events in other same-origin tabs or windows, not in the tab that made the change.

Example: Remember Me

class AuthService {
  static $inject = ['$cookie', '$window'];

  constructor(
    private $cookie: ng.CookieService,
    private $window: Window & typeof globalThis,
  ) {}

  login(token: string, rememberMe: boolean) {
    if (rememberMe) {
      this.$cookie.put('auth_token', token, {
        path: '/',
        secure: true,
        samesite: 'Strict',
        expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
      });
    } else {
      this.$window.sessionStorage.setItem('auth_token', token);
    }
  }

  getToken(): string | null {
    return (
      this.$cookie.get('auth_token') ??
      this.$window.sessionStorage.getItem('auth_token')
    );
  }

  logout() {
    this.$cookie.remove('auth_token', { path: '/' });
    this.$window.sessionStorage.removeItem('auth_token');
  }
}