1 - 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.module('app', []).config(($httpProvider) => {
  $httpProvider.defaults.headers.common.Authorization = 'Bearer token';
  $httpProvider.defaults.withCredentials = true;
});

Runtime defaults are available through $http.defaults:

angular.module('app').run(($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.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);
    },
  }));
});

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.module('app', []).config(($httpProvider) => {
  $httpProvider.xsrfTrustedOrigins.push('https://api.example.com');
});

Next Steps

2 - 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 PubSub 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.PubSub) {}

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

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

  cartCount = 0;

  constructor($eventBus: ng.PubSub, $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.PubSub) {
    $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 error to $exceptionHandler and continues delivering the event to the remaining subscribers.

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

$eventBus.subscribe("order:created", (order) => {
  console.log("still delivered", order.id);
});

This keeps one failing listener from blocking unrelated subscribers.

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.

3 - 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", ["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"
  data-on-message="events.push($message)"
  data-on-reconnect="reconnects = $attempt"
  data-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. data-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"
  data-on-result="vm.compressed = $result"
  data-on-error="vm.error = $error"
  trigger="click"
>
  Compress
</button>

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

The directive evaluates data-params, posts the result to the worker, and exposes worker responses as $result in data-on-result. When no result expression is provided, it swaps the result into the element using the configured swap strategy.

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 exports on the scope under a configurable name.

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

WebAssembly loading is asynchronous. Guard calls until the export object exists or trigger work after the directive has linked.

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

4 - 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:

Register a resource

Register shared resources in a config block. The provider stores resource definitions before the application starts, then the $rest factory uses the live $http service at runtime.

class User {
  id: number;
  name: string;
  createdAt: Date;

  constructor(data: any) {
    this.id = data.id;
    this.name = data.name;
    this.createdAt = new Date(data.created_at);
  }
}

angular.module("demo", []).config(($restProvider: ng.RestProvider) => {
  $restProvider.rest("users", "/api/users", User, {
    timeout: 5000,
    withCredentials: true,
  });
});

The resource name is informational in the current API. The URL can be a plain path or an RFC 6570 URI template.

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) {
    if (await this.articles.delete(id)) {
      this.items = this.items.filter((item) => item.id !== id);
    }
  }
}

$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.

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 cache = new MapRestCacheStore();
const backend = new CachedRestBackend({
  network: new HttpRestBackend($http),
  cache,
  strategy: "network-first",
});

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

Supported read strategies are cache-first, network-first, and stale-while-revalidate. 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.

5 - 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.path();    // "/dashboard"
$location.search();  // { tab: "overview" }
$location.hash();    // "summary"
$location.url();     // "/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
  .path("/settings/profile")
  .search({ tab: "security" })
  .hash("billing-section");

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

Configure URL Mode

Configure $locationProvider before the application runs.

angular.module("demo", []).config(($locationProvider: ng.LocationProvider) => {
  $locationProvider.html5Mode({
    enabled: true,
    requireBase: false,
    rewriteLinks: true,
  });

  $locationProvider.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 is() for exact matches and includes() for ancestors or glob patterns.

$state.is("contacts.detail");
$state.is("contacts.detail", { id: 42 });

$state.includes("contacts");
$state.includes("*.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.get("contacts.detail");
const allStates = $stateRegistry.get();

$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.module("demo").run(($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 });
  }
}

6 - 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.

Provider Defaults

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

angular.module("demo", []).config(($cookieProvider: ng.CookieProvider) => {
  $cookieProvider.defaults = {
    path: "/",
    secure: true,
    samesite: "Lax",
  };
});

Per-call options are merged on top of provider 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.module("demo").run(($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");
  }
}