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

Return to the regular view of this page.

Documentation

AngularTS is a browser framework for building interactive web pages and applications with HTML, JavaScript, and TypeScript. These docs assume no prior framework knowledge.

Start here

  1. Introduction explains the framework’s HTML-first model and what it adds to AngularJS.
  2. Install AngularTS adds the framework with one script tag or an npm package.
  3. Build your first application creates a task list one small step at a time.
  4. Keep the application on the server explains when to enhance HTML and when a module should own a browser model.

If you already know the basics, use the task guides to add a feature or the reference to look up an API.

If a complex browser-owned screen needs stronger model-to-view type checking, compare the optional view styles.

Use the cookbook when you have a concrete problem and need a tested implementation pattern.

What AngularTS provides

  • Reactive state that updates only the affected DOM bindings.
  • HTML templates and type-safe views in the same component model.
  • Modules, dependency injection, directives, forms, HTTP, routing, and animations in one runtime package.
  • Buildless script-tag use and typed npm use.
  • Language integrations for JVM, JavaScript-targeting, and native ecosystems.

New terms are defined in the glossary.


What is AngularTS?

AngularTS is buildless, type-safe and reactive JS framework for building structured web applications at any scale. It continues the legacy of AngularJS by providing the best developer experience via immediate productivity without the burden of JS ecosystem tooling. Getting started with AngularTS does not even require JavaScript. All you need is a little bit of HTML. Below is a canonical example of a counter:

Example

<section ng-app ng-cloak>
  <button class="btn btn-dark" ng-init="count = 0" ng-click="count++">
    Count is: {{ count }}
  </button>
</section>

Result


This code demonstrates the following key AngularTS features:

  • HTML-first: AngularTS is designed for HTML-first approach, meaning your application logic can be expressed declaratively in your markup using custom attributes–called directives. Here, we are using ng-init directive to initialize our application state and ng-click directive to add an event handler that changes our state.

  • Template-driven: AngularTS’s built-in template engine automatically keeps the UI in sync with your application state, eliminating the need for manual DOM tracking and updates. The {{count}} expression above is an example of Angular’s interpolation syntax. As the value of count variable increases, our UI is updated with the new state.

  • Island-first and zero-cost: AngularTS creates an application on any HTML tag with ng-app attribute, allowing multiple independent applications (modules) to live on a single page. This allows AngularTS to work alongside your existing tech stack where the majority of your page is rendered on the server, while AngularTS is isolated to small “islands” (regions of your page) where custom interactivity or personalization is required.

  • Micro-framework appearance: With its minimal setup, AngularTS is well-suited for quick experiments, LLM-generated code, and learning web development in general. But beneath its lightweight surface, it supports structured enterprise design patterns, MVC architecture, and component-driven design. With its rich directive library, state-based routing and support for animations, AngularTS is a complete package for building large SPAs, server, mobile, and desktop applications.

1 - Get started

Install AngularTS and build a complete first application without prior framework experience.

Follow these pages in order. They use the same task-list application so each page adds one idea instead of starting over.

  1. Introduction
  2. Installation
  3. First application
  4. How AngularTS works
  5. Next learning paths

You only need a browser and a text editor. Node.js, a bundler, and TypeScript are optional.

Already comfortable with browser frameworks? Use the Quickstart as a shorter alternative to the complete beginner sequence.

1.1 - AngularTS: modern evolution of AngularJS

Understand where AngularTS fits, what remains compatible with AngularJS, and what changed in the new runtime.

AngularTS is a modernized continuation of AngularJS — carrying forward its three core pillars (string interpolation, dependency injection, and two-way data binding) while rebuilding the internals around a reactive change-detection model, full TypeScript support, and direct access to native browser APIs. It requires no build step and no bundler to get started.

What AngularTS preserves from AngularJS

AngularJS was the result of a decade of engineering by the Angular team at Google and accumulated one of the largest test suites in open-source JavaScript. AngularTS inherits that foundation directly:

  • Declarative HTML templates using familiar ng-* directives
  • Dependency injection container for organizing and composing application services
  • Two-way data binding between the DOM and application state
  • Controllers, filters, and the module system — all structurally compatible with AngularJS patterns

If you have existing AngularJS knowledge, the mental model transfers directly.

What AngularTS adds

AngularTS extends the AngularJS foundation with modern primitives and new capabilities:

Reactive change detection

Proxy-based reactivity replaces the digest cycle. The DOM updates only when data actually changes — no polling, no virtual DOM diffing.

Native DOM and Promises

Directives and controllers work directly with native DOM APIs. The $q and $timeout abstractions are replaced by native Promise and setTimeout.

Built-in enterprise router

ng-router is a port of ui-router, supporting nested views, state transitions, resolves, and URL matching out of the box.

Declarative HTTP directives

ng-get, ng-post, ng-put, and ng-delete let you make HTTP requests declaratively from HTML attributes. The server-first approach may feel familiar to HTMX users, but AngularTS has its own API and behavior.

Real-time injectables

$websocket, $sse (Server-Sent Events), $rest, Web Workers, and WebAssembly modules are first-class injectable services.

Built-in animations

CSS and JavaScript animation support ships in the core package with no additional dependencies.

When to use AngularTS

AngularTS is a strong fit for these scenarios:

Server-rendered applications with interactive islands. Any HTML element can host an independent ng-app. You can drop AngularTS into a server-rendered page and add interactivity to specific regions without restructuring the whole application.

Applications where a build step is a liability. AngularTS loads from a CDN script tag and runs directly in the browser. There are no compilation steps, no bundler configuration files, and no Node.js required to develop or deploy.

Large-scale SPAs that need structure. The module system, dependency injection, state-based router, and MVC architecture scale to enterprise applications. AngularTS is not just a micro-library — it is a complete framework for applications of any size.

Migrating from AngularJS. If you maintain an AngularJS codebase and want to modernize incrementally, AngularTS preserves the API surface you already know.

How it compares to other frameworks

AngularTSReactVueAngular (v2+)
Build step requiredNoYesOptionalYes
CDN drop-inYesPartialYesNo
Two-way bindingYesNoYesYes
Dependency injectionYesNoNoYes
HTML-first templatesYesNo (JSX)YesYes
Bundle sizeSmall (UMD)MediumSmallLarge

Note: Angular 2+ (also called just “Angular”) is a complete rewrite of AngularJS and shares no API surface with it. AngularTS is a continuation of AngularJS — not a migration target from Angular 2+.

Philosophy

AngularTS stays as close to web standards as possible. It avoids inventing abstractions where the platform already provides them: native Promise instead of $q, native fetch under the hood for $http, direct DOM element references in directives instead of jQuery wrappers. This keeps the mental model small and the debugging experience familiar.

The result is described by the project as “a high-performance, buildless, multi-paradigm and battle-tested JS framework.”

Next steps

Installation

Add AngularTS to your project via CDN or npm.

Quickstart

Build a working counter and todo list in minutes.

1.2 - Install AngularTS in your project

Add AngularTS via a CDN script tag or npm. The package ships TypeScript declarations and supports auto-bootstrap with ng-app or manual bootstrap.

AngularTS can be added to any project in two ways: a single <script> tag for zero-configuration browser use, or an npm package for projects that use a bundler or want TypeScript declarations. Both approaches support the full framework — there is no difference in available features between the two.

CDN

The fastest way to get started is to load AngularTS directly from jsDelivr. No installation, no configuration — just add the script to your HTML file:

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular-ts.umd.min.js"></script>
  </head>
  <body>
    <div ng-app ng-init="x = 'world'">Hello {{ x }}</div>
  </body>
</html>

The UMD build exposes the angular global on window and auto-bootstraps any element with an ng-app attribute once the DOM is ready.

Tip: The CDN URL always points to the latest published version on npm. To pin a specific version, include it in the URL: https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts@0.26.0/dist/angular-ts.umd.min.js

npm

Install the package from the npm registry:

npm install @angular-wave/angular.ts
yarn add @angular-wave/angular.ts
pnpm add @angular-wave/angular.ts

The package ships two distribution formats:

  • ESM (dist/angular-ts.esm.js) — the default entry point for bundlers
  • UMD (dist/angular-ts.umd.js) — for direct browser use

Import the library in your entry file:

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

The default ESM import and UMD script both scan for ng-app once the DOM is ready. The angular singleton is also assigned to window.angular automatically.

TypeScript setup

The published package includes generated TypeScript declarations under @types/. No separate @types/ package is required.

After installing, TypeScript will resolve types automatically. You can reference the type namespace in your project:

const myModule: ng.NgModule = angular.createModule('myApp', []);

myModule.controller('MyController', [
  '$scope',
  function ($scope: ng.Scope) {
    $scope.message = 'Hello, AngularTS';
  },
]);

Info: The ng namespace provides types for scopes, injectors, services, directives, and all other AngularTS primitives. It is declared globally by the package and available anywhere TypeScript resolves the package types.

Auto-bootstrap with ng-app

The simplest way to start an AngularTS application is the ng-app attribute. Place it on any HTML element and AngularTS will bootstrap that element as the application root when the DOM is ready. Importing the default package entry is enough; no startup call is needed for a static ng-app root.

<div ng-app>{{ 1 + 1 }}</div>

To connect a named module, set ng-app to the module name:

<div ng-app="myApp">
  <p ng-controller="GreetController">{{ greeting }}</p>
</div>

<script src="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular-ts.umd.min.js"></script>
<script>
  angular.createModule('myApp', []).controller('GreetController', [
    '$scope',
    function ($scope) {
      $scope.greeting = 'Hello from AngularTS';
    },
  ]);
</script>

Multiple independent ng-app elements can exist on the same page. Each becomes its own isolated application instance.

Explicit dependency injection

AngularTS requires explicit annotations for every dependency-bearing function. Use array annotation or a static $inject property; parameter names are never interpreted as injection tokens.

Manual bootstrap with angular.bootstrap()

For full control over when and how your application starts, call angular.bootstrap() directly instead of using ng-app:

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular-ts.umd.min.js"></script>
  </head>
  <body>
    <div id="app">
      <p ng-controller="WelcomeController">{{ greeting }}</p>
    </div>

    <script>
      angular.createModule('demo', []).controller('WelcomeController', [
        '$scope',
        function ($scope) {
          $scope.greeting = 'Welcome!';
        },
      ]);

      angular.bootstrap(document.getElementById('app'), ['demo']);
    </script>
  </body>
</html>

angular.bootstrap() accepts two arguments:

ArgumentTypeDescription
elementstring | HTMLElement | HTMLDocumentThe root element to bootstrap the application on
modulesArray<string | any>Module names or annotated config functions

It returns the $injector instance for the bootstrapped application.

Warning: Do not call angular.bootstrap() on an element that already has an ng-app attribute, or on an element that contains ng-view, ng-if, or other transclusion directives. This causes the root element and injector to be misplaced.

Next steps

Quickstart

Build a working app with a counter and todo list.

Modules

Learn how the AngularTS module system organizes your application.

1.3 - Build your first application

Build a task list from one HTML file while learning modules, controllers, state, events, forms, and reactive rendering.

What you will build

You will build a task list that accepts text, adds tasks, marks them complete, and shows the remaining count. The whole application fits in one HTML file.

Before you start

Complete installation or use the CDN script shown below. Create index.html.

Create the application

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular-ts.umd.min.js"></script>
  </head>
  <body ng-app="todoApp" ng-controller="TodoController">
    <form ng-submit="addTodo()">
      <input ng-model="draft" placeholder="What needs doing?" required />
      <button>Add</button>
    </form>
    <p>{{ remaining() }} remaining</p>
    <ul>
      <li ng-repeat="todo in todos">
        <input type="checkbox" ng-model="todo.done" /> {{ todo.title }}
      </li>
    </ul>
    <!-- Add the script below here. -->
  </body>
</html>

Place this script before the closing body tag, where the comment appears:

<script>
  angular.createModule('todoApp', []).controller('TodoController', [
    '$scope',
    ($scope) => {
      $scope.draft = '';
      $scope.todos = [{ title: 'Learn AngularTS', done: false }];
      $scope.addTodo = () => {
        if (!$scope.draft.trim()) return;
        $scope.todos.push({ title: $scope.draft.trim(), done: false });
        $scope.draft = '';
      };
      $scope.remaining = () => $scope.todos.filter((todo) => !todo.done).length;
    },
  ]);
</script>

Open the file in a browser. Enter a task and press Add. The list and count change without a page reload.

Understand each part

ng-app="todoApp" starts the named module. A module is the application container. ng-controller connects behavior and state to this part of the DOM. The injected $scope is the reactive state visible to the HTML template.

ng-model keeps an input and state property synchronized. ng-submit runs a function when the form is submitted. ng-repeat creates one list item per task. {{ remaining() }} displays an expression and refreshes when its reactive inputs change.

Make a change

Change the initial task title, reload the page, and add two tasks. Then open the browser console and resolve any reported error before continuing.

Next step

Read how AngularTS works to connect the code to the framework lifecycle.

1.4 - How AngularTS works

Understand how the module, injector, compiler, scope, directives, and DOM cooperate when an AngularTS application starts.

What you will learn

You will trace the first application from page load to a reactive DOM update.

Before you start

Complete the first application.

Startup in six steps

  1. The browser parses the HTML and creates the DOM.
  2. The AngularTS script finds the element marked with ng-app.
  3. AngularTS loads that element’s named module and creates an injector.
  4. The compiler walks the element’s DOM and finds directives.
  5. The controller creates state on its scope.
  6. Directives connect that state to text, properties, events, and child DOM.

An injector creates and shares dependencies. A directive adds behavior to HTML. A scope owns reactive state and lifecycle cleanup. The compiler is the runtime process that connects directives to DOM nodes; it is not a build step.

A reactive update

When addTodo() pushes an item into todos, the reactive scope records that change. AngularTS schedules the affected bindings, updates the repeated list and remaining count, and leaves unrelated DOM alone.

You do not manually redraw the page, compare a virtual DOM, or start a digest loop. Browser events and native asynchronous callbacks can update reactive state directly.

Application structure

Small applications may stay in one file. Larger applications normally use a module, components for UI regions, services for shared behavior or data, and a router for multiple screens. Dependency injection connects those parts without global variables.

Next step

Choose a learning path.

1.5 - Choose your learning path

Continue from the first application through templates, typed views, application features, migration, or language integrations.

What you will learn

This page selects the shortest route from the beginner tutorial to your goal.

Before you start

Complete how AngularTS works or be comfortable with modules, scopes, directives, and reactive updates.

Build with HTML templates

Read template views, then the form and server-data guides.

Build with type-safe views

Read typed views, then collections and cleanup.

Build a multi-screen application

Read application structure and then routing.

Move an existing application

Use the migration guide. It separates AngularJS-compatible concepts from AngularTS replacements.

Use another language

Use the integration guide after learning the runtime concepts in this tutorial.

Optimize and standardize an application

Use the cookbook after the first feature is working. Start by measuring, then apply the narrowest relevant recipe. Adopt the application style guide before multiple contributors add components.

Next step

Open the first page in the path that matches your project.

1.6 - Build your first AngularTS app

A step-by-step guide to creating modules, controllers, and directives in AngularTS — with a counter example and a complete todo list application.

AngularTS apps are built from HTML and JavaScript — no compilation required. This guide walks through creating a module, wiring up a controller, and using directives to bind data to the DOM. By the end you will have a working counter and a functional todo list.

Add AngularTS to your page

Create an HTML file and include the AngularTS script from the CDN. This single tag is all you need to get the full framework:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>My AngularTS App</title>
    <script src="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular-ts.umd.min.js"></script>
  </head>
  <body>
    <!-- application markup goes here -->
  </body>
</html>

If you are using npm, install the package and import the singleton instead:

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

Create a module

A module is the top-level container for your application. It holds controllers, services, directives, and filters. Create one by calling angular.createModule() with a name and an empty dependency array:

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

The first argument is the module name. The second argument lists other modules your module depends on — an empty array means no dependencies. You reference this name in the ng-app attribute to tell AngularTS which module to bootstrap.

Connect the module to your HTML by adding ng-app to a container element:

<body ng-app="myApp">
  <!-- AngularTS controls everything inside this element -->
</body>

Add a controller

Controllers attach behavior to a region of the DOM. They receive a $scope object — a plain JavaScript object that acts as the data model for their template. Anything you put on $scope becomes available in the HTML template.

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

app.controller('CounterController', [
  '$scope',
  function ($scope) {
    $scope.count = 0;

    $scope.increment = function () {
      $scope.count++;
    };

    $scope.decrement = function () {
      if ($scope.count > 0) {
        $scope.count--;
      }
    };
  },
]);

Attach the controller to a DOM element with ng-controller:

<body ng-app="myApp">
  <div ng-controller="CounterController">
    <p>Count: {{ count }}</p>
    <button ng-click="increment()">+</button>
    <button ng-click="decrement()">-</button>
  </div>
</body>

The {{ count }} expression is AngularTS’s interpolation syntax — it renders the current value of $scope.count and updates automatically whenever the value changes. ng-click binds a click event to the controller method.

Use directives in your template

Directives are the building blocks of AngularTS templates. They extend HTML with behavior declared as attributes or element names. The core library ships over 50 directives covering data binding, conditional rendering, list rendering, forms, and HTTP requests.

Here is a complete counter example using only HTML attributes and no separate JavaScript file:

<!doctype html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular-ts.umd.min.js"></script>
  </head>
  <body>
    <section ng-app ng-cloak>
      <button ng-init="count = 0" ng-click="count++">
        Count is: {{ count }}
      </button>
    </section>
  </body>
</html>

Key directives used here:

  • ng-app — designates the root element of the application and triggers auto-bootstrap
  • ng-cloak — hides the element until AngularTS has compiled the template, preventing a flash of unrendered {{ }} expressions
  • ng-init — initializes a scope variable inline; useful for simple cases without a controller
  • ng-click — evaluates an expression when the element is clicked

Complete example: todo list

This excerpt combines a named module, two-way binding, and list rendering. Put the normal AngularTS script tag in the document head as shown above.

<section ng-app="todoApp" ng-controller="TodoController">
  <form ng-submit="addTodo()">
    <input ng-model="newTodo" required />
    <button>Add</button>
  </form>
  <li ng-repeat="todo in todos">
    <input type="checkbox" ng-model="todo.done" />
    {{ todo.text }}
  </li>
</section>

<script>
  angular.createModule('todoApp', []).controller('TodoController', [
    '$scope',
    ($scope) => {
      $scope.todos = [];
      $scope.addTodo = () =>
        $scope.todos.push({ text: $scope.newTodo, done: false });
    },
  ]);
</script>

This example uses:

DirectivePurpose
ng-modelTwo-way binding between the input value and $scope.newTodo
ng-submitCalls addTodo() when the form is submitted
ng-repeatRenders a <li> for each item in $scope.todos
ng-classAdds the done CSS class when todo.done is true
ng-showShows the element only when the expression is truthy
ng-clickCalls a scope function when the element is clicked

Using TypeScript

If you are working with npm and TypeScript, annotate the controller function using the ng.Scope type:

interface TodoItem {
  text: string;
  done: boolean;
}

interface TodoScope extends ng.Scope {
  todos: TodoItem[];
  newTodo: string;
  addTodo(): void;
  remaining(): number;
}

angular.createModule('todoApp', []).controller('TodoController', [
  '$scope',
  function ($scope: TodoScope) {
    $scope.todos = [{ text: 'Learn AngularTS', done: false }];
    $scope.newTodo = '';

    $scope.addTodo = function () {
      if ($scope.newTodo.trim()) {
        $scope.todos.push({ text: $scope.newTodo.trim(), done: false });
        $scope.newTodo = '';
      }
    };

    $scope.remaining = function () {
      return $scope.todos.filter((t) => !t.done).length;
    };
  },
]);

Note: TypeScript declarations ship with the package under @types/. No separate @types/angular-wave__angular.ts package is needed.

Next steps

Core concepts

Understand how modules, dependency injection, and scopes work together.

Directives reference

Browse all 50+ built-in directives with examples.

Routing

Add state-based routing with nested views and URL matching.

Services

Use built-in services for HTTP, WebSockets, REST, and more.

2 - Tutorial

Build and ship one complete AngularTS application from an empty directory.

This tutorial assumes basic HTML and JavaScript but no AngularTS knowledge. You will build a browser-owned task board with reactive state, form validation, routing, server data, tests, and a production checklist.

This application deliberately uses AngularTS’s optional typed programmatic views because its model and rendering live in TypeScript. If your server renders the HTML, follow the first application guide instead and keep the HTML you already have.

What you will build

The finished application can list tasks, add a task, open a task route, load initial data, and expose loading and error states.

Follow the tutorial

  1. Create the application
  2. Add the task board’s typed view
  3. Add reactive state
  4. Build the form
  5. Add routing and server data
  6. Test the behavior
  7. Prepare production
  8. Run the complete application

Complete the steps in order. Every page states the expected result before moving on.

2.1 - Create the application

Install AngularTS, define the application module, and bootstrap the page.

Before you start

Use a current Node.js release and an application bundler that supports TypeScript and browser ESM.

Install

npm install @angular-wave/angular.ts

Create the application entrypoint:

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

export const app = angular.createModule('taskBoard', []);
angular.bootstrap(document, ['taskBoard']);

Create the host page:

<!doctype html>
<html lang="en">
  <body>
    <task-board></task-board>
    <script type="module" src="/src/app.ts"></script>
  </body>
</html>

Checkpoint

Start the development server. The page should load without a bootstrap or missing-module error. The custom element is empty until the next step.

Next step

Add the optional typed component view used by this tutorial.

2.2 - Add a typed view

Register a component whose model and DOM bindings are checked by TypeScript.

A typed view constructs real DOM and keeps controller reads reactive. Its main advantage over string templates is type safety between the component model and its bindings.

import { h1, main, p } from '@angular-wave/angular.ts';

class TaskBoard {
  title = 'Task board';
  tasks = [{ id: 1, title: 'Read the guide', done: false }];
}

app.component<TaskBoard>('taskBoard', {
  controller: TaskBoard,
  view: ({ controller }) =>
    main(
      h1(() => controller.title),
      p(() => `${controller.tasks.length} task(s)`),
    ),
});

The view is registered on the normal application module. It does not need a separate mount operation. Functions used as children or property values are reactive readers; plain values render once.

Checkpoint

The page should display “Task board” and one task count.

Next step

Render and update the collection.

2.3 - Add reactive state

Render a keyed collection and update it without replacing unrelated DOM.

Add a method to the controller:

class TaskBoard {
  nextId = 2;
  tasks = [{ id: 1, title: 'Read the guide', done: false }];

  add(title: string) {
    this.tasks = [...this.tasks, { id: this.nextId++, title, done: false }];
  }
}

Render the collection with stable keys:

import { each, li, ul } from '@angular-wave/angular.ts';

view: ({ controller }) =>
  ul(
    each(
      () => controller.tasks,
      (task) => task.id,
      (task) => li(() => task().title),
    ),
  ),

The item renderer receives a reader. Call task() inside reactive bindings so a same-key item replacement updates its row.

Checkpoint

Calling controller.add("Ship it") appends one row while retaining the existing row.

Next step

Build the form.

2.4 - Build the form

Bind typed DOM events, validation state, and a reactive disabled property.

Add draft and a submit method:

draft = "";

submit() {
  const title = this.draft.trim();
  if (!title) return;
  this.add(title);
  this.draft = "";
}

Add the form before the list:

import { button, form, input, label } from '@angular-wave/angular.ts';

form(
  {
    onsubmit: (event) => {
      event.preventDefault();
      controller.submit();
    },
  },
  label({ htmlFor: 'new-task' }, 'Task'),
  input({
    id: 'new-task',
    value: () => controller.draft,
    oninput: (event) => {
      controller.draft = (event.currentTarget as HTMLInputElement).value;
    },
  }),
  button({ disabled: () => !controller.draft.trim() }, 'Add'),
);

The callback expresses reactive intent directly. No separate computed wrapper is required.

Checkpoint

The button is disabled for blank input. Submitting valid text adds a row and clears the input.

Next step

Add routing and initial server data.

2.5 - Add routing and server data

Give the task board a URL and expose explicit loading and error states.

Register the component as a route:

app.router({
  name: 'tasks',
  url: '/tasks',
  component: 'taskBoard',
});

Place the route outlet in the host page:

<main ng-view></main>

Load initial data from a controller method:

loading = true;
error = "";
tasks: Task[] = [];

async onInit() {
  try {
    const response = await fetch("/api/tasks");
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    this.tasks = await response.json();
  } catch (error) {
    this.error = error instanceof Error ? error.message : "Load failed";
  } finally {
    this.loading = false;
  }
}

Render loading, error, empty, and populated states deliberately. An empty array does not prove loading succeeded.

Checkpoint

Opening /tasks renders the board. A failed request produces a readable error state.

Next step

Test the application behavior.

2.6 - Test the application

Separate model tests from browser-level rendering checks.

Start with a fast controller test:

it('adds a trimmed task', () => {
  const board = new TaskBoard();
  board.draft = '  Write tests  ';
  board.submit();

  expect(board.tasks.at(-1)?.title).toBe('Write tests');
  expect(board.draft).toBe('');
});

Then verify the user-visible contract in a browser:

test('adds a task', async ({ page }) => {
  await page.goto('/tasks');
  await page.getByLabel('Task').fill('Write tests');
  await page.getByRole('button', { name: 'Add' }).click();
  await expect(page.getByText('Write tests')).toBeVisible();
});

Test behavior, not internal scope fields or generated DOM comments. Keep network outcomes deterministic.

Checkpoint

Both the model test and browser test pass, including the loading failure case.

Next step

Prepare the application for production.

2.7 - Prepare production

Apply the minimum production, security, and operability checks to the task board.

Before deployment:

  • Build with production optimization and an intentional source-map policy.
  • Serve hashed assets with long-lived caching and HTML with revalidation.
  • Keep API URLs and secrets out of the browser bundle.
  • Enforce HTTPS and an explicit Content Security Policy.
  • Exercise loading, empty, error, offline, and slow-network states.
  • Test keyboard navigation and visible focus.
  • Record the AngularTS version in the lockfile.
  • Run type checking, unit tests, browser tests, and integration checks in CI.

Expected result

The application can be deployed repeatedly, reports failures without exposing secrets, and remains usable when requests fail.

Continue with Production and deployment and Best practices.

2.8 - Run the complete application

Open the maintained task-board example and compare it with your implementation.

The repository contains a browser-runnable version of the task board:

  • docs/examples/task-board/task-board.ts is compiled as a package consumer.
  • docs/static/examples/task-board/task-board-demo.html provides the host page.
  • docs/static/examples/task-board/task-board.js registers the component view.

Open the task-board example.

The example keeps data local so it runs without a backend. The routing and server-data step shows where application-owned transport behavior belongs.

Compare behavior

Verify that both versions:

  1. Render the initial keyed collection.
  2. Disable submission for blank text.
  3. Trim accepted task titles.
  4. Clear the input after submission.
  5. Preserve existing rows when a task is appended.

CI type-checks the TypeScript application and runs the browser interaction contract. Documentation checks also validate its AngularTS API usage.

3 - Core concepts

Understand the small set of ideas that AngularTS applications use: modules, components, scopes, dependency injection, templates, views, and reactivity.

Read this section after the first-application tutorial. A concept explains why the framework behaves as it does; a task guide shows how to complete a particular job.

  1. Angular runtime
  2. Modules
  3. Components
  4. Scopes and state
  5. Dependency injection
  6. Change detection
  7. Templates
  8. Optional: Programmatic views

Use the glossary whenever a term is unfamiliar.

3.1 - Angular Runtime

Use the Angular runtime to create modules, bootstrap applications, access injectors, and bridge external code.

Angular is the runtime entry point for AngularTS. It owns module registration, application bootstrap, injector creation, cached DOM helpers, and the event-based invocation helpers exposed through window.angular.

Exact runtime contracts live in TypeDoc:

Create Modules

Use angular.createModule() to create or replace a module. Use angular.getModule() to retrieve a module that is already registered.

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

app.service('UserService', UserService);

app.config({
  $location: {
    hashPrefix: '!',
  },
});

const existing = angular.getModule('myApp');

Calling angular.getModule("name") without first creating that module throws the same nomod error as AngularJS.

Bootstrap Manually

angular.bootstrap() starts an application on a DOM element. It is the programmatic alternative to ng-app.

document.addEventListener('DOMContentLoaded', () => {
  angular.bootstrap(document.body, ['myApp']);
});

The built-in ng module is prepended automatically. Every dependency-bearing function must use array annotation or declare a static $inject property.

Each element can host only one application. Bootstrapping an element that already has an injector throws ng:btstrpd.

Auto-Bootstrap

The default package entry and UMD build scan for ng-app when the DOM is ready. The first root uses the global Angular instance; additional roots become sub-applications in angular.subapps. No startup call is required.

Custom runtimes do not scan the document. Bootstrap their root explicitly:

angular.bootstrap(document.querySelector('#app'), ['myApp']);

Use the same explicit call for an application root inserted after the initial document load.

Create A Standalone Injector

Use angular.injector() when tests or non-DOM code need services without compiling an application root.

const injector = angular.injector(['ng', 'myApp'], true);
const $http = injector.get('$http');

Publish A Standalone Custom Element

Use defineAngularElement() from @angular-wave/angular.ts/runtime/web-component when an AngularTS feature should ship as a native web component. The helper creates a custom runtime, registers only the directives and services you list, defines the custom element, and builds the injector without requiring a host page bootstrap.

Custom runtime core providers include only the compile, scope, controller, parse, interpolation, and exception-handling services needed to render templates. Orchestration services such as $machine and $workflow are not part of that core set. Include machineModule for reactive state machines, workflowModule for workflows, or the compatible orchestrationModule aggregate when the runtime needs both.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { machineModule } from '@angular-wave/angular.ts/runtime/orchestration';

const angular = createAngular({
  modules: [machineModule],
});

Include wasmModule when a custom runtime loads WebAssembly or exposes AngularTS scope state through the language-neutral WASM ABI. The bridge remains owned by that runtime and is disposed with it.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { wasmModule } from '@angular-wave/angular.ts/runtime/wasm';

const angular = createAngular({
  modules: [wasmModule],
});
const injector = angular.injector(['ng']);
const playerGuest = injector.get('$wasm').load({
  source: '/player.wasm',
});
const player = injector.get('$rootScope');

await playerGuest.bind(player, { name: 'player' });
player.score = 42;

Include sseModule, websocketModule, or webTransportModule when a custom runtime uses one transport. Include the compatible realtimeModule aggregate when it uses all three. Connection defaults still use the normal typed module configuration, and all active connections close when the custom runtime is destroyed.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { websocketModule } from '@angular-wave/angular.ts/runtime/realtime';

const angular = createAngular({
  modules: [websocketModule],
});

angular.createModule('app', []).config({
  $websocket: {
    defaults: { retryDelay: 500, maxRetries: 5 },
  },
});

Include routerModule when a custom runtime owns navigation. The slice includes URL state, navigation security, route links, routed views, ARIA state, anchor scrolling, and route-template fetching, while leaving unrelated browser and orchestration services out of the build.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { routerModule } from '@angular-wave/angular.ts/runtime/router';

const angular = createAngular({
  modules: [routerModule],
});

angular.createModule('app', []).router({
  name: 'dashboard',
  url: '/dashboard',
  template: '<dashboard-view></dashboard-view>',
});

angular.bootstrap(document.body, ['app']);

Include serviceWorkerModule for managed service-worker registration, updates, and messaging without including the full browser integration group. The native listeners owned by $serviceWorker are released when the custom runtime is destroyed.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { serviceWorkerModule } from '@angular-wave/angular.ts/runtime/service-worker';

const angular = createAngular({
  modules: [serviceWorkerModule],
});

angular.createModule('app', []).serviceWorker('/service-worker.js', {
  scope: '/app/',
  autoRegister: true,
});

const injector = angular.injector(['ng', 'app']);
const appWorker = injector.get('$serviceWorker');

appWorker.onMessage((event) => {
  console.log(event.data);
});

Include webComponentModule when a custom runtime defines scoped custom elements directly. defineAngularElement(...) includes it automatically.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { webComponentModule } from '@angular-wave/angular.ts/runtime/web-component';

const angular = createAngular({
  modules: [webComponentModule],
});
import { defineAngularElement } from '@angular-wave/angular.ts/runtime/web-component';
import { ngEventDirectives } from '@angular-wave/angular.ts/directive/events';

defineAngularElement('billing-summary', {
  directives: {
    ngClick: ngEventDirectives.ngClick,
  },
  services: {
    billingApi: BillingApi,
  },
  component: {
    shadow: true,
    inputs: {
      accountId: String,
    },
    template: `
      <button ng-click="refresh()">
        {{ accountId }} / {{ status }}
      </button>
    `,
    connected({ dispatch, injector, scope }) {
      const api = injector.get('billingApi');

      scope.status = 'ready';
      scope.refresh = () => {
        scope.status = api.status(scope.accountId);
        dispatch('billing-refresh', { status: scope.status });
      };
    },
  },
});

Consumers only need the bundled module and the native element:

<script type="module" src="/widgets/billing-summary.js"></script>
<billing-summary account-id="acct_123"></billing-summary>

Inputs are DOM attributes or properties. Outputs should be CustomEvents dispatched with the dispatch() helper from the component context.

Bridge External Code

angular.emit() and angular.call() evaluate expressions against an injectable service or a named scope. The input format is "<target>.<expression>".

angular.emit('UserService.logout()');

const count = await angular.call('cartScope.items.length');

Use these helpers for small integrations such as browser callbacks, legacy scripts, or embedded widgets. Normal application code should prefer dependency injection.

Locate Named Scopes

getScopeByName() searches from $rootScope for a scope with a matching scopeName.

$scope.scopeName = 'dashboard';

const scope = angular.getScopeByName('dashboard');
scope?.refresh();

Inspect Compiled Elements

The runtime exposes DOM cache helpers for integration and debugging:

const el = document.querySelector("[ng-controller='MyCtrl']") as Element;

const ctrl = angular.getController(el);
const scope = angular.getScope(el);
const injector = angular.getInjector(el);

These helpers read metadata attached during compilation and bootstrap.

Use Injection Tokens

angular.tokens exposes public injection token strings as a typed object. Prefer it when writing $inject arrays in TypeScript.

MyService.$inject = [angular.tokens.$http, angular.tokens.$rootScope];

3.2 - Typed Component Views

Construct component and directive views with real DOM nodes, reactive readers, keyed collections, and normal AngularTS module registration.

Programmatic views provide a JSX-free alternative to HTML templates. A view is an ordinary function that returns real DOM nodes created with typed tag factories. It uses the normal AngularTS compiler, scope lifecycle, dependency injection, component registration, directive registration, and transclusion pipeline.

There is no separate mount API. Register the component or directive on an AngularTS module and let the compiler instantiate it from normal markup.

Create a component view

Import the helpers you use from the package entry point:

import {
  angular,
  attrs,
  button,
  div,
  each,
  event,
  h2,
  input,
  li,
  output,
  p,
  props,
  section,
  tag,
  tagNS,
  tags,
  time,
  ul,
} from '@angular-wave/angular.ts';

Named imports are preferred in ESM applications because bundlers can retain only the factories that are used. No-build UMD applications can access tag factories through angular.tags and helpers through angular.view. Keep tags for namespace proxies; use tag() when the element name is dynamic.

Define the view directly on the component registration:

class CounterController {
  count = 0;
}

angular.createModule('app', []).component('counterButton', {
  controller: CounterController,
  view: ({ controller }) =>
    button(
      { onclick: () => controller.count++ },
      () => `Count: ${controller.count}`,
    ),
});

The component is used like any template-backed component:

<counter-button></counter-button>

Return view children

A component or directive view can return any ProgrammaticViewChild.

  • A DOM Node.
  • A string, number, boolean, or bigint.
  • A reactive child reader function.
  • A nested array of view children.
  • A DocumentFragment.
  • null, undefined, or either boolean to render nothing.

Arrays are flattened recursively. Existing nodes are moved rather than cloned, and document fragments contribute their current child nodes.

view: ({ controller }) => [
  h2(() => controller.title),
  controller.showDetails ? p(controller.initialDetails) : null,
];

The conditional expression in this example is evaluated only when the view is constructed. Make the condition itself a reader when it must remain reactive:

view: ({ controller }) => [
  h2(() => controller.title),
  () => (controller.showDetails ? p(controller.details) : null),
];

Use reactive readers

A function in a child or ordinary non-event property position is a reactive reader. AngularTS executes it, tracks the proxied scope and controller state it reads, and updates only the corresponding DOM binding when that state changes.

output(() => controller.total);

button({
  disabled: () => !controller.canSubmit,
});

input({
  value: () => controller.query,
});

A value that is evaluated before the tag function is a static snapshot:

output(controller.total);
button({ disabled: !controller.canSubmit });

Use reader functions whenever a child or property must be reevaluated reactively.

Unlike string templates, programmatic views keep bindings between the model and the rendered DOM inside TypeScript’s type system. Renaming a controller or model property updates through normal refactoring tools, while misspelled members and incompatible typed DOM property values fail at compile time rather than becoming runtime template errors.

Properties and attributes

The first plain object passed to a tag is its property map. Reactive readers use DOM property semantics, including properties exposed only by custom elements. For static values, AngularTS assigns a DOM property when it exists on the element and otherwise uses an attribute.

Normal property maps are checked against the element’s DOM type. Use attrs() for arbitrary attribute names and props() for custom-element properties; misspelled native properties are rejected by TypeScript.

input({
  value: () => controller.name,
  disabled: () => controller.saving,
  'data-field': 'name',
});

Use attrs() to force attribute semantics. Functions inside attrs() remain reactive readers.

section({
  ...attrs({
    'aria-busy': () => controller.loading,
    'data-count': () => controller.items.length,
  }),
});

Boolean HTML attributes use presence semantics. Other attributes, including ARIA attributes, are serialized normally. For example, an ARIA value of false becomes the string "false" rather than removing the attribute.

Use props() to assign exact, literal DOM property values. Values inside props() are not reactive readers. This distinction permits function-valued properties on custom elements:

const grid = tag('data-grid', {
  ...props({
    rowFormatter: (row: { name: string }) => row.name.toUpperCase(),
  }),
  hidden: () => controller.rows.length === 0,
});

Do not place reactive readers inside props():

// Reactive property
input({ value: () => controller.name });

// Literal function assigned as the value property
input(props({ value: () => controller.name }));

Potentially dangerous properties such as innerHTML and srcdoc still pass through AngularTS contextual escaping. Use trusted SCE values only when the content has been reviewed and is intentionally trusted.

Handle events

Functions assigned to native on* properties are event handlers:

button({
  onclick: (event) => controller.submit(event),
});

Use event() for listener options, object listeners, or custom event names. It uses native addEventListener semantics and is removed with the view.

button({
  click: event(() => controller.submit(), { once: true }),
});

tag('data-grid', {
  selectionchange: event<CustomEvent<Selection>>((event) =>
    controller.select(event.detail),
  ),
});

Errors thrown by programmatic event handlers and reactive bindings are routed through $exceptionHandler.

Render collections

Standard array methods are useful for static collections:

ul(controller.todos.map((todo) => li(todo.title)));

This calls map() once and creates a DOM snapshot. Changes to the array do not insert, remove, or reorder list nodes.

Use each() for a reactive collection:

ul(
  each(
    () => controller.todos,
    (todo) => todo.id,
    (todo) => li(() => todo().title),
  ),
);

each() accepts three functions:

  • The collection reader returns an iterable, null, or undefined.
  • The key selector receives the concrete item and must return a unique, stable key.
  • The renderer receives a reactive ProgrammaticViewReader rather than an item snapshot.

Calling todo() reads the current value associated with that key. When a new object replaces an old object with the same key, AngularTS preserves its DOM nodes and updates bindings that read todo(). Reordering moves existing nodes, new keys create nodes, and removed keys dispose their nodes and bindings.

Duplicate keys are rejected through $exceptionHandler. Keep keys stable and unique for the lifetime of each logical item.

Create dynamic and namespaced elements

Use tag() when the HTML tag name is dynamic or cannot use property access:

const widget = tag('account-summary', { accountId: controller.accountId });

Use tagNS() or call tags with a namespace URI for SVG, MathML, or another XML namespace:

const svg = tags('http://www.w3.org/2000/svg');

const icon = svg.svg(
  { viewBox: '0 0 24 24' },
  svg.circle({ cx: 12, cy: 12, r: 10 }),
);

const otherCircle = tagNS('http://www.w3.org/2000/svg', 'circle', {
  cx: 12,
  cy: 12,
  r: 10,
});

Access the view context

Component views receive a ProgrammaticViewContext with these members:

MemberPurpose
controllerThe initialized component controller.
scopeThe scope that owns reactive bindings and generated DOM.
hostThe component host element.
transclude
onDestroyRegisters view-owned cleanup and returns a cancellation function.

Directive views receive a ProgrammaticViewContext. It adds required, containing controllers resolved from the directive’s require declaration. Its controller is undefined when none is declared.

Create a directive view

Directives can provide view instead of a template. Generated nodes continue through the normal compile and link pipeline, so registered directives on those nodes remain available.

app.directive('liveClock', () => ({
  view: ({ scope, onDestroy }) => {
    scope.now = new Date();
    const timer = setInterval(() => (scope.now = new Date()), 1000);
    onDestroy(() => clearInterval(timer));
    return time(() => scope.now.toLocaleTimeString());
  },
}));

Use it through normal compiled markup:

<div live-clock></div>

Own external resources

Reactive bindings, event listeners, keyed children, and compiled fragments are disposed automatically. Register resources created by application code with onDestroy():

view: ({ onDestroy }) => {
  const socket = new WebSocket('/events');
  onDestroy(() => socket.close());
  return div('Connected');
};

Cleanup runs when either the owning scope is destroyed or the host element is deallocated. The returned cancellation function transfers ownership elsewhere. A cleanup registered after destruction runs immediately. Cleanup errors are reported through $exceptionHandler.

Language integrations

The Kotlin, Scala, and Dart integrations expose typed wrappers for the same runtime model:

  • ProgrammaticTags wraps angular.tags.
  • ProgrammaticViewApi wraps angular.view helpers.
  • ProgrammaticViewContext exposes controller, required, scope, host, transclude, and onDestroy.
  • Integration each() renderers receive an item reader, matching TypeScript.

Plain callback functions are reactive in normal child and property positions. Use each integration’s props() helper when assigning a literal callback as a DOM property.

Choose templates or programmatic views

Use a programmatic view when direct DOM construction, dynamic tag selection, custom-element property assignment, or fine-grained imperative integration is clearer than markup. Use an HTML template when static document structure and designer-readable markup are more important.

Both forms use the same AngularTS module, scope, compiler, directive, and lifecycle infrastructure and can coexist in one application.

3.3 - Components

Understand components as registered UI units with a controller, inputs, lifecycle, and either an HTML template or typed view.

A component is a reusable UI unit registered on a module. It owns a DOM region, an optional controller, explicit inputs, and one rendering definition.

angular.getModule('todoApp').component('todoSummary', {
  bindings: { count: '<' },
  template: '<strong>{{ $ctrl.count }} remaining</strong>',
});

Use it as an HTML element:

<todo-summary count="remaining()"></todo-summary>

The module registration makes the component available to compiled DOM in that application. No annotation syntax, decorator, separate mount call, or global custom-element registration is required.

Controller and bindings

The controller stores component behavior and local state. In a template, $ctrl refers to that controller. Bindings define the component’s public inputs and outputs instead of exposing its internal scope.

Template or typed view

A component can render an HTML template, load a templateUrl, or provide a typed view function. These options share component registration, dependency injection, directives, child compilation, and cleanup.

Read choose a view style before selecting one for a new component.

3.4 - Reactive change detection with ES6 Proxy scopes

See how AngularTS replaces the AngularJS digest loop with ES6 Proxy interception, scheduling microtask DOM updates that only re-evaluate affected bindings.

Change detection is how a framework decides when to update the DOM to reflect new data. AngularJS used a polling mechanism called the digest cycle — every time something might have changed, AngularJS ran all registered watchers, compared old and new values, and repeated until nothing changed. AngularTS replaces this entirely with ES6 Proxy-based reactive observation: the framework knows exactly which property changed, which bindings depend on it, and schedules only those bindings to re-evaluate.

The digest cycle problem

In AngularJS, every watch registered anywhere in the application was checked on every digest. A large application could have thousands of watchers running on every user interaction, mouse move, or HTTP response. This was O(n) in the number of watchers per cycle, and cycles could chain into one another.

AngularTS eliminates this entirely. There are no cycles. There is no polling.

How Proxy-based reactivity works

When AngularTS creates a scope via createScope(), it wraps the plain object in a Proxy whose handler is a Scope instance. The Scope class implements the set trap:

set(target, property, value, proxy): boolean {
  target[property] = createScope(value, this); // recursively proxy nested objects

  if (oldValue !== value) {
    const listeners = this._watchers.get(property); // O(1) Map lookup
    if (listeners) {
      this._scheduleListener(listeners); // queue a microtask
    }
  }

  return true;
}

The key points:

  1. O(1) lookup: Listeners are stored in a Map<string, Listener[]> keyed by property name. When count changes, only listeners registered under 'count' are scheduled — not every watcher in the application.
  2. Microtask scheduling: _scheduleListener calls queueMicrotask(), which defers the flush until after the current synchronous call stack completes. Multiple changes to the same property in the same tick are coalesced.
  3. Recursive proxying: When you assign an object — $scope.user = { name: 'Alice' } — the new value is itself wrapped in a Proxy. Nested property changes ($scope.user.name = 'Bob') are tracked just as well as top-level ones.

A concrete example

  $scope.count = 0;

  $scope.increment = function () {
    $scope.count += 1;
    // At this point the Proxy set trap fires:
    // 1. Stores the new value
    // 2. Looks up listeners for 'count' in the watchers Map
    // 3. Schedules them via queueMicrotask
    // 4. Returns — no $apply() needed
  };
}]);
<div>
  <button ng-click="increment()">+</button>
  <span>{{ count }}</span>
  <!-- binding re-evaluates only when 'count' changes -->
</div>

When increment() fires, the Proxy intercepts the assignment to count, finds the binding registered for 'count', and schedules it. The DOM update happens on the next microtask — after increment() returns but before the browser renders the next frame.

Comparison with AngularJS

AngularJS (1.x)AngularTS
Detection mechanismDirty-checking digest loopES6 Proxy set trap
Watcher lookup costO(n) — all watchers checkedO(1) — direct Map lookup by property name
TriggerManual digest entry required for async codeAutomatic — Proxy intercepts every assignment
Update granularityAll watchers, all the timeOnly bindings for the changed property
Nested objectsShallow by default; $watchCollection needed for deepDeep — nested objects are automatically proxied
Async codeMust manually enter change detectionNo wrapping needed for standard async (Promise, fetch, etc.)

When no $apply is needed

Because the Proxy fires synchronously on every assignment, any code that assigns to a scope property — whether inside a controller, a service callback, or a Promise handler — automatically triggers the right DOM update:

  $scope.users = [];
  $scope.loading = true;

  // No $apply() needed — the Proxy intercepts the assignment
  $http.get('/api/users').then(({ data }) => {
    $scope.users = data;    // triggers DOM update for {{ users }}
    $scope.loading = false; // triggers DOM update for ng-if="loading"
  });
}]);

Tip: If you are integrating a third-party library that updates data outside of AngularTS (such as a raw WebSocket callback or a non-Promise-based timer), and the library stores results in scope properties, those assignments still flow through the Proxy and trigger updates automatically. No $apply wrapper is needed.

Microtask batching

Multiple property changes within the same synchronous block are batched. The listener scheduler enqueues a queueMicrotask flush only once per tick:

_enqueueScheduledTask(task: ScheduledTask): void {
  const scheduler = this._listenerScheduler;

  scheduler._queue.push(task);

  if (!scheduler._queued && !scheduler._flushing) {
    scheduler._queued = true;
    queueMicrotask(() => {
      this._flushScheduledTasks(); // runs all queued listener notifications at once
    });
  }
}

If you set ten properties in one synchronous function, all ten listener notifications are flushed together in a single microtask. The DOM is updated once, not ten times.

Post-render layout work

Use afterRender when a controller needs to read layout after AngularTS has applied the DOM work from the current flush:

class BoardGridController {
  afterRender() {
    this.realignShips();
  }
}

AngularTS coalesces afterRender to one callback per controller instance per render flush. The hook runs after directive and binding DOM mutations have completed, after linked children from structural directives such as ng-repeat exist, after class/style/attribute bindings for that flush are applied, and after one browser animation frame gives layout a chance to settle. It does not wait for external resources such as fonts or images by default.

For explicit scheduling outside the controller lifecycle, import afterRender or queueAfterRender:

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

afterRender(() => {
  this.realignShips();
});

If font metrics are required, opt in per callback:

afterRender(
  () => {
    this.realignLabels();
  },
  { fonts: true },
);

What objects are tracked

Not all objects are proxied. The isNonScope function explicitly excludes:

  • Browser built-ins: Window, Document, Element, Node, Event, Promise, Map, Set, WeakMap, Date, RegExp, typed arrays, Blob, File, URL, and others.
  • Objects that carry $nonscope: true on the instance or constructor.

This prevents the framework from wrapping DOM nodes or native collections in Proxies, which would be incorrect and slow.

  // Prevents this class from being wrapped in a Proxy
  static $nonscope = true;

  readonly apiBase = 'https://api.example.com';
}

// Or per-instance:
$scope.rawData = Object.assign(new SomeClass(), { $nonscope: true });

When to use watch

In the reactive proxy model, watch is rarely necessary for keeping the DOM in sync — that happens automatically. Use watch when you need to run side-effect code in response to a scope property changing:

$scope.watch('selectedTab', function (newTab) {
  externalTabWidget.activate(newTab);
});

// Trigger a service call when a search term changes
$scope.watch('searchQuery', function (query) {
  if (query && query.length > 2) {
    searchService.find(query).then((results) => {
      $scope.results = results;
    });
  }
});

// React to a computed condition
$scope.watch('items.length > 100', function (tooMany) {
  $scope.showPagination = tooMany;
});

Note: watch on a constant expression — a bare string literal, number, or boolean — is evaluated exactly once and the returned deregistration function is a no-op. The runtime detects the _constant flag on the compiled expression and avoids registering a watcher at all.

Watcher count and $$watchersCount

You can inspect how many active watchers are registered in a scope subtree:

console.log($scope.$$watchersCount);

This is computed by walking the _watchers Map and counting entries whose _scopeId matches any scope in the subtree. It is useful for identifying scopes with unexpectedly high watcher counts during performance profiling.

Performance characteristics

Only affected bindings update

A change to user.name schedules only the bindings that depend on name. Thousands of unrelated bindings are never touched.

No re-entrancy problems

The scheduler tracks a _flushing flag. If a listener notification causes another property change, that change is enqueued and flushed in a follow-up microtask rather than re-entrantly.

Deep tracking at zero cost

Nested objects are proxied at assignment time, not at watch time. There is no `` or explicit deep-watch flag — all depths are tracked uniformly.

Scope destruction cleans up

When destroy() is called, all watcher entries for that scope’s id are removed from the shared _watchers Map in a single O(n) pass, with O(1) swap-pop removal for each matched entry.

3.5 - Dependency injection and the AngularTS injector

Understand how the AngularTS injector resolves named tokens, how to annotate dependencies, and how the provider pattern controls the config and run phases.

Dependency injection (DI) is the mechanism AngularTS uses to supply components with the services they need. Rather than constructing dependencies directly, you declare what you need by name and the injector finds, instantiates, and delivers each dependency. This keeps components decoupled, composable, and straightforward to test in isolation.

How the injector works

When you call angular.bootstrap() (or use the ng-app directive), AngularTS calls createInjector() with your list of modules. The injector builds two internal caches:

  • Provider cache — holds provider objects, accessible during the config phase.
  • Instance cache — holds fully constructed service singletons, built on first request.

When a service is requested via $injector.get('myService'), the injector looks up myServiceProvider, calls its get method (injecting that method’s own dependencies), caches the result, and returns it. Every later get returns the same instance.

// Retrieve a service
const $http = injector.get('$http');

// Built-in token literals infer their registered service type.
const $machine = injector.get(angular.tokens.$machine);

// Custom services can provide an explicit result type.
const flags = injector.get<{ enabled: boolean }>('featureFlags');

// Check existence without throwing
if (injector.has('myOptionalService')) {
  const svc = injector.get('myOptionalService');
}

// Invoke a function with injection
injector.invoke([
  '$rootScope',
  '$http',
  function ($rootScope, $http) {
    // both dependencies are injected automatically
  },
]);

Applications that use the injector directly can declare their complete custom service registry once. Built-in token inference remains available alongside the application services, and invoke() and instantiate() infer their return types.

interface AppServices {
  auth: AuthService;
  featureFlags: { enabled: boolean };
}

const appInjector: ng.InjectorService<AppServices> = injector;
const auth = appInjector.get('auth');
const answer = appInjector.invoke(() => 42); // number
const session = appInjector.instantiate(Session); // Session

Declaring dependencies

AngularTS reads dependency names from the function’s arguments — but argument names are erased by minifiers. The safe, minification-proof approach is array annotation: an array where every element except the last is a dependency name string, and the last element is the function.

angular.createModule('myApp', []).controller('UserCtrl', [
  '$scope',
  '$http',
  'userService',
  function ($scope, $http, userService) {
    $scope.users = [];

    userService.getAll().then(({ data }) => {
      $scope.users = data;
    });
  },
]);

$inject property

class UserCtrl {
  static $inject = ['$scope', '$http', 'userService'];

  constructor(
    private $scope: ng.Scope,
    private $http: ng.HttpService,
    private userService: UserService,
  ) {
    $scope.users = [];
    userService.getAll().then(({ data }) => {
      $scope.users = data;
    });
  }
}

angular.createModule('myApp', []).controller('UserCtrl', UserCtrl);

Note: The $inject static property on a class is the TypeScript-idiomatic form of array annotation. AngularTS does not infer dependencies from function parameter names.

Explicit dependency annotation

Every dependency-bearing function must use array annotation or a static $inject property. An unannotated function with parameters throws immediately. This invariant makes dependency injection deterministic under minification.

Injectable types

value

A fixed JavaScript value registered with module.value(). It is not available during the config phase.

constant

Like value, but available during the config phase and cannot be overridden by a decorator.

factory

A function whose return value becomes the singleton. Must not return undefined.

service

A constructor function. Instantiated with new once; the instance is the singleton.

provider

The most flexible type. An object with a get method. Can expose configuration methods accessible during the config phase.

decorator

Wraps an existing service. Receives $delegate (the original) and returns a replacement or augmented version.

The provider pattern

Providers give you a way to configure a service before any instance is created. During the config phase the provider itself is injected (not the instance), allowing configuration. During the run phase and everywhere else, the instance produced by get is injected.

  private prefix = 'Hello';

  // Configuration method — callable during config phase
  setPrefix(value: string) {
    this.prefix = value;
  }

  // The injector calls get to produce the service instance
  get = ['$log', ($log: ng.LogService) => {
    const prefix = this.prefix;
    return {
      greet(name: string) {
        $log.info(`${prefix}, ${name}!`);
      },
    };
  }];
}

angular.createModule('myApp', [])
  .provider('greeter', GreeterProvider)
  .config(['greeterProvider', function (greeterProvider) {
    // During config, the *provider* is injected — note the "Provider" suffix
    greeterProvider.setPrefix('Greetings');
  }])
  .run(['greeter', function (greeter) {
    // During run, the *instance* is injected
    greeter.greet('world');
  }]);

Info: During the config phase, inject a provider by appending Provider to its name: greeterProvider for the greeter service. This naming convention is enforced by AngularTS internally.

Public Injectable Contracts

ng.InjectionTokenMap is the authoritative mapping from every public single-dollar token to its named contract. InjectorService.get() uses this map automatically:

const compile: ng.CompileService = $injector.get('$compile');
const rootScope: ng.RootScopeService = $injector.get('$rootScope');
const stateRegistry: ng.StateRegistryService = $injector.get('$stateRegistry');
const browserWindow: ng.WindowService = $injector.get('$window');

Framework-owned double-dollar tokens are internal implementation details and are deliberately absent from PublicInjectionTokens, InjectionTokenMap, and the ng namespace.

Every InjectionTokenMap[ value is a named ](../../../typedoc/interfaces/InjectionTokenMap.html)ng.\*contract exported through the generated TypeDoc entry point. Adding a public token without both its namespace contract and documentation export failsmake check.

Decorating existing services

A decorator intercepts an existing service and can replace, wrap, or augment it. The original instance is available as $delegate.

  .decorator('$log', ['$delegate', function ($delegate) {
      const originalInfo = $delegate.info.bind($delegate);

      $delegate.info = function (...args: any[]) {
        originalInfo('[DECORATED]', ...args);
      };

      return $delegate;
    }]);

Injecting into different artifact types

Controllers

Controllers receive $scope as their first dependency by convention, followed by any other services. Use array annotation or $inject.

app.controller('DashboardCtrl', [
  '$scope',
  '$http',
  function ($scope, $http) {
    /* ... */
  },
]);

Directives

Directive factories are injected like any other factory. The returned directive definition object is not itself injected.

app.directive('myWidget', [
  '$http',
  'dataService',
  function ($http, dataService) {
    return {
      restrict: 'E',
      link(scope) {
        dataService.load().then((data) => {
          scope.data = data;
        });
      },
    };
  },
]);

Filters

Filter factories are injectable. The returned filter function itself is not — it receives only the value and optional arguments from the template.

app.filter('truncate', [
  '$log',
  function ($log) {
    return function (text: string, limit = 80) {
      $log.info('truncating');
      return text.length > limit ? text.slice(0, limit) + '…' : text;
    };
  },
]);

Config and run blocks

Framework configuration is a typed declarative object. Run blocks have access to runtime services.

app.config({
  $http: {
    defaults: {
      headers: {
        common: { 'X-App': 'myApp' },
      },
    },
  },
});

// Run — services are available
app.run([
  '$rootScope',
  '$state',
  function ($rootScope, $state) {
    $rootScope.on('$stateChangeError', () => $state.go('error'));
  },
]);

$injector API reference

MethodDescription
$injector.get(token)Returns the service instance for token. Throws if not found.
$injector.has(token)Returns true if a provider for token is registered.
$injector.invoke(fn, self?, locals?)Calls fn with injected arguments. Optionally overrides specific tokens with locals.
$injector.instantiate(Type, locals?)Instantiates a constructor with new, injecting its dependencies.
$injector.annotate(fn)Returns the array of dependency names for fn.
$injector.loadNewModules(mods)Loads additional modules into a running injector.

3.6 - Modules: organizing your AngularTS application

Learn how AngularTS modules group controllers, services, directives, and config into composable units that the injector loads at bootstrap.

Every AngularTS application is assembled from one or more modules. A module is a named container that registers services, directives, controllers, filters, and configuration blocks. Modules do not execute any code themselves — they record recipes that the injector later uses to construct and wire together your application. This separation between declaration and instantiation is what makes AngularTS applications easy to test and compose.

Create and retrieve modules

Use angular.createModule() to create or replace a module. Its dependency list defaults to an empty array. Use angular.getModule() to retrieve a module that is already registered.

TypeScript

// Create a new module with no dependencies
const app = angular.createModule('myApp', []);

// Retrieve an already-registered module
const same = angular.getModule('myApp');

JavaScript

// Create a new module
var app = angular.createModule('myApp', []);

// Retrieve it elsewhere (e.g., in another file)
var same = angular.getModule('myApp');

Warning: Calling angular.createModule('myApp') a second time replaces the existing module. Use angular.getModule('myApp') when you only need to add registrations to the existing module.

Module registration methods

The NgModule instance returned by angular.createModule() or angular.getModule() exposes a fluent API for registering every kind of application artifact. All methods return the same module so calls can be chained.

.controller(name, fn)

Registers a controller constructor. The controller is instantiated by $controller and receives a child scope.

.service(name, ctor)

Registers a singleton service constructed with new. The constructor is instantiated once and reused.

.factory(name, fn)

Registers a factory function whose return value becomes the singleton. The function must not return undefined.

.provider(name, type)

Registers a full provider. The provider’s get method is called to produce the service instance. Providers can expose configuration methods accessible during the config phase.

.value(name, val)

Registers an arbitrary value as a service. Values cannot be injected during the config phase.

.constant(name, val)

Registers a constant that is available during both the config and run phases and cannot be overridden by a decorator.

.directive(name, fn)

Registers a directive factory for the $compile provider.

.filter(name, fn)

Registers a filter function for use in templates and $filter.

.config(settings)

Declares typed configuration for built-in services before they are instantiated. Configuration keys use the same names as their runtime injectables. Application code does not inject or configure provider objects directly.

.run(fn)

Runs after the injector has been created. Services are available. Use this for one-time initialization logic.

.router(definition)

Registers one RouterModuleDeclaration or a readonly forest. Literal route names, parameters, children, and resolves are inferred into the returned typed router module. Custom runtimes include router implementation code only when the router runtime is composed into the build.

A complete module example

The following example creates a module, registers a constant, a service, a controller, and a config block, then composes that module with a second utility module.

const utilModule = angular.createModule('util', []);

utilModule.constant('API_BASE', 'https://api.example.com');

utilModule.factory('logger', [
  '$log',
  function ($log) {
    return {
      info(msg: string) {
        $log.info('[app]', msg);
      },
      warn(msg: string) {
        $log.warn('[app]', msg);
      },
    };
  },
]);
const app = angular.createModule('myApp', ['ng', 'util']);
app.service('userService', UserService);
app.component('userList', { controller: UserListController });
app.router({ name: 'users', url: '/users', component: 'userList' });

Module dependencies

The requires array lists the names of other modules that must be loaded before the current one. The injector resolves the entire dependency graph recursively before running any config or run blocks.

  'ng',        // AngularTS core — always auto-loaded but explicit is fine
  'util',      // local utility module
  'ngRoute',   // third-party module
]);

Modules are loaded in dependency order, depth-first. If two modules declare the same service, the last one registered wins.

The built-in ng module

The ng module is registered automatically when the Angular class is instantiated. You never need to declare it as a dependency — it is always prepended to the bootstrap module list. It provides every core service and directive, including:

TokenDescription
$rootScopeThe root of the scope hierarchy
$compileTemplate compiler
$httpHTTP client
$interpolateTemplate interpolation
$parseExpression parser
$filterFilter registry
$animateAnimation support
$locationURL management
$machineReactive mode machines
$stateRouter state service
$eventBusPub/sub messaging

Tip: Splitting your application into small, focused modules makes unit testing dramatically easier. Each module can be loaded in isolation with angular.injector(['ng', 'myModule']), giving you a fully functional injector without bootstrapping the DOM.

Special module methods

Beyond the standard registration methods, NgModule supports several higher-level conveniences:

app.model('user', () => ({ name: 'John', authenticated: false }));
app.machine('sessionMachine', {
  initial: 'setup',
  data: {},
  states: { setup: {} },
});
app.workflow('onboarding', workflowConfig);
app.wasm('mathLib', { source: '/wasm/math.wasm' });
app.worker('backgroundWorker', '/workers/bg.js');
app.rest('Post', '/api/posts', Post);
app.store('prefs', UserPreferences, 'local');
app.sse('notifications', '/events/stream');
app.websocket('chat', 'wss://chat.example.com/ws');

These methods create typed provider-registration commands. The injector applies them through its private registry during bootstrap; the registry is not injectable application state.

3.7 - Scopes, data binding, and the scope hierarchy

Explore how AngularTS scopes work as reactive ES6 Proxies, how the hierarchy flows from $rootScope, and how to use events and watchers effectively.

A scope in AngularTS is the glue between the model and the view. It is a plain JavaScript object wrapped in an ES6 Proxy so that property assignments automatically trigger DOM updates without any manual notification. Every controller, directive, and component operates against a scope, and those scopes are organized into a tree rooted at $rootScope.

What a scope actually is

When you write $scope.count = 0 in a controller, AngularTS is not storing count on a special object — it is storing it on an ordinary JavaScript object. The Proxy layer intercepts the set trap and, whenever a watched property changes, schedules the affected binding listeners to re-evaluate on the next microtask.

set(target, property, value, proxy): boolean {
  // ... after storing the value ...
  const listeners = this._watchers.get(property);
  if (listeners) {
    this._scheduleListener(listeners); // queues a microtask flush
  }
  return true;
}

The result is that you write natural JavaScript assignments and the DOM stays in sync — no $apply() call required for synchronous code paths.

Creating scopes

Scopes are created by the framework, not by application code. The injector creates $rootScope when the ng module loads. Each ng-controller directive, isolate directive, and ng-repeat iteration creates a child scope derived from its parent.

You can create child scopes manually when building custom directives:

const child = $scope.new();

// Isolate child — does not inherit watchable properties
const isolate = $scope.newIsolate();

// Transcluded child — linked to an outer parent scope
const transcluded = $scope.transcluded(outerScope);

Info: $scope.new() sets the new scope’s prototype to the parent’s underlying target object. Reading a property that does not exist locally will walk the prototype chain, just like plain JavaScript objects.

The scope hierarchy

Every scope holds a reference to root (the root scope) and parent (the scope that created it). Child scopes are tracked in the _children array.

$rootScope
├── MainCtrl scope
│   ├── SidebarCtrl scope
│   └── ContentCtrl scope
│       ├── ng-repeat item scope (item 0)
│       ├── ng-repeat item scope (item 1)
│       └── ng-repeat item scope (item 2)
└── NavCtrl scope

You can search for a named scope from anywhere in the tree:

<section ng-scope="userPanel">
  <user-panel></user-panel>
</section>

ng-scope requires a non-empty name. It names the current scope; it does not create a new one. Find that scope from JavaScript:

// Find it from $rootScope
const panel = $rootScope.searchByName('userPanel');

// Or via the angular instance
const panel2 = angular.getScopeByName('userPanel');

Scope events

Scopes communicate across the hierarchy through a lightweight event system. Events travel either upward or downward — never both directions at once.

emit — upward

// Fire an event from a child scope upward toward $rootScope
$scope.emit('user:selected', { id: 42 });

// A parent scope listens
$scope.on('user:selected', function (event, user) {
  console.log('Selected user ID:', user.id);
});

broadcast — downward

// Broadcast from $rootScope down to all descendants
$rootScope.broadcast('config:updated', newConfig);

// Any child scope can listen
$scope.on('config:updated', function (event, config) {
  $scope.theme = config.theme;
});

The on method returns a deregistration function. Call it when the listener is no longer needed:

// Later, when the listener should stop
deregister();

Stopping propagation

emit propagation can be stopped before it reaches $rootScope:

  event.stopPropagation(); // stops upward travel
  event.preventDefault();  // signals that the default was prevented
});

Watching scope properties

In the reactive proxy model, you rarely need watch — template bindings update automatically. Use watch when you need to run side-effect code in response to a data change, such as syncing to an external library or triggering a service call.

const deregister = $scope.watch('searchQuery', function (newValue, target) {
  if (newValue) {
    searchService.query(newValue).then((results) => {
      $scope.results = results;
    });
  }
});

// Lazy watch — skips the initial call, only fires on changes
$scope.watch(
  'count',
  function (newValue) {
    console.log('count changed to', newValue);
  },
  /* lazy */ true,
);

// Stop watching when done
deregister();

Note: watch on a constant expression (a literal string or number) is evaluated once and the deregistration function is a no-op. The runtime detects this via the _constant flag on the compiled expression.

Watchable expression types

watch accepts any expression that $parse can compile — identifiers, member expressions, binary comparisons, function calls, array and object literals, and conditional expressions.

$scope.watch('count > 10', handler); // binary expression
$scope.watch('items.length', handler); // member on array
$scope.watch('getTotal()', handler); // function call
$scope.watch('a || b', handler); // logical expression
$scope.watch('[firstName, lastName]', handler); // array expression

Merging scope state

// merge copies properties from a plain object into the scope
$scope.merge({ name: 'Alice', age: 30 });

Destroying a scope

When a controller’s element is removed from the DOM, AngularTS calls destroy() on the associated scope. You can also call it manually. Destroying a scope:

  1. Broadcasts $destroy downward to all children.
  2. Removes all watcher registrations for this scope’s ID from the shared _watchers map.
  3. Clears all on listeners.
  4. Removes itself from the parent’s _children array.
  5. Sets _destroyed = true and nulls internal references on the next microtask.
const tempScope = $rootScope.new();
tempScope.data = loadSomething();

// When done:
tempScope.destroy();

Warning: Do not read from or write to a scope after calling destroy(). The scope’s property map is reduced to a minimal tombstone on the next microtask.

Relationship to controllers and directives

Each ng-controller directive asks $scope.new() to create a child scope and passes it to the controller constructor as $scope. The controller writes model properties directly onto that scope:

<div>
  <button ng-click="increment()">+</button>
  <span>{{ count }}</span>
</div>
  $scope.count = 0;

  $scope.increment = function () {
    $scope.count += 1; // Proxy intercepts; DOM updates automatically
  };
}]);

Directives with scope: true get an inherited child scope; directives with scope: {} get an isolate scope. The isolate scope does not walk the parent prototype chain for watchable properties, but it can still receive values through explicit bindings.

Scope API quick reference

MethodDescription
$scope.new(child?)Creates an inherited child scope.
$scope.newIsolate(instance?)Creates an isolate child scope.
$scope.watch(expr, fn, lazy?)Registers a watcher; returns a deregistration function.
$scope.on(name, fn)Registers an event listener; returns a deregistration function.
$scope.emit(name, ...args)Fires an event upward through the hierarchy.
$scope.broadcast(name, ...args)Fires an event downward to all descendants.
$scope.merge(obj)Copies enumerable properties from obj into the scope.
$scope.destroy()Tears down the scope and all its watchers.
$rootScope.searchByName(name)Finds a scope by its scopeName anywhere in the tree.
$scope.getById(id)Finds a scope by numeric ID within the subtree.

3.8 - Templates, interpolation, and expression parsing

Master the {{ }} interpolation syntax, one-time bindings, filter pipes, and how $interpolate and $parse power programmatic expression evaluation.

Templates in AngularTS are ordinary HTML files augmented with a handful of special syntax forms. The most fundamental is interpolation: the {{ expression }} delimiters that tell the template compiler to evaluate an expression against the current scope and render its result as text. The $interpolate service handles this translation at compile time, and the reactive proxy system ensures the DOM re-renders whenever the underlying data changes.

Interpolation syntax

Place any valid AngularTS expression between {{ and }} to render its current value:

<p>Hello, {{ user.name }}!</p>

<!-- Method call -->
<p>Total: {{ cart.getTotal() }}</p>

<!-- Arithmetic -->
<p>Items: {{ items.length }}</p>

<!-- Ternary -->
<p>Status: {{ isActive ? 'Online' : 'Offline' }}</p>

<!-- String concatenation -->
<p>{{ firstName + ' ' + lastName }}</p>

The expression is parsed by $parse, evaluated against the current scope, and the result is stringified and inserted into the text node. If the expression throws, $interpolate catches and routes the error through $exceptionHandler.

What expressions can contain

AngularTS expressions are a safe subset of JavaScript. They support:

Property access

user.profile.name, items[0].label

Method calls

format(date), list.filter(fn)

Arithmetic and comparison

count * price, score >= 100

Logical operators

isAdmin || isMod, name && name.length

Ternary

flag ? 'yes' : 'no'

Array and object literals

[a, b, c], { key: value }

Expressions cannot contain new, delete, typeof, void, assignment operators, or any access to the global scope. This sandbox keeps templates safe against inadvertent or malicious code execution.

Filters in templates

Apply a filter to an expression using the pipe character |. Filters transform the value before it is rendered. You can chain multiple filters:

<p>Price: {{ product.price | currency:'USD' }}</p>

<!-- Sort a list by a property -->
<li ng-repeat="item in items | orderBy:'name'">{{ item.name }}</li>

<!-- Limit a list and filter it by a search term -->
<li ng-repeat="item in items | filter:search | limitTo:10">{{ item.title }}</li>

<!-- Date formatting -->
<span>{{ event.date | date:'longDate' }}</span>

<!-- Chained filters -->
<p>{{ message | uppercase | limitTo:50 }}</p>

Filters receive the value as their first argument and any parameters after the colon as later arguments. They are pure functions — given the same input, they always produce the same output, which lets the reactive system cache their results.

One-time bindings

Prefix an expression with :: to evaluate it once and then release the binding. This is a performance optimization for data that is set during initialization and never changes:

<h1>{{ ::pageTitle }}</h1>
<p>User ID: {{ ::user.id }}</p>

<!-- Inside ng-repeat — binds each item once -->
<li ng-repeat="item in ::staticList">{{ ::item.name }}</li>

Tip: Use one-time bindings for any data that is effectively constant after initialization: configuration values, user identity, translated strings, or items loaded once at startup. They reduce the number of active watchers and improve scroll performance in long lists.

The $interpolate service

The $interpolate service is what $compile uses internally to process interpolation expressions in templates. You can use it directly when you need to evaluate an interpolation string programmatically.

  '$scope', '$interpolate',
  function ($scope, $interpolate) {
    // Compile a template string into a reusable function
    const greetFn = $interpolate('Hello, {{ name }}! You have {{ count }} messages.');

    $scope.name = 'Alice';
    $scope.count = 5;

    // Evaluate the template against a context object
    const result = greetFn($scope);
    // → "Hello, Alice! You have 5 messages."

    // The interpolation function exposes the raw expressions it found
    console.log(greetFn.expressions); // ['name', 'count']
    console.log(greetFn.exp);         // 'Hello, {{ name }}! You have {{ count }} messages.'
  },
]);

$interpolate parameters

  text: string,
  mustHaveExpression?: boolean,  // return undefined if no {{ }} found
  trustedContext?: SceContext,   // SCE context for security checking
  allOrNothing?: boolean,        // return undefined if any expression is undefined
): InterpolationFunction | undefined

The returned InterpolationFunction is callable with (context, cb?). When cb is provided, $interpolate sets up watch calls on the scope so cb is invoked whenever any expression in the template changes.

// Watch mode: cb fires on every change
fn($scope, (newValue) => {
  titleElement.textContent = newValue;
});

Customizing the delimiters

The default delimiters are {{ and }}. Configure them with module.config({ $interpolate: ... }):

angular.createModule('app', []).config({
  $interpolate: {
    startSymbol: '{[',
    endSymbol: ']}',
  },
});

After this change, templates use {[ expression ]} instead. This is useful when you are embedding AngularTS in a server-side template engine that already uses {{ (such as Jinja2 or Handlebars).

The $parse service

$parse is the lower-level primitive beneath $interpolate. It compiles a single expression string into a CompiledExpression function that can be called with a context:

  '$scope', '$parse',
  function ($scope, $parse) {
    $scope.user = { name: 'Bob', score: 42 };

    // Compile once, call many times
    const getScore = $parse('user.score');

    console.log(getScore($scope));  // 42

    // Expressions with assign support two-way binding
    const nameParse = $parse('user.name');
    nameParse.assign($scope, 'Carol');
    console.log($scope.user.name); // 'Carol'

    // Parsed expressions are cached — repeated calls with the same
    // string return the same compiled function object
    const getScore2 = $parse('user.score');
    console.log(getScore === getScore2); // true
  },
]);

Compiled expression properties

PropertyTypeDescription
_constantbooleantrue if the expression is a literal constant
_literalbooleantrue if the expression is a simple literal
_assignfunction?Present for l-value expressions; assigns a value to the context
_inputsany[]?Sub-expressions tracked for change detection
_decoratedNodeBodyNodeThe annotated AST for this expression

Security and binding policies

Plain interpolation produces text. HTML bindings such as ng-bind-html use $compile.htmlPolicy to sanitize strings or reject them before insertion. Configure this callback with your application’s HTML sanitizer:

angular.createModule('htmlApp', []).config({
  $compile: {
    htmlPolicy: (html) => DOMPurify.sanitize(html),
  },
});
<p>{{ userComment }}</p>
<p ng-bind-html="userComment"></p>

Without an HTML policy, nonempty HTML strings are rejected. The framework also accepts native TrustedHTML. See binding policies for URL and script contexts.

Escaping interpolation delimiters

If you need to display literal {{ in a template without triggering interpolation, escape each character with a backslash:

<p>The syntax is \{\{ expression \}\}</p>

$interpolate unescapes these sequences before returning the final string.

3.9 - Glossary

Plain-language definitions of browser and AngularTS terms used throughout the documentation.

Application: The DOM region started by ng-app or angular.bootstrap().

Attribute: A setting written inside an HTML opening tag.

Binding: A connection that transfers a value between state and the DOM.

Bootstrap: Start an application on a specific DOM element.

Compiler: The AngularTS runtime that discovers directives and connects them to DOM nodes. It does not produce a build artifact.

Component: A registered, reusable UI unit with inputs, behavior, and a view.

Controller: An object or function that supplies state and behavior to a DOM region or component.

Dependency injection: Requesting a registered value by token instead of constructing or finding it directly.

Directive: An HTML attribute or element that adds AngularTS behavior.

DOM: The browser’s live object tree for an HTML document.

Expression: A small JavaScript-like statement evaluated against a scope in an HTML template.

Module: A named registry for an application’s components, directives, services, configuration, and dependencies.

Reactive: Able to notify dependent bindings when a value changes.

Scope: The reactive state and lifecycle context available to a compiled DOM region.

Service: Shared behavior or data created and supplied by the injector.

Template: HTML containing AngularTS directives and expressions.

Token: The name or value used to request a dependency.

Typed view: A TypeScript function that constructs component DOM with typed tags, properties, controller access, and reactive readers.

View: The DOM output rendered by a component, directive, or router state.

4 - Task guides

Implement complete AngularTS workflows with explicit state, failure, lifecycle, accessibility, and verification behavior.

These guides extend one task-board application. They explain implementation boundaries rather than presenting isolated snippets.

Start with server-rendered HTML

Do not start with an SPA. Keep the model, rules, URLs, and rendering together on the server. Send useful HTML first, then add AngularTS to the smallest region that needs browser behavior.

Even when using the router, prefer server-rendered route fragments. Move the model into the browser only for the most complex client-owned features. When the browser takes over model consistency and compilation, use typed programmatic views as the primary view style. REST services, explicit caching, and a browser-facing security policy also become primary at that same threshold. Until then, prefer normal HTTP pages, forms, and server-rendered fragments.

Apply this decision per module. A server-rendered login and public site can live beside a dashboard SPA in the same application. The dashboard’s complexity does not require every other module to move into the browser.

Read Keep the application on the server before choosing the application structure.

Build the application

  1. Keep the application on the server
  2. Structure the application
  3. Build and validate a form
  4. Load server data
  5. Add routing
  6. Handle errors
  7. Test behavior

Make it production-ready

Start with the complete tutorial if you have not built an AngularTS component.

4.1 - Keep the application on the server

Render HTML and keep domain logic on the server until the browser must own a genuinely complex application model.

Start with HTML from the server

Do not start by building an SPA. Render the useful page on the server and use normal links and forms. Keep domain models, validation, authorization, transactions, URLs, and HTML generation together for as long as possible.

This is the simplest way to keep the model consistent. The server reads current data, applies the rules, and renders the result in one place. The browser receives HTML it can display immediately. There is no second model to synchronize, no client startup request for content the server already had, and no duplicated loading, routing, validation, and error behavior.

HTML and server rendering are the default application architecture. AngularTS is there to add behavior where the browser can improve an already useful page.

Add behavior without taking over the page

Put ng-app around the smallest region that needs local interaction. Use scope, events, effects, HTTP directives, and DOM references for immediate browser behavior while the server continues to own the page and its durable model.

Keep these on the server:

  • Initial and durable application state
  • Validation and authorization
  • Data loading and mutations
  • Primary URLs and navigation
  • HTML for complete pages and reusable fragments
  • Error, empty, permission, and maintenance responses

Do not fetch JSON after startup merely to rebuild HTML that the server could have sent in the first response.

Keep router content server-rendered

Using the AngularTS router does not require moving rendering or the model into the browser. Let the router coordinate transitions and component lifetimes, but keep route content as server-rendered HTML fragments while that remains practical.

This preserves one server model while still allowing faster transitions, long-lived browser resources, and local interaction state. A router is not a reason to duplicate server templates as client templates.

Move the model only as a last resort

Move model ownership into the browser only when the feature genuinely needs it. Examples include substantial offline work, high-frequency local editing, complex cross-screen state, or an application-like workspace whose model must remain alive independently of server navigation.

Before making that move, confirm that server-rendered fragments, focused interactive regions, or a small client model cannot solve the problem. Most pages should never cross this line.

Move compilation with the model

Once the browser owns the model, it also owns model consistency and view compilation. Do not keep a complex client model on one side and loosely typed HTML template bindings on the other.

For a client-owned screen, optional typed programmatic views can keep model access, DOM properties, events, and generated structure in the same checked TypeScript code. HTML templates remain available for client-owned screens and are the default for server-owned pages, server-rendered fragments, and small enhancements.

Move the data boundary with the model

Before the browser owns the model, prefer simple HTTP: normal navigation, form submissions, and server-rendered HTML fragments. The server already has the authoritative data and can return the final representation directly.

Once the browser owns a complex application model, REST services become the primary server boundary. At that point the frontend also needs an explicit cache strategy and security policy:

  • Define stable request, response, validation, pagination, and error contracts.
  • Decide which model owns each cached record and how writes invalidate it.
  • Handle stale data, concurrent writes, retries, and offline state deliberately.
  • Define credential handling, authorization failures, CSRF or token policy, cross-origin access, content security, and how client code avoids secrets.

Do not introduce this REST, cache, and security machinery for a page that can submit a form or request a server-rendered fragment. It becomes primary only when the browser has genuinely taken ownership of the model.

Let modules use different architectures

The ownership decision is not application-wide. AngularTS applications are multimodular, and each module or ng-app root can stop at a different point in the progression.

For example:

AreaAppropriate architecture
Sign-in and account recoveryServer-rendered HTML forms and normal HTTP
Public content and checkoutServer pages with focused AngularTS enhancements
Dashboard workspaceClient router, browser-owned model, REST, cache policy, and a chosen view style
Reports and downloadsServer-generated files and normal browser navigation

The login form does not need to become part of the dashboard SPA. The dashboard can start as its own ng-app after authentication and own only the screens whose complexity justifies it. Both modules can share the same server session and authorization rules without sharing a frontend architecture.

This keeps complexity contained. A client-owned dashboard does not force public, authentication, settings, or support pages to adopt its router, model cache, or rendering strategy.

Use this order

Start hereEscalate only when needed
Server-rendered pageSmall AngularTS-enhanced region
Normal links and formsServer-rendered router fragment
Server-owned modelSmall browser interaction model
Simple HTTP pages and fragmentsREST, cache, and browser security policy
Server HTML templatesOptional typed view when stronger checks are useful
One server-rendered moduleOne client-owned module where justified

At every step, stop when the current approach solves the user problem. The last row is not a target architecture. It is the option for the minority of features that truly need the browser to own the application model.

Verify the decision

For each client-owned model, write down why a server-rendered page, server fragment, or focused enhancement is insufficient. Test direct navigation, JavaScript failure, authorization, validation, refresh, Back and Forward, stale data, deployment rollback, and recovery after a failed request.

4.2 - Structure an application

Separate AngularTS registration, UI state, domain operations, and external resources without adding speculative layers.

Start with server rendering

Follow the server-first guideline. Keep domain models, validation, authorization, navigation, and HTML rendering together on the server. Render the useful document and initial state before AngularTS starts.

Place an AngularTS root around the smallest region that needs browser behavior. Do not create a browser copy of the domain model merely to make the page interactive. Local selection, disclosure, pending state, and DOM behavior can remain small browser concerns while durable state stays on the server.

Do not choose an SPA as a default project shape. If client routing is justified, continue loading server-rendered route fragments while the server can own the model. Move the model into the browser only when complex offline, editing, cross-screen, or long-lived workspace behavior requires independent client ownership.

When that move happens, the browser becomes responsible for model consistency and view compilation. If compile-time checks between that model and its DOM are valuable, the module can use optional typed programmatic views. HTML templates remain supported.

The same threshold changes server communication. Server-owned pages should use normal HTTP navigation, forms, and rendered fragments. A browser-owned model needs REST contracts, explicit cache ownership and invalidation, and a complete browser security policy. Introduce those systems together with frontend model ownership, not before it.

Mix architectures by module

Do not force one rendering architecture across the product. Use separate modules or ng-app roots for areas with different ownership needs.

A sign-in module can remain a server-rendered HTML form. After authentication, a dashboard module can bootstrap a client router, keep a complex model in the browser, and use REST and caching. It can independently choose HTML or typed programmatic views. Public pages, reports, and account recovery can continue using server rendering and normal navigation.

Keep shared concerns on the server: session identity, authorization, domain rules, and durable state. Share frontend services only when two client-owned modules genuinely need the same browser resource or model.

Start from ownership, not file types

A component owns DOM-facing state and its view lifetime. A model owns shared reactive domain state. A service owns an operation or external resource. A module records how those pieces are assembled.

Use one feature directory until a capability is independently reusable:

tasks/
  task-module.ts
  task-list.ts
  task-repository.ts
  task-list.test.ts

Avoid global controllers/, services/, and views/ directories once features evolve independently; they scatter one change across the repository.

Register one feature module

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

export class TaskRepository {
  async list() {
    const response = await fetch('/api/tasks');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }
}

export const tasks = angular
  .createModule('tasks', [])
  .service('taskRepository', TaskRepository);

Keep registration free of request execution and DOM access. Registration describes recipes; runtime methods perform work.

Keep view state local

A task-list controller may own loading, error, selection, and filter text. Move task data to a named model only when multiple component roots need reactive access to the same records. Move loading into a repository only when the repository intentionally deduplicates or caches requests.

Do not introduce “manager” or “facade” layers without a lifetime or policy they own. A forwarding class adds indirection but no boundary.

Verify the structure

A feature passes the ownership test when its component can be destroyed without leaking resources, its domain logic can be tested without a DOM, and its repository can be replaced in a test without changing view code.

4.3 - Build and validate a form

Model AngularTS form submission, validation visibility, duplicate prevention, and accessible recovery as one workflow.

Separate validity from validation visibility

A field can be invalid before the user has interacted with it. Show field feedback after blur or a failed submit, but always prevent invalid submission.

<form name="taskForm" ng-submit="submit(taskForm)" novalidate>
  <label for="task-title">Task title</label>
  <input
    id="task-title"
    name="title"
    ng-model="draft.title"
    required
    aria-describedby="task-title-error"
  />
  <p
    id="task-title-error"
    role="alert"
    ng-show="submitted && taskForm.title.$invalid"
  >
    Enter a task title.
  </p>
  <button ng-disabled="saving">Add task</button>
</form>

The controller owns submitted, saving, and the editable draft. The repository owns the write.

$scope.submit = async (form) => {
  $scope.submitted = true;
  if (form.$invalid || $scope.saving) return;

  $scope.saving = true;
  try {
    await taskRepository.add($scope.draft);
    $scope.draft = { title: '' };
    $scope.submitted = false;
  } finally {
    $scope.saving = false;
  }
};

Do not clear the draft before the server confirms success. Disable or reject repeated submission while a non-idempotent write is pending. Return focus to the first invalid field after a failed submit and announce server failures separately from field validation.

Use browser constraints for semantics, AngularTS form state for view coordination, and server validation as the authority. A disabled button does not enforce a business rule.

4.4 - Load server data

Own AngularTS HTTP requests, cancellation, stale-result protection, and explicit screen states.

Represent request state explicitly

Do not use tasks.length === 0 to mean both “not loaded” and “loaded empty.”

type LoadState = 'idle' | 'loading' | 'ready' | 'error';

class TaskList {
  state: LoadState = 'idle';
  tasks: Task[] = [];
  error = '';
}

Put transport behavior in a repository

app.factory('taskRepository', [
  '$http',
  ($http) => ({
    list: (cancelled) =>
      $http
        .get('/api/tasks', { timeout: cancelled })
        .then((response) => response.data),
  }),
]);

The timeout promise should resolve when the component is destroyed or a newer query supersedes the request. Cancellation saves work; a request sequence or identity check still protects against transports that complete while cancellation is racing.

Set loading before starting. On success, replace the collection property so typed readers observe the commit. In finally, clear loading only if the completing request is still current. Render loading, stale data, empty success, recoverable error, and permission failure deliberately.

Use the $http guide for interceptors, defaults, caching, and response contracts.

4.5 - Add routing

Register AngularTS route trees, choose resolve boundaries, and make navigation cleanup and failure behavior explicit.

Do not default to client-side routing

Use server routes and ordinary links first. They provide addressability, history, streaming HTML, metadata, authorization boundaries, and resilient navigation without a client router.

Introduce the AngularTS router when multiple screens are intentionally owned by one bootstrapped application and preserving client state or resources between those screens materially improves the product. A client router is not required for AngularTS components embedded in server-rendered pages.

Keep route snippets server-rendered for as long as possible. Let the router own the transition, URL state, and component lifetime while the server still renders the route HTML from its authoritative model. Do not move the model and rendering into the browser merely because navigation no longer reloads the document.

If a route eventually needs a complex browser-owned model, move compilation with that model. A typed programmatic view is available when the screen benefits from compile-time checks between the model and DOM; it is not required by the router.

That transition also makes REST services, frontend cache consistency, and browser-facing security policy primary concerns. A router using server-rendered fragments can continue using simple HTTP; client navigation alone does not justify building a separate API and cache layer.

Scope the router to the module that needs it. A dashboard can be a full SPA under its own ng-app while sign-in, recovery, public pages, and downloads remain server-rendered routes. Adding a dashboard router does not make the entire application a client-owned router tree.

Register components as screens

The router is part of the built-in runtime; do not add a speculative ng.router module dependency.

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

const routes = [
  {
    name: 'tasks',
    url: '/tasks',
    component: 'taskList',
  },
  {
    name: 'tasks.detail',
    url: '/{taskId:int}',
    component: 'taskDetail',
  },
] as const satisfies readonly ng.RouterModuleDeclaration[];

app.router(routes);
<nav aria-label="Tasks">
  <a ng-state="'tasks'">All tasks</a>
</nav>
<main ng-view></main>

Use stable state names as application identifiers. URLs may evolve; state names are referenced by links, transitions, tests, and diagnostics.

Resolve identity or authorization data that must exist before the screen can be valid. Load optional panels and large collections inside the component so the route can render a shell. Parent states own shared layout and resolves; children should not refetch parent-owned data.

Test direct URL entry, back/forward, invalid parameters, same-state parameter changes, rejected resolves, and leaving during pending work. Continue with the routing overview.

4.6 - Handle errors

Classify AngularTS domain, transport, transition, and programming failures and give each one the correct owner.

Classify before rendering

  • Validation and domain rejection are expected outcomes near the action.
  • HTTP timeout, abort, offline, and non-success status belong to the request owner.
  • Route resolve failure belongs to transition policy.
  • Unexpected exceptions belong to $exceptionHandler and diagnostics.

Do not convert every rejection to [], null, or false. That destroys information and commonly renders an empty success state after a real failure.

Wrap low-level errors with the operation name while retaining the original cause. The UI should receive a stable category and recovery action, not a stack trace or server payload. Logs may include release, route, operation, and correlation identifiers but must exclude credentials and private data.

A repository may retry a safe idempotent read. A component should own the retry button. Global interceptors should refresh credentials or normalize protocol behavior, not retry every request indiscriminately.

Unexpected exceptions should reach the $exceptionHandler reference. Test that expected failures remain recoverable and unexpected exceptions are reported.

4.7 - Test application behavior

Allocate AngularTS tests across plain logic, typed contracts, browser integration, lifecycle, and failure boundaries.

Use the narrowest layer that can fail

Test transformations and domain transitions as plain TypeScript. Compile typed component views to catch invalid model properties, event types, and DOM property names. Use a browser for compilation, directives, focus, routing, and reactive rendering.

test('task form prevents duplicate submission', async ({ page }) => {
  await page.goto('/tasks');
  await page.getByLabel('Task title').fill('Write tests');
  await page.getByRole('button', { name: 'Add task' }).dblclick();
  await expect(page.getByRole('listitem')).toHaveCount(1);
});

Select by role, label, visible text, or stable application identifiers. Scope IDs, generated comments, internal CSS classes, and scheduler timing are implementation details.

For each asynchronous screen, cover loading, stale data, empty success, recoverable failure, retry, cancellation on navigation, and late completion. Control server responses rather than adding sleeps. For resource owners, enter and leave repeatedly and assert listener and connection counts return to baseline.

Run critical browser contracts against the production bundle.

4.8 - Troubleshoot an application

Locate AngularTS bootstrap, injection, compilation, reactive, routing, and request failures by testing boundaries in order.

Find the first failed boundary

  1. Confirm the JavaScript module loaded.
  2. Confirm registration completed before automatic or manual bootstrap.
  3. Confirm ng-app names the created module.
  4. Read the first AngularTS error, not the cascade.
  5. Confirm injected token order matches function parameters.
  6. Confirm the owning scope/controller contains the value read by the view.
  7. Inspect transport status before changing rendering code.

Raw interpolation means bootstrap or compilation did not complete. “Module not available” usually means boot order or spelling. “Unknown provider” means the token is absent from the loaded module graph.

For typed views, verify changing positions receive a function reader rather than a snapshot. Call keyed item readers inside nested bindings. Replace collection properties in controller methods. ESM applications import tag factories such as div and button directly from @angular-wave/angular.ts; no-build UMD applications use angular.tags. Controller typing is supplied to component<T>(); use DOM property names such as htmlFor.

Remove sibling features while preserving the failing owner, data, and timing. Do not hide races with timeouts. Use the error catalog after identifying the failed boundary.

4.9 - Production and deployment

Produce, verify, deploy, observe, and roll back an AngularTS artifact without relying on development behavior.

Produce one immutable artifact

Install from the lockfile, typecheck source and tests, validate generated bindings, run browser tests, and build once. Promote the same artifact through environments. Rebuilding after approval changes the release.

Fingerprint immutable assets. Revalidate HTML and route manifests so they reference current fingerprints. Version service-worker caches and remove obsolete entries only after the new worker can serve a complete application.

Verify production behavior

Smoke-test generated output with development fallbacks disabled. Cover direct route entry, refresh on nested URLs, CSP, API credentials, loading and failure states, offline behavior, and service-worker upgrade from the previous release.

Design rollback before deployment

Retain the previous artifact. Determine whether storage migrations, server contracts, or service-worker caches make rollback incompatible. Prefer additive data changes.

Attach a release identifier to errors and operations. After deployment, verify asset integrity, critical routes, request latency, error rate, and one synthetic workflow. Stop promotion or roll back from explicit thresholds.

Follow the upgrade procedure for framework changes.

4.10 - Browser support

Define an AngularTS browser contract from required Web APIs, compiled output, fallbacks, and production tests.

List browser families and versions, mobile requirements, assistive-technology coverage, and optional capabilities. Framework support does not automatically provide WebTransport, service workers, storage, workers, or WebAssembly everywhere.

Isolate capability selection:

function createRealtime() {
  if ('WebTransport' in window) return createWebTransport();
  if ('WebSocket' in window) return createWebSocket();
  return createPolling();
}

Detect the API used, not a browser brand. Keep fallback selection in one service so components consume domain events rather than transport differences.

Test production output: cold load, direct route entry, back/forward, storage denial, offline transition, reduced motion, keyboard navigation, and service-worker upgrade. Disable capabilities to test fallbacks even when CI supports them.

When no fallback exists, render an actionable unsupported state before constructing the service. A runtime exception is not a support policy.

4.11 - Accessibility

Preserve semantics, focus, validation, announcements, and motion preferences across AngularTS structural and route updates.

ng-if removes content and destroys focused descendants. Before closing a dialog, menu, or editor, choose the element that regains focus. ng-show retains the subtree; verify hidden descendants cannot remain in keyboard order.

Use native buttons, links, labels, headings, and lists before ARIA. Attach AngularTS events to those controls instead of rebuilding their keyboard behavior on generic elements.

Associate errors with controls using aria-describedby. Show invalid state after interaction or submission, retain the invalid value, focus the first invalid control after failed submission, and provide a summary for long forms. Server failures are separate from field constraints.

Use a polite live region for completion or background updates and an alert for failures requiring immediate attention. Do not place an entire keyed list in a live region.

Test direct route entry, route transitions, keyed reordering, loading replacement, reduced motion, zoom, keyboard-only operation, and a screen-reader workflow. Automated rules cannot determine whether focus movement makes sense.

4.12 - Internationalization

Design AngularTS message ownership, locale formatting, asynchronous catalogs, and bidirectional layouts.

Use stable message identifiers and named placeholders. Do not concatenate translated fragments or use English text as an identifier. Choose plural forms from locale rules rather than count === 1.

AngularTS formatting filters delegate to Intl. Pass the application locale and explicit currency or time-zone policy; browser defaults may differ from user settings.

<span>{{ total | currency:user.locale }}</span>
<time>{{ dueAt | date:user.locale }}</time>

A locale service should deduplicate catalog requests, cache by locale and catalog version, and expose loading and fallback state. Do not render raw message identifiers while loading.

Set document or feature lang and dir. Prefer logical CSS properties. Test long labels, narrow screens, mixed-direction names, non-Latin input, plural edge cases, currency minor units, and dates around daylight-saving transitions.

Locale switching should invalidate message and formatter readers without rebuilding unrelated domain state.

4.13 - Application security

Apply AngularTS trust boundaries to templates, typed properties, URLs, requests, storage, workers, and service workers.

External sources include URL parameters, forms, storage, server responses, realtime messages, worker messages, and cross-window events. Sensitive sinks include HTML, executable URLs, navigation, credentials, logs, and persisted state.

Prefer interpolation, text children, and typed DOM properties. Use ng-bind-html only for markup that passed application sanitization. A trust override must be local and reviewable; never trust a value merely because it came from your server.

Hidden routes, disabled buttons, and client guards improve UX but do not authorize operations. The server must validate identity, permission, input shape, size, and state transition.

Configure credentials and XSRF behavior for known origins. Reject unexpected redirect and content types at the repository boundary. Store the minimum browser data, validate versioned records on read, and never persist bearer credentials for convenience.

Deploy HTTPS and a specific Content Security Policy. Review worker and service-worker scope, allowed connection origins, update behavior, and rollback. Security failures should remain visible rather than trigger broad policy relaxation.

4.14 - Observability

Correlate AngularTS navigation, requests, workflows, rendering outcomes, and unexpected failures without recording sensitive state.

Assign a stable operation name and correlation ID at the user-action or route boundary. Record start, terminal outcome, duration, release, and route. Do not log every reactive read or scope write; volume hides causality and changes timing.

performance.mark(`task-save:${id}:start`);
try {
  await repository.save(task);
  record({ operation: 'task.save', outcome: 'success', id });
} catch (error) {
  record({ operation: 'task.save', outcome: classify(error), id });
  throw error;
}

Expected domain and transport failures should produce classified outcomes. Unexpected exceptions should reach $exceptionHandler with their cause. Redact credentials, request bodies, user-entered text, and model snapshots.

Carry one correlation ID through route transition, request, retry, and workflow command. Record cancellation separately from failure. For realtime connections, record state transitions and reconnect exhaustion, not every message.

After deployment, verify release tagging, source-map policy, critical routes, error rate, request latency, and one synthetic path. Every alert needs an owner and actionable threshold.

5 - View guides

Choose and learn AngularTS HTML templates or type-safe component views, which share the same module and component lifecycle.

A view is the DOM output of a component or directive. AngularTS offers two authoring styles:

  • HTML templates keep static structure in markup and use directives for behavior.
  • Programmatic views construct DOM in TypeScript and type-check controller-to-view bindings.

Read how to choose. Both styles register on normal modules. A server-rendered module can use HTML while a client-owned dashboard module uses typed programmatic views in the same application.

5.1 - HTML template views

Render a component with readable HTML, interpolation, directives, controller methods, and explicit bindings.

What you will build

Render the task count as a small template component.

Before you start

Read components.

angular.getModule('todoApp').component('todoSummary', {
  bindings: { remaining: '<' },
  template: `
    <p>
      <strong>{{ $ctrl.remaining }}</strong>
      tasks remaining
    </p>
  `,
});
<todo-summary remaining="remaining()"></todo-summary>

The binding is the component’s public input. $ctrl is the component controller in its template. Interpolation converts the value to displayed text and keeps that text current.

Templates are a strong default for mostly static HTML, designer-edited markup, and teams that want structure visible without reading JavaScript.

Next step

Learn template expressions or compare typed views.

5.2 - Type-safe component views

Construct component DOM in TypeScript with typed tags, properties, events, controller access, and reactive reader functions.

What you will build

Render a task form whose controller properties and event handlers are checked by TypeScript.

Before you start

Install the npm package, enable TypeScript, and read components.

import { angular, button, form, input } from '@angular-wave/angular.ts';

class TodoForm {
  draft = '';
  add() {}
}

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

app.component('todoForm', {
  controller: TodoForm,
  view: ({ controller }) =>
    form(
      {
        onsubmit: (event) => {
          event.preventDefault();
          controller.add();
        },
      },
      input({
        value: () => controller.draft,
        oninput: (event) => {
          controller.draft = (event.currentTarget as HTMLInputElement).value;
        },
      }),
      button({ disabled: () => !controller.draft.trim() }, 'Add'),
    ),
});

A function in a child or property position is a reactive reader. AngularTS runs it, records the controller values it reads, and updates that DOM position when those values change. A plain value is a snapshot.

Typed views are useful when compile-time model-to-view checks, dynamic element construction, native DOM properties, or direct browser API integration matter.

Next step

Learn collections and cleanup.

5.3 - Reactive collections and cleanup

Render keyed reactive collections, preserve DOM identity, and release resources with the owning view.

What you will build

Render the changing task collection and close an external resource when its component is removed.

Before you start

Complete typed views.

import { each, li, ul } from '@angular-wave/angular.ts';

view: ({ controller, onDestroy }) => {
  const channel = new BroadcastChannel('todos');
  onDestroy(() => channel.close());

  return ul(
    each(
      () => controller.todos,
      (todo) => todo.id,
      (todo) => li(() => todo().title),
    ),
  );
};

Normal array.map() creates a one-time DOM snapshot. each() observes the collection, preserves nodes by a stable unique key, moves existing nodes when order changes, and disposes nodes for removed keys. Its renderer receives a reader because an item object can be replaced while retaining the same key.

Bindings and framework listeners are cleaned up automatically. Register sockets, channels, timers, observers, and other application-owned resources with onDestroy().

Next step

Use the complete typed component view reference.

5.4 - Choose a view style

Decide between HTML templates and typed component views using team, type-safety, DOM, and maintenance requirements.

What you will decide

First decide who owns the model and rendering. The server should own both by default. View style follows that decision.

Before you start

Read the short template and typed-view examples.

Prefer HTML templates whenPrefer typed programmatic views when
The server owns the model and renderingThe browser owns the application model
The page or route is server-renderedThe browser owns model consistency and compilation
AngularTS adds a small enhancementA complex screen is primarily TypeScript
Server-rendered markup is reusedChecked model-to-view bindings are required
Normal HTML remains the clearest representationNative properties and browser APIs dominate

Both choices use module.component(), controller injection, reactive state, transclusion, routing, and lifecycle cleanup. They can interoperate, but avoid splitting one complex client-owned model across TypeScript and loosely typed HTML bindings.

Start with server-rendered HTML, not a client template that recreates the same page. Keep using HTML for server-owned pages, route fragments, and focused enhancements. If the browser must take ownership of a complex model, a typed programmatic view can add compile-time checks between the model and DOM. It is optional; HTML templates remain supported. Make REST contracts, cache consistency, and browser security policy primary at the same ownership threshold; simple HTTP remains preferable before it.

Choose at the module boundary. A server-rendered login module can use HTML while a dashboard module in the same application uses a client-owned model and its preferred view style. Do not convert one merely to make it match the other.

Next step

Read Keep the application on the server, then follow the selected authoring guide.

6 - Directive guides

Learn AngularTS directives by the job they perform before using the individual directive reference pages.

Directives are easier to learn by role than by name. Start here when you want to understand how AngularTS extends HTML, then use the Directives reference for individual attributes and API details.

Learning path

  1. Overview: how directives are discovered, compiled, linked, and prioritized.
  2. Data Binding: directives that synchronize scope data with text, HTML, classes, styles, and form controls.
  3. Structural Directives: directives that add, remove, repeat, switch, include, or protect DOM sections.
  4. Forms: validation, model options, form state, and messages.
  5. HTTP Directives: declarative requests and Server-Sent Events from HTML.
  6. Animation Directives: CSS hooks and animation-specific attributes.
  7. Advanced Directives: workers, WebAssembly, channels, DOM references, accessibility, and event helpers.

Reference lookup

When you already know the directive name, use the per-directive reference. The reference keeps pages small and specific, while this section keeps the conceptual grouping that helps new users build a mental model.

6.1 - Advanced directives: workers, WASM, channels, viewport

AngularTS advanced directives for Web Workers, WebAssembly, lazy loading, event channels, DOM references, and accessibility — all declarative in HTML.

AngularTS ships several advanced directives that expose modern browser APIs — Web Workers, WebAssembly, Intersection Observer, and custom event channels — directly in HTML without requiring JavaScript. These directives are registered as part of the core ng module and are available in every AngularTS application.

Controller and scope directives

ng-controller

Attaches a controller function to a section of the DOM, creating a new child scope populated by the controller.

<div ng-controller="TodoController as ctrl">
  <p>{{ ctrl.title }}</p>
</div>
  .controller('TodoController', ['$scope', function($scope) {
    $scope.title = 'My Todos';
  }]);

ng-scope

Assigns a stable name to the current scope so debugging tools and integration code can find it later. The name is required. ng-scope does not create a child or isolate scope.

<div ng-controller="UserController" ng-scope="userPanel">
  <p>Hello {{ user.name }}</p>
</div>

ng-init

Initializes scope variables inline in the template. Best suited for simple one-off values or prototyping.

<div ng-init="name = 'World'; count = 0">
  Hello {{ name }}, count is {{ count }}
</div>

Tip: Prefer controllers or services over ng-init for any real application logic. ng-init is ideal for zero-JS demos and quick prototypes.

DOM reference directives

ng-el

Use ng-el when you only need the native DOM element. It is the simple DOM reference API and is usually the clearest choice for canvas, focus management, layout measurement, and pointer-driven UI.

<section ng-controller="BoardController as $ctrl">
  <canvas ng-el="$ctrl.boardEl"></canvas>
</section>
function BoardController() {
  this.boardEl = null;
}

ng-el="$ctrl.boardEl" stores the element on the controller. Use this form with controller-as syntax.

For simple scope shorthand, pass a bare name:

<canvas ng-el="boardEl"></canvas>

If the value is omitted, AngularTS uses the element id.

<canvas id="board" ng-el></canvas>

ng-ref

Use ng-ref when you need a component or directive controller reference, or when you need to assign the reference to a full expression such as $ctrl.child.

<search-box ng-ref="$ctrl.search"></search-box>
<button ng-click="$ctrl.search.clear()">Clear</button>

By default, ng-ref reads the component or element-directive controller when one exists, and falls back to the DOM element.

ng-ref-read is not a standalone directive. It only changes what ng-ref assigns.

<canvas ng-ref="$ctrl.boardEl" ng-ref-read="$element"></canvas>
<my-widget ng-ref="$ctrl.resizeHandle" ng-ref-read="resizeHandle"></my-widget>

Use ng-ref-read="$element" only when you specifically need the DOM element through an assignable expression. For the common DOM-only case, prefer ng-el.

function BoardController() {
  this.boardEl = null;
}

Event and communication directives

ng-channel

Subscribes to a named event channel (backed by $eventBus). When an event is published on the channel, the expression is evaluated.

<div>Latest: {{ lastNotification }}</div>

ng-listener

Attaches a DOM event listener to the element using the native addEventListener API — without the overhead of Angular’s event directive wiring.

<div>Scroll-tracked content</div>

ng-inject

Injects a named service directly into the scope, making it accessible in the template without a controller.

<div ng-inject="$http">
  <!-- $http is now available in this scope -->
</div>

Lazy loading and performance

ng-viewport

Uses the browser’s Intersection Observer API to run expressions when an element enters or leaves the viewport. Ideal for below-the-fold loading, lazy request triggers, analytics markers, and canvas/chart initialization.

<div
  ng-viewport
  on-enter="loadComments($entry)"
  on-leave="visible = false"
  data-viewport-threshold="0.5"
  data-viewport-margin="200px 0px"
>
  <div ng-repeat="comment in comments">{{ comment.text }}</div>
</div>

Use data-viewport-once when the enter expression should run only once:

<div ng-viewport data-viewport-once on-enter="loadOnce($entry)"></div>

ng-viewport

  • Type: marker attribute

Supported attributes:

AttributeDescription
on-enterExpression evaluated when the element intersects the viewport.
on-leaveExpression evaluated when the element stops intersecting.
data-viewport-thresholdIntersection threshold, such as 0.5 or 0, 0.5, 1.
data-viewport-marginRoot margin passed to IntersectionObserver, such as 200px 0px.
data-viewport-onceDisconnects the observer after the first enter callback.

on-enter and on-leave receive $entry and $entries locals from the Intersection Observer callback.

ng-cloak

Prevents the browser from briefly displaying uncompiled template syntax ({{ }}) before AngularTS bootstraps. Add to the root element or any element with interpolation.

<div ng-cloak>
  <p>Hello {{ user.name }}</p>
</div>

Note: Always include the CSS rule [ng-cloak] { display: none; } in your stylesheet when using ng-cloak.

Web Workers and WebAssembly

ng-worker

Runs a JavaScript file in a Web Worker and binds the result back to scope. The worker communicates via postMessage.

<div
  ng-worker="/workers/sort.js"
  data-params="{ items: largeList }"
  on-result="sortedItems = $result"
>
  <div ng-repeat="item in sortedItems">{{ item.name }}</div>
</div>

ng-worker

  • Type: string
  • Required: yes

Path to the Web Worker JavaScript file.

Use an empty ng-worker with data-handle="name" to reuse a worker registered through app.worker(name, script). Shared handles are application-owned rather than terminated with the directive scope.

data-params

  • Type: expression

Data to pass to the worker as the message payload.

on-result

  • Type: expression

Expression evaluated when the worker posts a result. $result contains the worker’s response.

Worker results are not inserted as HTML when this expression is absent.

data-request

  • Type: boolean data attribute

Use the standard correlated request protocol instead of consuming every stream message. This is the preferred mode for shared workers.

interval

  • Type: number

Re-send the params to the worker on a millisecond interval.

throttle

  • Type: number

Throttle worker invocations to at most once per N milliseconds.

The worker file uses standard onmessage / postMessage:

  var sorted = e.data.items.slice().sort((a, b) => a.name.localeCompare(b.name));
  postMessage(sorted);
};

ng-wasm

Loads a WebAssembly module and makes its WasmResource available on scope.

<div ng-wasm src="/wasm/math.wasm" as="math"></div>
<p>
  Result: {{ math.status === 'ready' ? math.exports.add(3, 4) : 'Loading...' }}
</p>

ng-wasm

  • Type: string
  • Required: yes

Path to the .wasm file to load.

as

  • Type: string

Scope property name to assign the WasmResource to. Defaults to wasm. The reserved object keys __proto__, constructor, and prototype are rejected.

Transclusion

ng-transclude

Used inside custom directive templates to mark where transcluded content should be inserted.

  return {
    transclude: true,
    template: '<div class="panel"><ng-transclude></ng-transclude></div>'
  };
});
<my-panel>
  <p>This content is transcluded into the panel.</p>
</my-panel>

Non-bindable sections

ng-non-bindable

Prevents AngularTS from compiling or interpolating a subtree. Use this for displaying raw Angular syntax as documentation or code examples.

<pre ng-non-bindable>
  {{ this will not be interpolated }}
  ng-repeat="item in items"
</pre>

Aria directives

AngularTS automatically manages ARIA attributes for accessibility. These directives are applied alongside their functional counterparts:

DirectiveARIA attribute managed
ng-disabledaria-disabled
ng-checkedaria-checked
ng-readonlyaria-readonly
ng-requiredaria-required
ng-show / ng-hidearia-hidden
ng-modelaria-value*, role

The $aria service and module config let you configure which ARIA attributes are automatically managed:

angular.createModule('app', []).config({
  $aria: {
    ariaDisabled: true,
    ariaChecked: true,
    ariaReadonly: true,
    ariaRequired: true,
    ariaHidden: true,
    ariaValue: true,
    tabindex: true,
  },
});

Event directives

AngularTS generates event directives for all common DOM events. They all follow the ng-<eventname>="expression" pattern:

DirectiveDOM event
ng-clickclick
ng-dblclickdblclick
ng-focusfocus
ng-blurblur
ng-keydownkeydown
ng-keyupkeyup
ng-mouseentermouseenter
ng-mouseleavemouseleave
ng-mousemovemousemove
ng-mouseovermouseover
ng-mousedownmousedown
ng-mouseupmouseup
ng-submitsubmit (on forms)
ng-cutcut
ng-copycopy
ng-pastepaste
ng-loadload

The expression is evaluated in the current scope. $event refers to the native DOM event object:

<input ng-keydown="$event.key === 'Enter' && submit()" />

Event policy can be declared once on the same element and applies to generated event directives on that element:

<div
  ng-click="$ctrl.activate($event)"
  data-event-prevent
  data-event-capture
></div>

Supported policy attributes:

AttributeBehavior
data-event-preventCalls $event.preventDefault() first
data-event-stopCalls $event.stopPropagation() first
data-event-captureRegisters the listener with capture: true
data-event-onceRegisters the listener with once: true
data-event-passiveRegisters the listener with passive: true

data-event-passive cannot be combined with data-event-prevent, because passive listeners cannot cancel default browser behavior.

ng-pointer-capture can be combined with an application directive that installs native pointer listeners for game and board interactions:

<div ng-pointer-capture pointer-drag></div>

6.2 - Animation Directives and CSS Hooks

Use CSS class hooks and JavaScript animation factories with structural directives such as ng-if, ng-repeat, and ng-show.

AngularTS integrates animations into the same lifecycle that drives structural directives. Add the animate attribute to an element and the $animate service applies entry, exit, and class-change hooks at the appropriate DOM update.

How $animate integrates with directives

Structural directives such as ng-if, ng-repeat, ng-show, ng-hide, ng-switch, and ng-include check for animation data before manipulating the DOM. When animate is present, they delegate DOM operations to $animate:

OperationWithout $animateWith $animate
Insert elementelement.after(clone)$animate.enter(clone, parent, after)
Remove elementelement.remove()$animate.leave(element)
Toggle classelement.classList.add(cls)$animate.addClass(element, cls)
Swap classesel.add(a); el.remove(b)$animate.setClass(element, add, remove)

This means you can add CSS transitions to any structural operation simply by adding the animate attribute and writing the corresponding CSS rules.


CSS animation hook classes

When $animate performs an operation it applies a sequence of CSS classes in two frames to give the browser time to set up the transition:

Element enter

ng-enter  →  ng-enter + ng-enter-active  →  (classes removed)

Element leave

ng-leave  →  ng-leave + ng-leave-active  →  element removed

Element move (ng-repeat reorder)

ng-move  →  ng-move + ng-move-active  →  (classes removed)

Class add/remove

ng-CLASS-add  →  ng-CLASS-add + ng-CLASS-add-active  →  CLASS applied
ng-CLASS-remove  →  ng-CLASS-remove + ng-CLASS-remove-active  →  CLASS removed
.card.ng-enter {
  opacity: 0;
  transform: translateY(-8px);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease;
}
.card.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

/* Fade out when the element is removed */
.card.ng-leave {
  opacity: 1;
  transition: opacity 0.2s ease;
}
.card.ng-leave-active {
  opacity: 0;
}

Animating ng-repeat lists

ng-repeat triggers ng-enter and ng-leave animations for items added to or removed from the collection. Add the animate attribute on the repeated element to opt in.

<ul>
  <li class="task-item" ng-repeat="task in tasks" animate>
    <span ng-bind="task.title"></span>
    <button ng-click="removeTask(task)">Remove</button>
  </li>
</ul>
  animation: slideIn 0.25s ease forwards;
}

.task-item.ng-leave {
  animation: slideOut 0.2s ease forwards;
}

.task-item.ng-move {
  transition: all 0.3s ease;
}

@keyframes slideIn {
  from { opacity: 0; transform: translateX(-16px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes slideOut {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(16px); }
}
  .controller('TaskCtrl', ['$scope', function($scope) {
    $scope.tasks = [
      { id: 1, title: 'Design wireframes' },
      { id: 2, title: 'Write tests' },
      { id: 3, title: 'Deploy to staging' }
    ];

    $scope.removeTask = function(task) {
      const idx = $scope.tasks.indexOf(task);
      if (idx !== -1) $scope.tasks.splice(idx, 1);
    };
  }]);

Animating ng-show / ng-hide

ng-show and ng-hide both apply a temporary ng-hide-animate class alongside ng-hide when the $animate service is active. This class serves as the transition anchor:

<div animate>{{ message }}</div>
  transition: opacity 0.3s ease, max-height 0.3s ease;
  overflow: hidden;
}

/* State when hidden */
.notification.ng-hide {
  opacity: 0;
  max-height: 0;
}

/* Applied during the animation frame to enable the transition */
.notification.ng-hide-animate {
  display: block !important;
}

JavaScript animations

Register JavaScript animation hooks with the module’s animation() method:

angular.getModule('app').animation('.flip-card', function () {
  return {
    enter: function (element, done) {
      // Use Web Animations API or any library
      element.animate(
        [{ transform: 'rotateY(90deg)' }, { transform: 'rotateY(0deg)' }],
        { duration: 300, easing: 'ease-out' },
      ).onfinish = done;

      return function (cancelled) {
        if (cancelled) element.style.transform = '';
      };
    },
    leave: function (element, done) {
      element.animate(
        [{ transform: 'rotateY(0deg)' }, { transform: 'rotateY(-90deg)' }],
        { duration: 250, easing: 'ease-in' },
      ).onfinish = done;
    },
  };
});
<div ng-repeat="card in cards" animate>{{ card.content }}</div>

Tip: JavaScript animations and CSS animations can coexist on the same element. The $animate service runs CSS animations and JavaScript animation hooks in parallel, calling done only after both complete.


Animation and the HTTP directives

The HTTP directives (ng-get, ng-post, etc.) also support the animate attribute. When present, swapped content uses $animate.enter and $animate.leave instead of direct DOM manipulation:

<div
  ng-get="/api/posts"
  trigger="load"
  swap="innerHTML"
  animate
  class="post-content"
></div>
  opacity: 0;
  transition: opacity 0.4s ease;
}
.post-content.ng-enter-active {
  opacity: 1;
}

6.3 - Data Binding Directives in AngularTS Explained

Explore ng-bind, ng-bind-html, ng-model, ng-class, ng-style, and ng-init with real examples covering one-way and two-way data binding.

Data binding directives synchronise values between the scope and the DOM without requiring you to write event listeners or DOM queries by hand. AngularTS provides one-way and two-way binding variants to cover every use case, from safe text output to full form synchronisation.

ng-bind

ng-bind is the attribute equivalent of double-curly interpolation ({{ }}). It watches the given scope expression and writes the result as the element’s textContent. Because it sets textContent, it is immune to XSS — no HTML is ever parsed.

<p>Hello, {{ user.name }}!</p>

<!-- Using ng-bind — element is empty until AngularTS compiles -->
<p>Hello, <span ng-bind="user.name"></span>!</p>

The implementation observes the expression’s reactive dependencies and updates the text when one of them changes.

Tip: Prefer ng-bind over {{ }} in the <body> of server-rendered pages to avoid the flash of uncompiled template (FOUC). The element starts empty and is filled when Angular bootstraps.

Parameters

ng-bind

  • Type: expression
  • Required: yes

Any AngularTS expression. The result is converted to a string via stringify() and written to element.textContent. null and undefined render as an empty string.


ng-bind-template

ng-bind-template lets you embed multiple interpolated expressions inside a single attribute value — useful when the surrounding text cannot hold a child element.

<title ng-bind-template="{{pageTitle}} — {{siteName}}"></title>

<!-- Equivalent using child elements (not possible inside <title>) -->

The directive reacts to interpolation dependencies and writes the resulting string to textContent.

ng-bind-template

  • Type: string with {{ }} expressions
  • Required: yes

A string literal containing one or more {{ expression }} placeholders. The entire interpolated string is written to textContent.


ng-bind-html

ng-bind-html inserts the expression result directly into element.innerHTML. Use this directive when you need to render server-provided or pre-sanitised markup. It calls $parse during compilation to check for interpolation errors, then watches the expression and sets innerHTML on every change.

<div ng-bind-html="article.body"></div>
  .controller('ArticleCtrl', ['$scope', function($scope) {
    $scope.article = {
      body: '<p>Hello <strong>world</strong></p>'
    };
  }]);

Warning: ng-bind-html sets innerHTML directly. Always sanitise content on the server before placing it in scope. Avoid interpolating raw user input with this directive.

ng-bind-html

  • Type: expression
  • Required: yes

An expression evaluating to a string of HTML. The string is written to element.innerHTML. null and undefined clear the element.


ng-model

ng-model establishes a two-way binding between a form input and a scope property. Changes to the input update the scope; changes to the scope property update the input. It is the backbone of AngularTS form handling.

Text input

<input type="text" ng-model="user.name" />
<p>Hello, {{ user.name }}!</p>

Checkbox

<input type="checkbox" ng-model="settings.notifications" />
<p ng-bind="settings.notifications ? 'On' : 'Off'"></p>

Select

<select ng-model="selected.country">
  <option value="us">United States</option>
  <option value="gb">United Kingdom</option>
</select>
<p>You selected: {{ selected.country }}</p>

Textarea

<textarea ng-model="message.body" rows="4"></textarea>
<p>Characters: {{ message.body.length }}</p>

ng-model creates an NgModelController instance that manages:

  • viewValue — the formatted value shown in the control (always a string for native inputs)
  • modelValue — the parsed value stored in the scope
  • parsers — pipeline from view to model (view → model transformation)
  • formatters — pipeline from model to view (model → view transformation)
  • validators — synchronous validator functions keyed by error name
  • asyncValidators — async validator functions that return Promises
  • error — object whose keys are failing validator names

State properties

PropertyTypeDescription
pristinebooleantrue if the user has not interacted with this control
dirtybooleantrue after the user has changed the value
touchedbooleantrue after the control has lost focus
untouchedbooleantrue before the control has ever been blurred
validbooleantrue if all validators pass
invalidbooleantrue if any validator fails

CSS classes

AngularTS automatically toggles CSS classes on the element:

.ng-valid {
  border-color: green;
}

/* Applied when the model is invalid */
.ng-invalid {
  border-color: red;
}

/* Applied when the user has not yet changed the value */
.ng-pristine {
}

/* Applied once the user has changed the value */
.ng-dirty {
}

/* Applied once the input has been blurred */
.ng-touched {
}

ng-model

  • Type: expression
  • Required: yes

An assignable AngularTS expression. The expression must be assignable (pointing to a scope property), otherwise AngularTS throws an ngModel:nonassign error.


ng-class

ng-class conditionally adds and removes CSS classes based on a scope expression. It supports three input formats: an object, an array, or a string.

Object syntax

Keys are class names; values are expressions. A class is applied when its value is truthy.

<div ng-class="{ active: isActive, 'text-danger': hasError }">Status panel</div>
$scope.isActive = true;
$scope.hasError = false;
// Result: class="active"

For larger views, prefer projecting domain state into a view-model class map and binding that map directly. This keeps templates readable and lets JavaScript type checking validate the object shape.

<button ng-class="tile.classes">Fire</button>
/**
 * @param {{ state: string, sunk: boolean }} tile
 * @returns {ng.ClassMap}
 */
function tileClasses(tile) {
  return {
    placed: tile.state === 'unit',
    hit: tile.state === 'hit',
    miss: tile.state === 'miss',
    sunk: tile.sunk,
  };
}

$scope.tile = {
  state: 'unit',
  sunk: false,
  classes: tileClasses({ state: 'unit', sunk: false }),
};

Array syntax

Each element is evaluated as a class name or object. Useful when combining static and conditional classes.

<div ng-class="[baseClass, { highlighted: isSelected }]">Item</div>
$scope.baseClass = 'card';
$scope.isSelected = true;
// Result: class="card highlighted"

String syntax

The expression evaluates to a space-separated class string.

<div ng-class="currentTheme">Themed content</div>
$scope.currentTheme = 'theme-dark compact';
// Result: class="theme-dark compact"

The implementation uses reference counting internally (digestClassCounts) so that when multiple ng-class directives compete on the same element, classes are not prematurely removed while still in use.

Alternating repeated rows

Use the repeater’s $even and $odd locals in a normal ng-class map.

<ul>
  <li
    ng-repeat="item in items"
    ng-class="{ 'row-even': $even, 'row-odd': $odd }"
  >
    {{ item.name }}
  </li>
</ul>

ng-class

  • String — a space-delimited list of class names.
  • Object — keys are class names, values are truthy/falsy conditions.
  • Array — each element is recursively processed using the above rules.

ng-style

ng-style watches a scope object and applies its key-value pairs as inline CSS properties using element.style.setProperty. When the expression changes, the old properties are first removed before the new ones are applied.

<div ng-style="boxStyles">Dynamic box</div>
  'background-color': '#4f46e5',
  'color': '#ffffff',
  'padding': '16px',
  'border-radius': '8px'
};
<p
  ng-style="{ 'font-size': fontSize + 'px', 'font-weight': isBold ? 'bold' : 'normal' }"
>
  Styled text
</p>

Note: CSS property names must use hyphen-case (background-color, not backgroundColor) since the implementation calls element.style.setProperty rather than assigning to element.style[key].

ng-style

  • Type: object
  • Required: yes

An expression evaluating to an object where keys are CSS property names in hyphen-case and values are CSS value strings. Setting the expression to null or undefined removes all previously applied inline styles.


ng-init

ng-init evaluates an expression during the pre-link phase — before child directives are linked. It is most useful for initialising scope variables inline without requiring a separate controller.

<div ng-init="count = 0; title = 'Hello'">
  <button ng-click="count = count + 1">Clicked {{ count }} times</button>
</div>
<ul>
  <li ng-repeat="item in items" ng-init="pos = $index + 1">
    {{ pos }}. {{ item.name }}
  </li>
</ul>

The implementation checks whether a controller is present on the element (via getController) and, if so, evaluates the expression on the controller’s scope. Otherwise it falls back to the current scope.

Warning: Use ng-init sparingly. For any non-trivial initialisation, use a controller instead. Keeping logic in templates makes it harder to test and maintain.

ng-init

  • Type: expression
  • Required: yes

An AngularTS expression or semicolon-separated list of expressions evaluated once in the pre-link phase. The expression has access to the current scope.

6.4 - Form Directives and Validation in AngularTS

Complete guide to AngularTS form directives: ng-model, built-in validators, ng-messages, ng-model-options, and form state properties like valid and dirty.

AngularTS treats HTML forms as first-class citizens of the framework. Every <form> element (and ng-form directive) creates a FormController instance that tracks the aggregate validity and dirty state of all its inputs. Each input bound with ng-model gets its own NgModelController that manages the view-to-model pipeline, validation, and CSS class hooks.

The form directive

A plain <form> element is enhanced by AngularTS into a FormController. Give the form a name attribute and the controller is published on the current scope under that name, giving you programmatic access to validation state.

<form name="registrationForm">
  <input type="email" name="email" ng-model="user.email" required />

  <button type="submit" ng-disabled="registrationForm.invalid">Register</button>
</form>
  .controller('RegistrationCtrl', ['$scope', function($scope) {
    $scope.register = function() {
      if ($scope.registrationForm.valid) {
        // submit logic
      }
    };
  }]);

FormController state properties

PropertyTypeDescription
validbooleantrue when all child controls are valid
invalidbooleantrue when any child control is invalid
pristinebooleantrue before any control has been changed
dirtybooleantrue after any control has been changed
submittedbooleantrue after the form’s submit event fires
errorobjectKeys are validator names; values are arrays of failing controls
pendingobjectKeys are validator names with pending async validators

CSS classes on forms

AngularTS automatically toggles these classes on both the <form> element and each ng-model input:

ClassApplied when
ng-pristineControl has not been changed
ng-dirtyControl has been changed
ng-validAll validators pass
ng-invalidAny validator fails
ng-submittedForm has been submitted
ng-touchedInput has been blurred at least once
ng-untouchedInput has never been blurred
ng-pendingAn async validator is in progress
input.ng-invalid.ng-dirty {
  border: 2px solid #ef4444;
}

input.ng-valid.ng-dirty {
  border: 2px solid #22c55e;
}

/* Show error messages only after the form is submitted or field is touched */
.field-error {
  display: none;
}
input.ng-touched.ng-invalid ~ .field-error {
  display: block;
}

ng-form

ng-form creates a nested FormController that tracks its own controls independently. Because browsers do not allow nested <form> elements, ng-form is the standard way to group controls into sub-sections with their own validity state.

<form name="outerForm">
  <!-- Shipping address group -->
  <ng-form name="shippingForm">
    <input name="street" ng-model="shipping.street" required />
    <input name="city" ng-model="shipping.city" required />
    <p ng-if="shippingForm.invalid">Please complete shipping address.</p>
  </ng-form>

  <!-- Billing address group -->
  <ng-form name="billingForm">
    <input name="street" ng-model="billing.street" required />
    <p ng-if="billingForm.invalid">Please complete billing address.</p>
  </ng-form>

  <button ng-disabled="outerForm.invalid">Submit</button>
</form>

The outer form’s valid is false if any nested ng-form is invalid, allowing you to check a single top-level form while still presenting granular errors per section.


Input directives

All standard HTML input types are supported. For <input>, <textarea>, and <select> elements, AngularTS registers type-specific parsers and validators automatically when ng-model is present.

Text / Email / URL

<input
  type="text"
  name="username"
  ng-model="user.username"
  required
  minlength="3"
  maxlength="20"
  ng-pattern="/^[a-z0-9_]+$/"
/>

<input type="email" name="email" ng-model="user.email" required />

<input type="url" name="website" ng-model="user.website" />

Number

<input
  type="number"
  name="age"
  ng-model="user.age"
  min="18"
  max="120"
  required
/>

Parses the input into a JavaScript number. Sets error.number if the value is not numeric, error.min or error.max if out of range.

Checkbox

<!-- Simple boolean checkbox -->
<input type="checkbox" name="agree" ng-model="user.agreedToTerms" required />

Checkbox models use boolean values.

Radio

<label> <input type="radio" ng-model="plan" value="free" /> Free </label>
<label> <input type="radio" ng-model="plan" value="pro" /> Pro </label>
<label>
  <input type="radio" ng-model="plan" value="enterprise" /> Enterprise
</label>
<p>Selected: {{ plan }}</p>

Select

<!-- Simple string options -->
<select name="country" ng-model="user.country" required>
  <option value="">-- Choose a country --</option>
  <option value="us">United States</option>
  <option value="gb">United Kingdom</option>
</select>

<!-- Object options with ng-options -->
<select
  ng-model="selectedRole"
  ng-options="role.id as role.label for role in roles"
>
  <option value="">-- Select a role --</option>
</select>

Built-in validators

required / ng-required

required makes the field mandatory. ng-required accepts an expression so you can make a field conditionally required.

<!-- Conditionally required -->
<input
  type="text"
  ng-model="user.company"
  ng-required="user.accountType === 'business'"
/>

The validator calls NgModelController.isEmpty on the view value. For text inputs, empty means "". For checkboxes, empty means false.

minlength / ng-minlength

ng-model="user.password" ng-minlength="8" />
<p ng-if="form.password.error.minlength">
  Password must be at least 8 characters.
</p>

maxlength / ng-maxlength

ng-model="tweet.text" ng-maxlength="280" />
<p>{{ 280 - tweet.text.length }} characters remaining</p>

pattern / ng-pattern

pattern accepts a regex literal (as an attribute value subject to interpolation). ng-pattern accepts a scope expression that evaluates to a RegExp object or a string.

<input type="text" ng-model="user.phone" pattern="^\d{10}$" />

<!-- Dynamic pattern from scope -->
<input
  type="text"
  ng-model="user.code"
  ng-pattern="validationRules.codePattern"
/>
  codePattern: /^[A-Z]{2}\d{4}$/
};

Note: Avoid the g (global) flag on patterns used with ng-pattern. RegExp objects with the g flag maintain internal state between calls to .test(), which causes alternating pass/fail results.


ng-messages

ng-messages simplifies displaying validation error messages. It watches a form control’s error object and renders the first matching ng-message child (or all matching, with the multiple attribute).

<form name="signupForm">
  <div class="field">
    <label>Email</label>
    <input type="email" name="email" ng-model="user.email" required />

    <div ng-messages="signupForm.email.error" role="alert">
      <p ng-message="required">Email address is required.</p>
      <p ng-message="email">Please enter a valid email address.</p>
    </div>
  </div>
</form>

By default, only the first matching error is shown. Add the multiple attribute to show all failing validators at once:

<div ng-messages="signupForm.password.error">
  <p ng-message="required">Password is required.</p>
  <p ng-message="minlength">At least 8 characters required.</p>
  <p ng-message="pattern">Must contain a number and special character.</p>
</div>

ng-message-default

ng-message-default acts as a fallback shown when none of the specific ng-message keys match but there is at least one truthy error:

<div ng-messages="field.error">
  <p ng-message="pattern">Invalid format.</p>
  <p ng-message-default>This field has an error.</p>
</div>

Reusable message templates

Use ng-messages-include to load a shared message template file:

<div ng-messages="signupForm.email.error">
  <div ng-messages-include="'/partials/common-messages.html'"></div>
  <p ng-message="email">Not a valid email address.</p>
</div>

ng-model-options

ng-model-options controls when the model is updated and how updates are debounced. It can be placed on a form (to apply to all controls) or on individual inputs.

Debounce

Delay model updates by a given number of milliseconds after the user stops typing:

<input
  type="text"
  ng-model="search.query"
  ng-model-options="{ debounce: 300 }"
/>

This fires a search 300ms after the last keystroke rather than on every character.

Update on blur

Only update the model when the input loses focus:

<input
  type="text"
  ng-model="user.username"
  ng-model-options="{ updateOn: 'blur' }"
/>

Combined events

Update immediately on blur, but also debounced on default (which covers all other events):

<input
  type="text"
  ng-model="user.slug"
  ng-model-options="{
         updateOn: 'blur default',
         debounce: { 'default': 500, 'blur': 0 }
       }"
/>

Available options

updateOn

  • Type: string

Space-separated DOM event names that trigger model updates. Use "default" as a placeholder for the control’s standard update event (e.g., "input" for text inputs).

debounce

  • Type: number | object

Milliseconds to wait before committing the view value. Can be an object mapping event names to delays: { 'blur': 0, 'default': 500 }.

allowInvalid

  • Type: boolean

When true, the model is updated even when the value is invalid (normally the model is set to undefined when validation fails).

timezone

  • Type: string

Timezone offset for date/time input parsing, e.g. "UTC" or "+0530".


Complete form example

The following example demonstrates a registration form with multiple input types, validators, debouncing, and ng-messages error display.

<form name="regForm" ng-submit="register()">
  <input name="email" type="email" ng-model="form.email" required />
  <div ng-messages="regForm.email.error" ng-if="regForm.email.touched">
    <p ng-message="required">Email is required.</p>
    <p ng-message="email">Enter a valid email.</p>
  </div>
  <button ng-disabled="regForm.invalid">Create account</button>
</form>
  .controller('RegistrationCtrl', ['$scope', function($scope) {
    $scope.form = {};

    $scope.submitRegistration = function() {
      $scope.regForm.setSubmitted();

      if ($scope.regForm.valid) {
        // Call your API
        console.log('Registering:', $scope.form);
      }
    };
  }]);

6.5 - HTTP Directives: ng-get, ng-post, ng-put, ng-sse

Make HTTP requests and stream SSE from HTML using ng-get, ng-post, ng-put, ng-delete, and ng-sse — no JavaScript required for common data-fetching patterns.

AngularTS ships a family of declarative HTTP directives. Rather than wiring up $http calls in a controller, you attach ng-get, ng-post, ng-put, ng-delete, or ng-sse directly to HTML elements. The directives handle the request lifecycle, DOM insertion, loading states, error handling, and explicit response assignment — all configured through HTML attributes.

Similarity to HTMX

Developers familiar with HTMX may recognize a similar declarative, server-first approach: requests can start from HTML, the server can return fragments, and an element can declare where a response belongs. That similarity is a useful mental model, not a compatibility or migration contract.

Learn each AngularTS pattern from its AngularTS contract. AngularTS combines HTTP directives with scope state, bindings, components, and router states. For example, an HTTP directive can prefetch JSON into scope while ng-click and ng-if decide when to reveal it.

Do not infer AngularTS syntax or behavior from HTMX documentation. AngularTS does not automatically implement HTMX attributes, extensions, events, request headers, or history behavior. Use the attributes documented on this page and AngularTS services when a pattern needs capabilities beyond them.

How they differ from $http

When you use $http directly you write controller code: define the request, subscribe to the promise, update scope properties, handle errors, and trigger a digest. The HTTP directives do all of this declaratively:

<button ng-click="loadUser()">Load user</button>
<p>{{ user.name }}</p>
  $http.get('/api/user/1').then(function(res) {
    $scope.user = res.data;
  });
};
<button ng-get="/api/user/1" on-success="user = $res">Load user</button>
<p>{{ user.name }}</p>

JSON is available to on-success as $res; it never mutates scope implicitly. HTML responses are compiled and inserted using the configured swap strategy.


ng-get

ng-get fires a GET request when the configured event occurs (default: click for buttons, change for inputs, submit for forms).

<button ng-get="/api/weather?city=London" on-success="weather = $res">
  Get weather
</button>
{{ weather.temperature }}°C — {{ weather.description }}
<button
  ng-get="/partials/user-card.html"
  data-swap="outerHTML"
  data-target="#user-area"
>
  Show profile
</button>
<div id="user-area"></div>

Automatic trigger

Use trigger="load" to fire the request immediately when the element is linked (no user interaction required):

<div ng-get="/api/dashboard/stats" trigger="load" on-success="stats = $res">
  <span ng-bind="stats.users"></span> users
</div>

Polling with interval

<div
  ng-get="/api/live-count"
  trigger="load"
  on-success="status = $res"
  interval="5000"
>
  {{ count }} online
</div>

ng-post

ng-post fires a POST request. On <form> elements, the default trigger is submit and the form data is collected via FormData. On buttons and other elements, the trigger defaults to click.

<form ng-post="/api/contact" name="contactForm">
  <input name="email" type="email" ng-model="contact.email" required />
  <textarea name="message" ng-model="contact.message" required></textarea>
  <button type="submit" ng-disabled="contactForm.invalid">Send</button>
</form>
<button
  ng-post="/api/cart/add"
  on-success="cartMessage = $res.message"
  on-error="cartError = $res.error"
>
  Add to cart
</button>

When the form element has an enctype attribute, the request body is URL-encoded using Content-Type: application/x-www-form-urlencoded. Without enctype, form data is sent as a JSON object.


ng-put

ng-put fires a PUT request. Use it to update resources:

<form
  ng-put="/api/profile"
  name="profileForm"
  on-success="profileSaved = true"
  data-state-success="dashboard"
>
  <input name="name" ng-model="user.name" required />
  <input name="email" type="email" ng-model="user.email" required />
  <button type="submit">Save changes</button>
</form>

ng-delete

ng-delete fires a DELETE request. It shares all the same modifier attributes as ng-get:

<li>
  <span>{{ item.name }}</span>
  <button
    ng-delete="/api/items/{{ item.id }}"
    data-swap="delete"
    data-target="#item-{{ item.id }}"
  >
    Delete
  </button>
</li>

ng-sse

ng-sse opens a persistent Server-Sent Events connection using the $sse service. Incoming JSON is available to on-success as $res; HTML strings are inserted using the configured swap strategy.

<div ng-sse="/api/events/notifications" swap="beforeend"></div>
<div ng-sse="/api/events/market" trigger="load" on-success="market = $res">
  <p>{{ market.price | currency }}</p>
  <p>{{ market.change }}%</p>
</div>

The connection is torn down automatically when the scope is destroyed (e.g., when navigating away). Reconnect behaviour and error logging are provided by the $sse service.

app.get('/api/events/market', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');

  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ price: 42.5, change: 0.3 })}\n\n`);
  }, 2000);

  req.on('close', () => clearInterval(interval));
});

Shared modifier attributes

All HTTP directives accept the same set of modifier attributes to control their behaviour:

swap

  • Type: string
  • Default: innerHTML

Controls how the response HTML is inserted. Possible values:

  • innerHTML — replaces the element’s inner content (default)
  • outerHTML — replaces the entire element
  • textContent — inserts plain text without HTML parsing
  • beforebegin — inserts immediately before the element
  • afterbegin — inserts inside the element, before its first child
  • beforeend — inserts inside the element, after its last child
  • afterend — inserts immediately after the element
  • delete — removes the target element entirely
  • none — performs no DOM insertion

data-target

  • Type: CSS selector

A querySelector selector for the element that receives the response. When omitted, the directive element itself is the target.

<button ng-get="/api/user" data-target="#profile-card">Load</button>
<div id="profile-card"></div>

trigger

  • Type: DOM event name

The DOM event that fires the request. Defaults to click for buttons and generic elements, change for inputs/selects/textareas, and submit for forms. Use "load" to trigger immediately on link.

latch

  • Type: interpolated expression

Re-fires the request every time the interpolated value changes.

<div ng-get="/api/search" latch="{{ query }}" swap="innerHTML">
  {{ results.length }} results
</div>

interval

  • Type: number

Fires the request immediately and then repeats every N milliseconds. The interval is cleared when the scope is destroyed.

<div ng-get="/api/status" interval="10000">{{ status }}</div>

delay

  • Type: number

Wait N milliseconds before sending the request after the trigger event fires.

throttle

  • Type: number

Ignore later trigger events for N milliseconds after the request fires.

loading

  • Type: none

When present, sets data-loading="true" on the element while the request is in flight and data-loading="false" when it completes. Useful as a CSS hook.

<button ng-get="/api/data" loading>Load</button>
button[data-loading='true'] {
  opacity: 0.6;
  cursor: wait;
}

loading-class

  • Type: string

A CSS class toggled on the element while the request is in flight.

<button ng-get="/api/data" loading-class="is-loading">Load</button>

on-success

  • Type: expression

Expression evaluated when the response has a 2xx status code. The response data is available as $res.

<button ng-get="/api/items" on-success="items = $res">Fetch</button>

on-error

  • Type: expression

Expression evaluated when the response has a 4xx or 5xx status code. The response data is available as $res.

<button
  ng-post="/api/login"
  on-success="redirect('/dashboard')"
  on-error="loginError = $res.message"
>
  Log in
</button>

data-state-success

  • Type: string

Router state name to navigate to on success (calls $state.go).

data-state-error

  • Type: string

Router state name to navigate to on error (calls $state.go).

animate

  • Type: none

When present, enables $animate transitions for the swap operation. Requires the $animate CSS hooks to be defined.

enctype

  • Type: string

Sets the Content-Type request header and URL-encodes form data. Use "application/x-www-form-urlencoded" to replicate a native HTML form submission.


Practical examples

Loading a data table on mount

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Status</th>
      <th>Joined</th>
    </tr>
  </thead>
  <tbody ng-get="/api/users" trigger="load" swap="innerHTML">
    <!-- Populated with server-rendered rows -->
  </tbody>
</table>

Infinite scroll

<ul id="post-list">
  <li ng-repeat="post in posts">{{ post.title }}</li>
</ul>

<button
  ng-get="/api/posts?page={{ nextPage }}"
  ng-viewport
  on-enter="loadMore()"
  data-swap="beforeend"
  data-target="#post-list"
>
  Load more
</button>

Real-time notifications via SSE

<div class="notification-badge">
  <span ng-bind="notifications.unread"></span>
</div>

<div
  ng-sse="/api/events/notifications"
  trigger="load"
  on-success="notifications = $res"
></div>

Form submission with redirect on success

<form
  name="loginForm"
  ng-post="/api/auth/login"
  data-state-success="app.dashboard"
  on-error="authError = $res.message"
>
  <input type="email" name="email" ng-model="credentials.email" required />
  <input
    type="password"
    name="password"
    ng-model="credentials.password"
    required
  />
  <p ng-if="authError" ng-bind="authError"></p>
  <button type="submit" ng-disabled="loginForm.invalid">Log in</button>
</form>

6.6 - Directive Overview

Learn how AngularTS directives attach behavior to HTML, how built-in directive groups are organized, and how to create custom directives.

Directives are how AngularTS attaches behavior and structure to HTML. A directive is a marker on a DOM element or attribute that tells $compile to attach behavior, transform DOM, wire events, create scopes, or connect controllers.

Every built-in directive in AngularTS is applied as an HTML attribute using the ng- prefix. The compiler normalizes prefixes, so ng-bind, data-ng-bind, and x-ng-bind all match the same directive.

Exact custom directive contracts live in TypeDoc:

How Directives Are Matched

The restrict option controls where a directive can appear.

RestrictMatch formExample
AAttribute<span ng-bind="name"></span>
EElement<my-widget></my-widget>
AE / EAAttribute or element<div ng-form> or <ng-form>

Attribute directives are the common case in AngularTS. They keep the host element in place and augment it.

Built-In Directive Groups

AngularTS groups its built-in directives by the user-facing job they perform. Keep these groups in mind when choosing where to look in the docs.

GroupPurposeExamples
Data bindingSynchronize scope data with the viewng-bind, ng-model, ng-class, ng-style
StructuralAdd, remove, repeat, or switch DOMng-if, ng-repeat, ng-show, ng-hide, ng-switch
FormsTrack validity, dirty state, messages, and model optionsform, ng-model, ng-messages, validators
HTTPTrigger declarative network work from the DOMng-get, ng-post, ng-put, ng-delete, ng-sse
AnimationsCoordinate directive-driven animation hooksanimate, $animate, CSS hooks
AdvancedConnect browser APIs and integrationsng-worker, ng-wasm, ng-viewport, ng-pointer-capture

Create A Custom Directive

Register directives on a module with .directive(name, factory). The factory returns a directive definition object.

angular.createModule('demo', []).directive('highlight', () => {
  return {
    restrict: 'A',
    scope: {
      color: '@highlight',
    },
    link(scope, element) {
      const paint = (color) => {
        element.style.backgroundColor = color || 'yellow';
      };

      paint(scope.color);

      scope.watch('color', paint);
    },
  };
});
<p highlight="gold">Pinned note</p>

Use compile() when the directive must transform template DOM before linking. Use link() when it only needs to attach behavior to each compiled instance.

Execution Order

When multiple directives appear on the same element, AngularTS sorts them by priority from highest to lowest. Directives with equal priority run in registration order.

<li ng-repeat="item in items" ng-class="{ active: item.selected }">
  {{ item.name }}
</li>

Structural directives such as ng-repeat and ng-if use high priorities and terminal behavior because they replace or remove DOM before lower-priority directives are linked.

Scope Choice

Use the default inherited scope for simple behavior. Create a child scope when the directive needs local state. Use isolate scope bindings when the directive is designed as a reusable widget with explicit inputs and callbacks.

scope: {
  title: "@",
  value: "=",
  onSave: "&",
}

Prefer components for larger reusable UI pieces. Use directives for behaviors, DOM integrations, structural transforms, and lightweight element coordination.

Next Steps

6.7 - Structural Directives: ng-if, ng-repeat, ng-switch

Master AngularTS structural directives including ng-if, ng-show, ng-hide, ng-repeat, ng-switch, ng-include, and ng-non-bindable.

Structural directives reshape the DOM by adding, removing, or repeating elements. Unlike attribute directives that modify an existing element, structural directives use transclusion to stamp out or remove entire sections of the template. They run at high priority (600–1000) and set terminal: true so that lower-priority directives do not compile until the structure is resolved.

ng-if

ng-if conditionally includes or removes a DOM element based on a scope expression. When the expression becomes falsy, the element and its entire subtree — including child scopes and event listeners — are destroyed. When it becomes truthy again, a fresh element and child scope are created.

<div ng-if="user.role === 'admin'">
  <h2>Admin Panel</h2>
  <p>Welcome, {{ user.name }}. You have full access.</p>
</div>
<div ng-if="isLoggedIn">
  <p>Welcome back, {{ user.name }}!</p>
  <button ng-click="logout()">Log out</button>
</div>
<div ng-if="!isLoggedIn">
  <p>Please <a href="/login">sign in</a> to continue.</p>
</div>

The implementation uses transclude: "element" at priority 600, which means the compiler replaces the element with a comment placeholder and hands the element template to the transclusion function. When the expression turns truthy, the transclusion function clones the template and inserts it after the comment. When it turns falsy, the child scope is destroyed and the clone is removed.

If the $animate service is available on the element, ng-if delegates to $animate.enter and $animate.leave to trigger CSS transition hooks.

Note: ng-if vs ng-show: ng-if completely removes the element from the DOM (destroying the child scope), while ng-show keeps the element in the DOM and toggles an ng-hide CSS class. Use ng-if when the hidden content is expensive to keep around or when it should not run watchers while invisible.

Parameters

ng-if

  • Type: expression
  • Required: yes

An expression evaluated on each digest. When truthy, the element is inserted into the DOM in a new child scope. When falsy, the element and its child scope are destroyed.


ng-show / ng-hide

ng-show and ng-hide toggle element visibility without removing the element from the DOM. They work by adding or removing the ng-hide CSS class, which sets display: none.

ng-show

Removes ng-hide when the expression is truthy, showing the element.

<div ng-show="form.submitted">
  <p>Thank you! Your form was submitted successfully.</p>
</div>

ng-hide

Adds ng-hide when the expression is truthy, hiding the element.

<div ng-hide="isLoading">
  <p>Content is ready.</p>
</div>
<div ng-show="isLoading">
  <p>Loading...</p>
</div>

Both directives integrate with $animate. When the animate service detects the element has animation hooks, they call $animate.addClass or $animate.removeClass with the temporary class ng-hide-animate for the duration of the animation.

.my-panel.ng-hide {
  opacity: 0;
}
.my-panel.ng-hide-animate {
  transition: opacity 0.3s ease;
}

ng-show

  • Type: expression
  • Required: yes

When truthy, ng-hide class is removed from the element, making it visible.

ng-hide

  • Type: expression
  • Required: yes

When truthy, ng-hide class is added to the element, hiding it.


ng-repeat

ng-repeat iterates over a collection (array or object) and stamps a copy of the template for each item. Each copy gets its own child scope populated with the iteration variables below.

Special scope properties

VariableTypeDescription
$indexnumberZero-based index of the current iteration
$firstbooleantrue for the first item
$lastbooleantrue for the last item
$middlebooleantrue when neither first nor last
$evenbooleantrue when $index is even
$oddbooleantrue when $index is odd

Repeating over an array

<ul>
  <li ng-repeat="product in products">
    <strong>{{ product.name }}</strong> — ${{ product.price }}
    <span ng-if="$first">(newest)</span>
    <span ng-if="$last">(oldest)</span>
  </li>
</ul>

Repeating over an object

Use the (key, value) tuple syntax to iterate over an object’s own enumerable properties. Keys starting with $ are excluded automatically.

<dl>
  <div ng-repeat="(key, value) in user">
    <dt>{{ key }}</dt>
    <dd>{{ value }}</dd>
  </div>
</dl>

Alias with as

The as clause aliases the filtered collection to a new scope variable. This is useful when you apply filters to the collection and need the filtered count.

<p>{{ filtered.length }} of {{ products.length }} items</p>
<ul>
  <li ng-repeat="product in products | filter:searchText as filtered">
    {{ product.name }}
  </li>
</ul>

Animation support

Add the animate attribute to enable $animate.enter / $animate.leave on items:

<li ng-repeat="item in items">{{ item.name }}</li>
  opacity: 0;
  transition: opacity 0.3s;
}
.list-item.ng-enter-active {
  opacity: 1;
}
.list-item.ng-leave {
  opacity: 1;
  transition: opacity 0.3s;
}
.list-item.ng-leave-active {
  opacity: 0;
}

ng-repeat

  • Type: string
  • Required: yes

Expression in one of the forms:

  • item in collection
  • (key, value) in object
  • item in collection | filter as alias

ng-switch

ng-switch renders exactly one child block matching the current value of a watched expression. It is the structural equivalent of a JavaScript switch statement.

<div ng-switch="user.status">
  <div ng-switch-when="active">
    <p>Account is active. Welcome, {{ user.name }}!</p>
  </div>
  <div ng-switch-when="suspended">
    <p>Your account has been suspended. Please contact support.</p>
  </div>
  <div ng-switch-when="pending">
    <p>Your account is awaiting verification.</p>
  </div>
  <div ng-switch-default>
    <p>Unknown account status.</p>
  </div>
</div>

ng-switch-when registers its transclusion function under the key !value (to avoid collisions with $-prefixed keys) using an internal NgSwitchController. When the watched expression changes, the currently shown block has its scope destroyed (with an optional $animate.leave) and the matching block is transcluded into place.

Multiple values on one case

The ng-switch-when-separator attribute allows a single ng-switch-when to match multiple values:

<div ng-switch="day">
  <div ng-switch-when="Saturday Sunday" ng-switch-when-separator=" ">
    Weekend!
  </div>
  <div ng-switch-default>Weekday</div>
</div>

ng-switch

  • Type: expression
  • Required: yes

The expression whose value is compared against ng-switch-when values.

ng-switch-when

  • Type: string
  • Required: yes

The literal string value to match against the ng-switch expression.

ng-switch-default

  • Type: none

Rendered when no ng-switch-when matches the current value.


ng-include

ng-include fetches an external HTML template, compiles it in the current scope (or a new child scope), and inserts it into the DOM. It watches the URL expression and replaces the content whenever the URL changes.

<div ng-include="'/partials/user-profile.html'"></div>

<!-- Include a dynamic template based on user role -->
<div ng-include="'/partials/nav-' + user.role + '.html'"></div>

The directive emits three events on the scope:

  • $includeContentRequested — when the template request begins
  • $includeContentLoaded — when the template is compiled and inserted
  • $includeContentError — when the template request fails
<div
  ng-include="templateUrl"
  ng-init="templateUrl = '/partials/home.html'"
  onload="onTemplateLoaded()"
></div>
  console.log('Template inserted successfully');
};

ng-include

  • Type: expression
  • Required: yes

Expression evaluating to the URL of the template to load. The URL is fetched via $templateRequest (which uses the template cache if available).

onload

  • Type: expression

Expression evaluated after the template is compiled and inserted.

autoscroll

  • Type: expression

When truthy, calls $anchorScroll after the template is inserted.


ng-non-bindable

ng-non-bindable tells the AngularTS compiler to skip the element and all its descendants entirely. No interpolation, directives, or watches are set up inside the marked section.

<pre ng-non-bindable>
  &lt;p&gt;Use {{ expression }} to interpolate values.&lt;/p&gt;
  &lt;div ng-bind="myValue"&gt;&lt;/div&gt;
</pre>
<div ng-non-bindable>{% raw %}{{ jinja_template_variable }}{% endraw %}</div>

Note: ng-non-bindable is essential when displaying code documentation, Jinja/Twig snippets, or any content that uses {{ }} but should not be treated as AngularTS expressions.

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

7.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,
        });
      }
    },
  );

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

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

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

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

7.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.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');
  }
}

8 - Decision guides

Short decision guides for common AngularTS architecture choices.

Use these guides when more than one feature can solve the problem:

Each page gives a default and the conditions that justify another choice.

8.1 - Templates or typed views

Choose between declarative HTML templates and type-safe DOM construction.

Default

Use typed views for new TypeScript components when model-to-view type safety matters. Use templates when designers own substantial HTML or server-delivered markup is required.

Choose a typed view when

  • Refactors must check property and event bindings.
  • The view is highly dynamic.
  • You want real DOM construction without parsing HTML.
  • A component and its view evolve together.

Choose a template when

  • Markup is mostly static and visually authored.
  • Existing template tooling is central to the team.
  • Progressive enhancement starts from server HTML.

Both use normal module registration, dependency injection, lifecycle, and change detection.

8.2 - Controller state, models, or services

Put state at the narrowest lifetime that satisfies its consumers.

Default

Keep transient view state on the component controller.

Use a named model when state is reactive, shared by multiple roots or components, and benefits from snapshot, restore, or synchronization behavior.

Use a service when the primary responsibility is an operation or external resource rather than mutable view data.

Ask who owns the state, who may mutate it, how long it lives, and how it resets. Global state is an application-level lifetime commitment, not a shortcut for passing data.

8.3 - HTTP, SSE, WebSocket, or WebTransport

Select a network transport from the communication pattern and fallback requirements.

Default

Use HTTP for request-response work.

  • Use SSE for server-to-client events with straightforward reconnect behavior.
  • Use WebSocket for broadly supported bidirectional messages.
  • Use WebTransport when independent streams or datagrams are required and the support contract permits it.

Decide from direction, ordering, reliability, backpressure, authentication, proxy support, observability, and fallback behavior. Hide transport details behind an application service.

8.4 - CSS or JavaScript animations

Choose the simplest animation mechanism that meets sequencing and control needs.

Default

Use CSS transitions or keyframes for visual state changes. They are easy to cancel and keep view logic small.

Use JavaScript animation hooks when sequencing depends on application state, measurements, asynchronous work, or coordinated entry and removal.

Always respect reduced-motion preferences. Animation should explain change or preserve spatial context; it must not block input or carry required information by itself.

8.5 - Component, directive, or web component

Choose the correct ownership and interoperability boundary.

Default

Use a component for an application-owned UI region with a controller and view.

Use a directive to add behavior to existing DOM when the host and surrounding content remain owned by another view.

Use a web component when the element must cross framework or document boundaries through browser-standard custom-element APIs.

Avoid directives that secretly own large subtrees and components that mutate unrelated DOM. Clear ownership makes cleanup, testing, and change detection predictable.

9 - Cookbook

Recipes for common AngularTS problems

Use the cookbook when you have a concrete problem and need a working pattern. Start with debugging if AngularTS is new to you. Every progressive recipe ends with a task to apply and checks that prove it worked.

Find recipes by area

Start with beginner recipes, then choose forms, HTTP, DOM updates, security, performance, data correctness, navigation, operations, or advanced browser work.

Choose who owns the result

Start from the result your application needs, not from a directive name.

Needed resultStart with
The server already knows the final HTMLSwap server-rendered HTML
One interaction needs JSON and local template stateBuild and submit forms
Several features share records and write operationsRead, write, and prefetch REST data
The browser must own URLs, history, and nested viewsAdd browser-owned routes
The server pushes one-way updatesReceive live server events
The browser and server both send live messagesExchange WebSocket messages
Existing computation blocks rendering after measurementMove measured CPU work to a worker
The measured runtime contains unused featuresBuild a smaller production runtime

Prefer the first applicable row. Do not introduce a client route or shared data service when one server response and one declarative interaction solve the problem.

Start here

  1. Debug a running application
  2. Select an element with ng-el
  3. Pass server data without another request
  4. Update existing DOM from state
  5. Replace DOM from state
  6. Swap server-rendered HTML
  7. Run effects outside the DOM
  8. Handle events
  9. Build and submit forms
  10. Read, write, and prefetch REST data
  11. Authenticate requests
  12. Cache REST reads
  13. Add browser-owned routes

Change server data

Data correctness: handle uniqueness races, submit money safely, send dates with zones, and keep identifiers opaque.

HTTP behavior: revalidate with ETag, serve maintenance responses, validate return redirects, standardize errors, and negotiate page or fragment.

Browser and content security: sanitize rich text, deploy CSP, harden session cookies, support password managers, and redact secrets.

Interaction details: focus the first error, design an empty state, and preserve focus after swaps.

Transaction workflows: respect rate limits, run background jobs, make critical writes idempotent, save multi-step forms, and apply bulk actions.

Account and record workflows: archive and restore, handle permission rejection, keep filters in the URL, and log out safely.

Production form workflows: show server validation, send CSRF proof, confirm deletion, and resolve concurrent edits.

Production response workflows: retry a failed read, paginate on the server, download with the browser, and swap trusted server HTML.

Keep important forms usable with progressive enhancement, and define their HTTP response contracts before wiring the success and failure branches.

ProblemRecipe
Edit one record without leaving the pageEdit a server record in place
Delete one row without rebuilding its listDelete a row after server confirmation
Search without stale responses winningSearch while the user types
Load choices from another fieldLoad one select from another
Show a reversible change immediatelyUpdate immediately and roll back
Upload a file and allow cancellationCancel a file upload
Choose status, content type, and bodyDesign an HTTP response

Load and navigate faster

ProblemRecipe
Data should be ready before clickPrefetch data before the user clicks
More rows should load near the viewportLoad the next page near the viewport
Stable lookup lists repeatCache reference data safely
A private request finds an expired sessionReturn users to login
A dialog needs current server contentLoad a dialog from the server

Add specialized browser behavior

Integrate another framework

Start with the integration decision guide, then use the event bus for transient messages, ng-observe for reflected web-component attributes, or a shared application model for current reactive state.

Diagnose a production problem

Production change management: deliver server flags, record audit events, and roll contracts out compatibly.

When an API must use another origin, follow the cross-origin request recipe before adding browser workarounds.

Make failures traceable with a request ID, then run the shipping check against the complete interaction.

Trace one interaction with the HTTP debugging recipe, then control repeated requests only after measuring them. Before shipping, audit the full server-driven interaction.

ProblemRecipe
A server page needs one interactive widgetAdd behavior without making an SPA
A component changed state but DOM did notMake a component view reactive
Refreshed rows lose focusKeep repeated rows stable
Hidden UI keeps doing workStop hidden UI work
An older request winsIgnore stale request results
A listener survives its UIClean up browser resources
An interaction feels slowFind the slow part
Request states are incompleteRender every request state

9.1 - Debug a running application

Read and change scopes, controllers, and services from browser DevTools

Problem

A page is running, but you do not know which state, controller, or service owns the DOM you are looking at. You want to inspect it before changing code.

Before you start

Run a development build with AngularTS debugging helpers available. Open the page and browser DevTools; no source changes are required for the first inspection.

Recipe

Open the browser’s Elements panel and select the element you want to inspect. The DevTools console exposes that element as $0.

Inspect its scope

Select an element that creates a scope, such as an ng-controller, component, or repeated row. Then run:

const scope = angular.getScope($0);
scope;

angular.getScope() returns the Scope attached to that compiled element. Read the same properties used by its template:

scope.query;
scope.results;

Change a value directly:

scope.query = 'boots';

AngularTS observes writes to the scope proxy and schedules the affected bindings. You do not need to run a digest. Collection changes work the same way:

scope.results.push({ id: 'debug', name: 'Temporary result' });

Find a scope by ID or name

AngularTS gives each scope a numeric id when it creates it:

scope.id; // for example, 7
scope.root.getById(7);

An ID is useful while inspecting the current page, but it can change after a reload or after AngularTS recreates part of the page. Give an important scope a name when you want to find it repeatedly.

Add ng-scope with a non-empty name to the element whose scope you want to find:

<section ng-controller="CartController" ng-scope="cart">
  <p>{{ items.length }} items</p>
</section>

ng-scope="cart" assigns cart to that scope’s scopeName. It does not create another child scope. The name is required. Find it from the console without selecting the element again:

const cart = angular.getScopeByName('cart');

cart.items;
cart.refresh();

angular.getScopeByName() searches the current application’s scope tree.

If the page has several ng-app roots, start with an element inside the intended application and search from that application’s root scope:

const injector = angular.getInjector($0);
const rootScope = injector.get('$rootScope');
const cart = rootScope.searchByName('cart');

Inspect its controller

angular.getController() finds the nearest inherited ng-controller controller:

const controller = angular.getController($0);
controller;
controller.reload();

Pass a directive name when you need a specific controller. For example, select an input with ng-model and run:

const model = angular.getController($0, 'ngModel');
model;

Use this to read controller state and call the same methods as the page.

Inspect available services

angular.getInjector() walks up from the selected element to the nearest bootstrapped application and returns its InjectorService:

const injector = angular.getInjector($0);

const rootScope = injector.get('$rootScope');
const http = injector.get('$http');
const router = injector.get('$state');

Use the injector to answer practical questions: whether a service is registered, which instance the page uses, and what state that instance currently holds.

Choose the right application

A page can contain more than one ng-app. Each application has its own injector, root scope, and service instances. Start from an element inside the application you want to inspect.

<main ng-app="storefront">...</main>
<aside ng-app="admin">...</aside>

Get each injector from its own root element:

const storefrontElement = document.querySelector('[ng-app="storefront"]');
const adminElement = document.querySelector('[ng-app="admin"]');

const storefront = angular.getInjector(storefrontElement);
const admin = angular.getInjector(adminElement);

storefront === admin; // false

When $0 is inside storefront, angular.getInjector($0) returns the storefront injector. When $0 is inside admin, it returns the admin injector. The same rule applies when reading scopes and controllers: select an element inside the application you mean to inspect.

Do not fetch a service from one injector and assume it belongs to the other application. Even when both applications register a service with the same name, they can hold different instances and state.

Verify

  1. Select the element whose text or value you want to change.
  2. Read its scope or controller and find the matching property.
  3. Change that property in the console.
  4. Confirm only the expected part of the DOM updates.
  5. If the page has several ng-app roots, confirm you selected the intended one.
  6. Reload the page and confirm the temporary change disappears.

Failure path

Helpers return state from the application containing the selected element. On pages with several ng-app roots, selecting outside the intended root can produce a different injector or no result.

Apply it now

Select one element whose behavior you do not understand. Find its application, scope, and controller in DevTools. Change one reversible value and identify the exact binding that updates. Keep the scope name only if you expect to inspect this part of the page again.

9.2 - Select an element with ng-el

Let AngularTS place a native DOM element on a scope or controller

Problem

Your controller needs a native element so it can focus an input, draw on a canvas, measure layout, or call another browser API.

Do not search the document with querySelector. Declare the element you need and let AngularTS wire it to your state.

Before you start

Use this inside a compiled scope or controller that owns the element. Keep document-wide elements in a service rather than attaching unrelated DOM to a local scope.

Recipe

Add ng-el with the scope property that should receive the element:

<section ng-controller="StatusController">
  <p ng-el="statusEl">Waiting</p>
</section>

AngularTS assigns the native paragraph element to scope.statusEl. Code using that scope can now change the element directly:

scope.statusEl.textContent = 'Saved';
scope.statusEl.className = 'status status-success';

To try this in DevTools, select the section and run:

const scope = angular.getScope($0);

scope.statusEl.textContent = 'Saved';
scope.statusEl.className = 'status status-success';

There is no selector, element ID, or setup callback to maintain. AngularTS also removes the reference when the element leaves the DOM.

Store the element on a controller

With controller-as syntax, give ng-el the controller property:

<section ng-controller="StatusController as status">
  <p ng-el="status.element">Waiting</p>
</section>

The controller can use this.element:

function StatusController() {
  this.markSaved = () => {
    this.element.textContent = 'Saved';
    this.element.className = 'status status-success';
  };
}

Use the element ID as the property name

If ng-el has no value, AngularTS uses the element’s id:

<input id="searchInput" ng-el />

The element is then available as scope.searchInput.

Failure path

The reference is unavailable before the directive links and is removed with the element. Guard optional elements and do not retain the node in a longer-lived service.

Apply it now

Search your controller code for one querySelector or getElementById call. If the element belongs to that controller’s template, replace the search with ng-el. Verify that the controller no longer knows the page-wide selector and that the reference disappears when the element is removed.

Verify

Remove and recreate the element. Confirm the reference points to the new node and no page-wide selector remains.

9.3 - Pass server data with ng-init or ng-setter

Initialize scope state once or keep it synchronized with server-rendered content

Problem

The server already knows values that the browser needs, such as the current locale, a record ID, or the initial result count. You want AngularTS to use those values without making another request.

Before you start

The server template must safely serialize and HTML-encode the initial value. Decide whether the value is fixed at compilation or can change when another system replaces element content.

Recipe

Render an ng-init expression with the page:

<main
  ng-app="catalog"
  ng-init='page = {"locale":"en","productId":42,"resultCount":8}'
>
  <p>Found {{ page.resultCount }} products.</p>
  <button ng-click="page.resultCount += 1">Add result</button>
</main>

AngularTS evaluates ng-init while it compiles the element. The resulting page object becomes normal reactive scope state.

The HTML and initial JSON arrive in one server response. This avoids a separate REST request, its network round trip, and an extra loading state. Use this when the server already loaded the data while rendering the page.

Inspect it from DevTools by selecting the main element:

const scope = angular.getScope($0);

scope.page.productId;
scope.page.resultCount = 12;

The paragraph updates when resultCount changes.

Keep scope data synchronized with ng-setter

Use ng-setter when the element’s content can change after the initial page render:

<section ng-app="catalog">
  <span ng-setter="availability">In stock</span>
  <p>Current availability: {{ availability }}</p>
</section>

AngularTS initially sets scope.availability to 'In stock'. If server-driven rendering or another integration later changes the span content, AngularTS updates scope.availability again.

ng-setter observes the element for content changes. It assigns the trimmed innerHTML string to the expression named by the directive:

<div ng-setter="serverMessage">Order accepted</div>

The scope receives:

scope.serverMessage === 'Order accepted';

Choose between them

UseBehavior
ng-initEvaluates an AngularTS expression once during compilation. Use it for JSON objects, numbers, booleans, and other initial values.
ng-setterCopies an element’s trimmed HTML into scope as a string and updates it whenever that content changes.

Both can place server-rendered data in the first HTML response and avoid an extra REST request. Use ng-setter when later element updates must also reach the scope.

Render the value on the server

The final HTML sent to the browser should contain the serialized value:

<main ng-init='page = {"locale":"lv","productId":108}'></main>

Use your server framework’s HTML attribute encoder when inserting serialized data. Do not build the attribute by joining untrusted strings.

Serialize the value to JSON on the server, encode it for an HTML attribute, and write it into ng-init. The browser can use the data as soon as AngularTS compiles the page instead of waiting for another endpoint.

Use ng-init for small values already needed by the page. Keep large records in the server-rendered HTML or load them through an application service when the user requests them.

Failure path

Malformed or unsafely encoded JSON can break the containing attribute. Encode on the server and keep large or sensitive records out of HTML attributes.

Apply it now

Find one request the browser makes for data the server already loaded while rendering the page. Move the smallest useful value into ng-init. If the value comes from HTML that another system may replace later, use ng-setter and change the element once in DevTools to confirm the scope follows it.

Verify

Inspect the first document response and confirm the initial value needs no follow-up request. Change an ng-setter element and confirm scope follows it.

9.4 - Update text, classes, styles, and visibility

Let bindings update existing DOM when scope state changes

Problem

State changes, and the page must update without finding elements or assigning DOM properties by hand.

Before you start

Identify the scope or controller property that owns the visible result. User-provided text must remain text unless it has passed an explicit trusted-HTML policy.

Recipe

Bind each part of the element to scope state:

<article ng-controller="ProductController as product">
  <h2 ng-bind="product.name"></h2>

  <p
    ng-class="{ featured: product.featured, soldOut: !product.inStock }"
    ng-style="{ opacity: product.inStock ? 1 : 0.5 }"
  >
    {{ product.price | currency }}
  </p>

  <p ng-show="product.inStock">Available now</p>
  <p ng-hide="product.inStock">Out of stock</p>

  <button ng-click="product.featured = !product.featured">
    Toggle featured
  </button>
</article>

Change the model, not the element:

product.name = 'Mechanical keyboard';
product.inStock = false;

AngularTS updates only the bindings that read those properties.

Choose the binding

TaskBinding
Replace textng-bind or {{ expression }}
Add and remove classesng-class
Change inline stylesng-style
Hide while keeping the DOM aliveng-show or ng-hide

Use ng-bind-html only for trusted HTML. Normal interpolation and ng-bind write text, so user content cannot become markup.

Failure path

Do not switch to ng-bind-html to fix formatting. Treat unexpected markup as a data or formatting problem unless the content has an explicit trust boundary.

Apply it now

Find one place where application code assigns textContent, className, style, or hidden. Name the state that actually controls that result and bind the DOM to it. Change the state from DevTools and confirm that no DOM-writing code remains.

Verify

Change each source property independently. Confirm only its bindings update and user text is rendered as text rather than HTML.

9.5 - Replace part of the page from state

Add, remove, or switch DOM subtrees with structural directives

Problem

A page has mutually exclusive steps or an expensive section that should not exist while it is inactive.

Before you start

Decide whether hidden DOM state must survive. Structural directives destroy branches; visibility directives keep their controls, media, and browser state alive.

Recipe

Use ng-switch when one state selects one subtree:

<section ng-switch="checkout.step">
  <cart-review ng-switch-when="review"></cart-review>
  <payment-form ng-switch-when="payment"></payment-form>
  <order-confirmation ng-switch-when="complete"></order-confirmation>
  <p ng-switch-default>Loading checkout...</p>
</section>

Change the state normally:

checkout.step = 'payment';

AngularTS removes the old branch, destroys its child scope, creates the new branch, and compiles its directives.

Use ng-if for one optional subtree:

<address-editor ng-if="editingAddress"></address-editor>

Use ng-include when the replacement comes from a template URL:

<section ng-include="panelTemplate"></section>

Use ng-show instead when the same DOM and local form or media state must remain alive while hidden.

Failure path

Using ng-if for a frequently toggled form recreates controls and loses local state. Using ng-show for expensive hidden work keeps that work alive.

Apply it now

Choose one conditional panel in your application. Use ng-show if its form, media, or local DOM state must survive hiding. Use ng-if if its work should stop while absent. Use ng-switch if one state chooses among several panels. Write down which lifecycle behavior you need before changing the directive.

Verify

Toggle the state repeatedly. Confirm hidden state is preserved only with ng-show and removed branches release their scopes and browser resources.

9.6 - Swap server-rendered HTML into the page

Select a target and insert, replace, append, or remove server HTML

Problem

An action needs fresh HTML from the server, but it does not justify a client route or a JSON API followed by client-side rendering.

Before you start

The endpoint must return a fragment rather than a complete document. Choose a stable target that belongs to the same application and define what remains when the request fails.

Recipe

Let the server return the final HTML. Select the target with data-target and choose how to place the response with swap:

<button
  ng-get="/account/summary"
  data-target="#account-summary"
  swap="innerHTML"
>
  Refresh summary
</button>

<section id="account-summary">
  <p>Current server-rendered summary</p>
</section>

innerHTML keeps the target and replaces its children. Use outerHTML when the response replaces the target itself:

<button ng-get="/account/card" data-target="#account-card" swap="outerHTML">
  Refresh card
</button>

Append server-rendered rows without rebuilding the list:

<button ng-get="/orders?page=2" data-target="#orders" swap="beforeend">
  Load more
</button>

<ul id="orders"></ul>

Useful swap values are textContent for plain text, beforeend for append, afterbegin for prepend, delete for removal, and none when only on-success should update scope state.

The response HTML is compiled after insertion, so directives in the new fragment join the same application.

Failure path

A failed response must not erase useful content. Return JSON to on-error or a deliberate error fragment, and never insert untrusted HTML.

Apply it now

Find one action that fetches JSON only so the browser can rebuild HTML the server already knows how to render. Return that fragment from the endpoint, choose the smallest stable target, and select innerHTML, outerHTML, or an append swap. Test the endpoint with JavaScript disabled before adding the swap.

Verify

Inspect the response content type and target. Confirm inserted directives compile, failed responses preserve useful UI, and unrelated DOM nodes retain identity.

9.7 - Run an effect when state changes

Use scope watches for work outside AngularTS bindings

Problem

A state change must update something outside AngularTS, such as analytics, storage, a chart, or another browser library.

Before you start

Use an effect only for work outside AngularTS rendering. Identify the scope that owns the external resource and the operation that releases it.

Recipe

Register a watch for the scope expression that drives the external work:

const stopThemeEffect = scope.watch('settings.theme', (theme) => {
  themeStore.save(theme);
  chart.setTheme(theme);
});

A watch listener runs after the write, in a microtask. Several synchronous writes to the same property are coalesced before listeners run:

scope.settings.theme = 'light';
scope.settings.theme = 'dark';

// The effect observes the final value: "dark".

watch returns a deregistration function:

const stop = scope.watch('selectedReport', (report) => {
  reportPreview.open(report);
});

scope.on('$destroy', () => {
  stop();
  reportPreview.destroy();
});

The owning scope removes its watches when it is destroyed. Keep explicit cleanup when the effect also owns an external resource.

Use bindings for DOM work

Do not use an effect to copy scope values into text, classes, styles, or visibility. Bind those values with ng-bind, ng-class, ng-style, or ng-show. Use watch only when the result lives outside AngularTS.

Failure path

Effects can loop when they write the value they watch. Keep the watched input and external output distinct, and make cleanup safe to call once.

Apply it now

Pick one watcher or state-change callback. If it only changes text, classes, styles, or visibility, replace it with a binding. If it writes to storage, analytics, a chart, or another external system, keep it as an effect and name the resource that must be cleaned up when its scope is destroyed.

Verify

Write the watched value several times synchronously, destroy the scope, and confirm one final effect runs and all external resources are released.

9.8 - Handle DOM and scope events

Respond to browser events and communicate through the scope tree

Problem

The page must react to user input, control browser event behavior, or notify related scopes.

Before you start

Start from a native user interaction and decide whether communication is local DOM behavior, a short scope notification, or durable shared state.

Handle a DOM event

Use the directive named after the native event:

<button ng-click="cart.add(product)">Add to cart</button>

<input
  ng-keydown="$event.key === 'Escape' && dialog.close()"
  aria-label="Search"
/>

$event is the native event. Declare event policy on the same element:

<a
  href="/checkout"
  ng-click="openCheckout()"
  data-event-prevent
  data-event-once
>
  Checkout
</a>

data-event-prevent calls preventDefault(). Other policies are data-event-stop, data-event-capture, data-event-once, and data-event-passive.

Send an event through scopes

Register with on and keep the returned cleanup function:

const stop = scope.on('cart:updated', (event, count) => {
  scope.cartCount = count;
});

scope.emit('cart:updated', 3);
scope.broadcast('cart:updated', 3);

emit travels to parent scopes. broadcast travels to child scopes. Scope event listeners run synchronously; reactive DOM updates still flush in the next microtask.

Use DOM events for element interaction. Use scope events when scopes in the same application tree need a short-lived notification. Use a service when unrelated features share durable state.

Failure path

Passive listeners cannot prevent defaults, and broad scope broadcasts can hide ownership. Keep event policy on the element and use services for lasting state.

Apply it now

Pick one manual addEventListener call. Move it to an ng-* event directive when it belongs to a template interaction. Use a scope event only when a parent or child scope needs a short notification. If the value must still exist after the event, put it in a service instead.

Verify

Use keyboard and pointer input, inspect default prevention, and destroy the scope. Confirm one handler runs and no listener survives its UI.

9.9 - Build and validate a form

Bind fields, show useful errors, and submit valid data

Problem

A form needs reactive values, validation messages, submit state, and a server request without replacing native form behavior unnecessarily.

Before you start

The server endpoint must validate independently and return a deliberate success or error format. Give every submitted native control a name.

Recipe

Name the form and every control. Bind values with ng-model and use native validation attributes:

<form
  name="signupForm"
  ng-post="/api/signup"
  on-success="account = $res"
  on-error="submitError = $res.message"
  loading
  loading-class="is-saving"
>
  <label for="email">Email</label>
  <input id="email" name="email" type="email" ng-model="user.email" required />

  <div ng-messages="signupForm.email.error" ng-show="signupForm.email.dirty">
    <p ng-message="required">Enter your email.</p>
    <p ng-message="email">Enter a valid email.</p>
  </div>

  <p ng-if="submitError" ng-bind="submitError"></p>
  <button type="submit" ng-disabled="signupForm.invalid">Create account</button>
</form>

The form name exposes its controller on scope. Each named ng-model control has an NgModelController.

Use dirty or touched to avoid showing errors before the user interacts. Disable submission with invalid, but always validate again on the server.

Submit with ng-post or ng-put

Put ng-post on a form when the request creates something or runs an action. Use ng-put when it replaces or updates a resource. Both directives:

  1. Listen for the form’s submit event.
  2. Prevent the browser from navigating away.
  3. Collect the current form controls.
  4. Send the request through $http.
  5. Run on-success for a successful response or on-error for a failed one.

The first example sends a JSON object to /api/signup. $res is the parsed response body. Assign it explicitly in on-success; the directive does not guess which scope property should receive it.

Use an interpolated URL when updating an existing record:

<form
  name="profileForm"
  ng-put="/api/profiles/{{ profile.id }}"
  on-success="profile = $res"
  on-error="saveError = $res.message"
>
  <input name="displayName" ng-model="profile.displayName" required />
  <button type="submit" ng-disabled="profileForm.invalid">Save</button>
</form>

Choose which controls are sent

The request body comes from the form’s native controls, not by serializing the whole scope object. Give every submitted control a name. Controls without a name, disabled controls, and unchecked checkboxes are omitted. This follows the browser’s FormData rules.

Without enctype, AngularTS converts the collected entries to a JSON object. Use URL encoding only when the server expects a traditional HTML form body:

<form ng-post="/api/contact" enctype="application/x-www-form-urlencoded">
  <input name="email" type="email" ng-model="contact.email" required />
  <textarea name="message" ng-model="contact.message" required></textarea>
  <button type="submit">Send</button>
</form>

Use the programmatic $http upload shown below for multipart/form-data. The browser must create that request from the original FormData and add its boundary; the declarative form directives convert entries to an object.

Handle the request lifecycle

loading sets data-loading="true" on the form while the request is running. loading-class="is-saving" adds and removes that class at the same time. Use either as a CSS hook for progress feedback.

Use on-success and on-error when the server returns JSON. If the server returns HTML, data-target and swap can replace or append server-rendered content. See Swap server-rendered HTML into the page.

Route after a successful form

A login form can submit JSON, show server validation errors, and open a router state without a controller or a JavaScript submit handler:

<form
  ng-post="/_login"
  data-state-success="dashboard"
  on-error="errors = $res"
  ng-init="errors = {}"
>
  <label ng-class="{ error: errors.email }">
    Email
    <input
      name="email"
      type="email"
      aria-invalid="{{ errors.email !== undefined }}"
      ng-keyup="errors.email = undefined"
      required
    />
    <span ng-if="errors.email">{{ errors.email }}</span>
  </label>

  <label ng-class="{ error: errors.password }">
    Password
    <input
      name="password"
      type="password"
      aria-invalid="{{ errors.password !== undefined }}"
      ng-keyup="errors.password = undefined"
      required
    />
    <span ng-if="errors.password">{{ errors.password }}</span>
  </label>

  <button type="submit">Log in</button>
</form>

The inputs do not need ng-model. Their name and current native values are enough for ng-post to produce this request body:

POST /_login
Content-Type: application/json

{"email":"user@example.com","password":"secret"}

For invalid credentials, return a 4xx status and an application/json body whose keys match the form fields:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{"email":"Unknown account","password":"Password does not match"}

AngularTS parses the JSON and exposes it as $res. The inline on-error="errors = $res" expression makes each message available to the template. Editing a field clears only that field’s message.

For a 2xx response, AngularTS ignores the error expression and calls $state.go('dashboard') through data-state-success. The dashboard state must already be registered with the application’s router. The successful response may be empty when the next state loads everything it needs.

ng-get and ng-delete can listen to a form submission, but they do not send the form controls as a request body. Put their parameters in the URL, or use them on a button. Use ng-post or ng-put when the form fields must be sent.

For search fields, debounce model updates instead of building a timer:

<input
  type="search"
  ng-model="filters.query"
  ng-model-options="{ debounce: 250 }"
/>

Upload a file

A file input writes its native FileList to ng-model. Send the selected File in native FormData; do not set Content-Type because the browser must add the multipart boundary.

The fetch transport supports cancellation through a promise-valued timeout, but it does not currently expose upload progress. Use Cancel a file upload and handle server rejection for the complete controller, cancellation, abort, retry, and server-validation pattern.

Failure path

Native and AngularTS validation improve feedback but never replace server validation. Preserve entered values on 4xx responses and prevent duplicate destructive submissions.

Apply it now

Take the smallest real form in your application. Give each submitted control a name, add ng-post or ng-put, and make the server return one realistic 4xx JSON error body. Confirm the page renders those errors without a controller and that a 2xx response performs the intended update or router transition.

Verify

Submit valid, invalid, slow, cancelled, and server-rejected requests. Confirm named controls form the body, errors remain editable, and success performs exactly one update.

9.10 - Read and write REST resources

Define a typed endpoint once and use it for CRUD operations

Problem

Several screens call the same endpoint and repeat URL construction, response mapping, and CRUD request code.

Before you start

Register $rest and $http in the application that owns the data. Define stable resource IDs and the server methods supported by the endpoint.

Recipe

Create one typed resource with $rest:

interface Task {
  id: number;
  title: string;
  complete: boolean;
}

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

  private tasks: ng.RestService<Task, number>;

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

  list() {
    return this.tasks.list();
  }

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

RestService provides list(), get(), create(), update(), and delete(). Writes stay close to HTTP:

const created = await tasks.create({
  title: 'Write release notes',
  complete: false,
});
if (!created) throw new Error('Task response was empty');

await tasks.update(created.id, {
  ...created,
  complete: true,
});

await tasks.delete(created.id);

create() sends POST, update() sends PUT, and delete() sends DELETE.

Prefetch data before the user clicks

Start a JSON request when the pointer enters a preview, but keep the result hidden until the user asks for it:

<div
  ng-get="/api/products/42/preview"
  data-trigger="mouseenter"
  data-throttle="60000"
  on-success="preview = $res"
>
  <button type="button" ng-click="showPreview = true">Show preview</button>

  <div
    ng-if="showPreview && !preview"
    ng-get="/api/products/42/preview"
    data-trigger="load"
    on-success="preview = $res"
  >
    Loading preview...
  </div>

  <section ng-if="showPreview && preview">
    <h3>{{ preview.name }}</h3>
    <p>{{ preview.summary }}</p>
  </section>
</div>

mouseenter starts the first request without revealing anything. AngularTS parses the JSON response and on-success stores it on scope. When the user clicks, ng-click changes only showPreview; if preview is ready, ng-if renders it immediately without another request.

The inner ng-get is the fallback for keyboard, touch, and fast clicks. It is created only when the preview is requested but no prefetched value is ready, and data-trigger="load" starts the request immediately. A click made while the hover request is still in flight can briefly create a second GET request. Use a shared cached service when request deduplication matters.

Prefer mouseenter to mouseover for prefetching. mouseover bubbles when the pointer crosses children and can issue repeated requests. The throttle limits later pointer-triggered refreshes, but it is not a durable cache. Errors reject normally, so the caller decides how to render retry and error states.

Failure path

Treat empty create responses, conflicts, cancellation, and stale replies explicitly. Do not assume every successful write returns an entity body.

Apply it now

Identify data users often need immediately after hovering, focusing, or opening a panel. Measure the normal click-to-content delay, add intent prefetching, and measure it again. Decide what touch and keyboard users should trigger, how long the result may stay fresh, and whether a duplicate in-flight GET is acceptable.

Verify

Inspect request methods and bodies for list, create, update, and delete. Repeat the prefetched interaction with mouse, keyboard, touch emulation, and a delayed response.

9.11 - Authenticate requests

Configure cookie or bearer credentials once for framework services

Problem

Requests need authentication, but controllers should not copy tokens or credential flags into every call.

Before you start

The server must authenticate and authorize every request. Choose cookie or bearer transport, define session expiry, and register any router states used after login.

Recipe

Prefer a server-managed, HttpOnly cookie when the application and API can share a cookie session:

const session = { currentUser: null };

angular
  .createModule('app', [])
  .value('session', session)
  .config({
    $security: {
      fallback: 'deny',
      isAuthenticated: () => session.currentUser !== null,
      credentials: { cookie: true },
    },
  })
  .controller('LoginController', [
    'session',
    function LoginController(session) {
      this.credentials = {};
      this.signedIn = (user) => {
        session.currentUser = user;
      };
    },
  ]);

$http and $rest consult $security before a request and enable browser credentials when policy allows it. Login remains a normal server request:

<form
  name="loginForm"
  ng-controller="LoginController as auth"
  ng-post="/api/login"
  on-success="auth.signedIn($res.user)"
  on-error="auth.error = $res.message"
>
  <input name="email" type="email" ng-model="auth.credentials.email" required />
  <input
    name="password"
    type="password"
    ng-model="auth.credentials.password"
    required
  />
  <button type="submit" ng-disabled="loginForm.invalid">Log in</button>
  <p ng-if="auth.error" ng-bind="auth.error"></p>
</form>

For an API that requires bearer tokens, configure the token source once:

credentials: {
  bearer: () => sessionStorage.getItem('accessToken'),
  order: ['bearer'],
}

Credential transport and login state are separate. credentials controls what may be sent; isAuthenticated reports whether the application currently has a signed-in user. The server remains responsible for authentication and authorization.

Failure path

Client authentication state is not authorization. Clear local session state after expiry, avoid token leakage, and let the server reject forbidden operations.

Apply it now

Write down four facts for your login flow: where credentials are submitted, which cookie or token proves later requests, how the client learns that login succeeded, and which router state opens next. Then test invalid credentials, an expired session, and a direct visit to a protected URL rather than testing only the successful form.

Verify

Test login, logout, expiry, direct private navigation, and a forbidden action. Confirm credentials use the intended transport and server authorization remains decisive.

9.12 - Cache REST reads

Add cache-first, network-first, or stale-while-revalidate behavior

Problem

Reference data is requested repeatedly even though it changes infrequently.

Before you start

Provide a RestCacheStore implementation and classify the response as public, user-specific, mutable, or immutable before selecting a policy.

Recipe

Start with a cache store. This in-memory version lasts until the page reloads:

class MemoryRestCache {
  entries = new Map();

  async get(key) {
    return this.entries.get(key);
  }
  async set(key, value) {
    this.entries.set(key, value);
  }
  async delete(key) {
    this.entries.delete(key);
  }
  async deletePrefix(prefix) {
    for (const key of this.entries.keys()) {
      if (key.startsWith(prefix)) this.entries.delete(key);
    }
  }
}

const appCache = new MemoryRestCache();

The object implements RestCacheStore. Replace it with IndexedDB or another persistent store when cached data must survive a page reload. Then wrap the HTTP REST backend:

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

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

const products = $rest<Product, number>('/api/catalog/products', Product, {
  backend,
});

CachedRestBackend accepts any RestCacheStore. Use memory for one-page reuse, IndexedDB for durable structured data, or the Cache API for HTTP-style storage.

Choose the strategy by what stale data means:

StrategyUse it when
cache-firstData is effectively immutable and startup speed matters most.
network-firstFresh data matters, but cached data is a useful offline fallback.
stale-while-revalidateShowing cached data now is better than waiting for a refresh.

Writes always go to the network first. Successful writes invalidate cached collection and entity keys for that resource.

Failure path

The wrong policy can expose another user’s data or preserve an invalid value. Partition private caches and invalidate after every relevant write.

Apply it now

Open the Network panel and find a GET that repeats during one normal task. Pick a policy from the consequence of stale data: use cache-first for an immutable country list, network-first for active orders, or stale-while-revalidate for a product catalog. Perform a write and confirm the affected collection and entity entries are invalidated. Never share user-specific cached responses between signed-in users.

Verify

Compare network requests before and after caching, then perform a write and an offline read. Confirm invalidation and stale behavior match the selected policy.

9.13 - Add application routes

Register states, render them in ng-view, and protect private pages

Problem

A client-owned area needs URLs, back-button support, nested views, and route data. A normal server page is no longer enough for that area.

Before you start

Use client routing only inside an area the browser must own. The server must still serve the initial application URL and authentication endpoints.

Recipe

Register states on the module:

const accountRoutes = [
  {
    name: 'account',
    url: '/account',
    component: 'accountHome',
  },
  {
    name: 'account.orders',
    url: '/orders',
    component: 'orderList',
    policy: {
      navigation: { authenticated: true, redirectTo: 'login' },
    },
  },
  {
    name: 'account.order',
    url: '/orders/:id',
    component: 'orderDetail',
    policy: {
      navigation: { authenticated: true, redirectTo: 'login' },
    },
  },
] as const satisfies readonly ng.RouterModuleDeclaration[];

angular.createModule('account', []).router(accountRoutes);

Render the active state and create links:

<nav>
  <a ng-state="'account'">Account</a>
  <a ng-state="'account.orders'">Orders</a>
</nav>

<main ng-view></main>

Use $state.go() when code initiates navigation:

$state.go('account.orders');
$state.go('account.order', { id: order.id });

StateService also provides href() for URL generation and matches() for active-state checks.

Keep the first page server-rendered. Add a route only when the browser must own navigation and state across several related views.

Failure path

A route that works only after visiting another state is incomplete. Resolve direct URLs, rejected policy, missing data, and superseded navigation.

Apply it now

List the URLs in one browser-owned area. Keep pages on the server unless users need client-side back-button history and state across those URLs. Register the smallest useful parent and child states, open a child URL directly, refresh it, and use Back. If any step depends on first visiting another state, the route is not complete.

Verify

Open every documented URL directly, refresh it, move through child states, and use Back and Forward. Confirm private states enforce policy before rendering.

9.14 - Load content when it enters the viewport

Delay below-the-fold work with ng-viewport

Problem

A chart, comment list, or request should not start until the user scrolls near it.

Before you start

Measure the initial request or CPU work first. The deferred feature must have a visible loading or empty state when it has not started.

Recipe

Use ng-viewport and run the enter expression once:

<section
  ng-viewport
  data-viewport-once
  data-viewport-margin="200px 0px"
  on-enter="comments.load()"
>
  <p ng-if="comments.loading">Loading comments...</p>
  <comment-list ng-if="comments.ready"></comment-list>
</section>

The margin starts work 200 pixels before the section enters the viewport. data-viewport-once stops later enter events after the first one.

Use on-leave when work should pause after leaving the viewport:

<video ng-viewport on-enter="player.play()" on-leave="player.pause()"></video>

The directive owns its browser observer and disconnects it when the scope is destroyed.

Failure path

Intersection is only a scheduling hint. Keep keyboard access and an explicit retry path, and disconnect observers when their scope is destroyed.

Apply it now

Record the requests and long tasks made before first paint. Choose one below-the-fold feature that is not needed yet, move its start expression to on-enter, and set the margin from measured load time rather than guessing. Scroll past it twice and confirm data-viewport-once prevents duplicate work.

Verify

Scroll into and out of the element twice, then remove it. Confirm the margin, once behavior, leave behavior, and observer cleanup match the recipe.

9.15 - Receive live server events

Update scope or append HTML through Server-Sent Events

Problem

The server must push notifications or status updates to the page without client polling.

Before you start

The server must support text/event-stream, flush events, and handle reconnects. Decide what the page shows before the first event arrives.

The browser side uses SseService through ng-sse. AngularTS owns the SseConnection, reconnects it, and closes it when the scope is destroyed.

For a custom production runtime, include sseModule from @angular-wave/angular.ts/runtime/realtime. Do not include the aggregate realtimeModule unless the same runtime also uses WebSocket and WebTransport.

Set up a JavaScript SSE endpoint

An SSE endpoint is a long-running HTTP GET. Send the event-stream headers, end every event with a blank line, flush each update, and stop work when the request closes.

import { createServer } from 'node:http';

const states = ['paid', 'packing', 'shipped'];

createServer((request, response) => {
  if (request.url !== '/api/events/orders') {
    response.writeHead(404).end();
    return;
  }

  response.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache, no-transform',
    Connection: 'keep-alive',
  });
  response.flushHeaders();

  let index = 0;
  const send = () => {
    const order = { id: 'A-1042', state: states[index++ % states.length] };
    response.write(`data: ${JSON.stringify(order)}\n\n`);
  };
  const updates = setInterval(send, 15_000);
  const heartbeat = setInterval(
    () => response.write(': keep-alive\n\n'),
    10_000,
  );

  send();
  request.on('close', () => {
    clearInterval(updates);
    clearInterval(heartbeat);
  });
}).listen(8080);

The blank line after data: completes one event. Lines beginning with : are comments that keep idle proxies from closing the connection. In production, publish real order changes rather than using timers.

Authenticate and authorize before writing the 200 response. Browser EventSource cannot attach arbitrary headers, so prefer an HttpOnly, Secure, SameSite session cookie and with-credentials="true". Do not put a long-lived bearer token in the URL.

Send complete current-state snapshots when reconnecting is enough. If every event must be delivered, add an id: field, store events durably, read the browser’s Last-Event-ID header on reconnect, and replay events after that ID before sending live ones. A process-local counter is not sufficient when more than one server can handle the request.

Recipe

Use ng-sse and assign incoming JSON from $res:

<section
  ng-sse="/api/events/orders"
  trigger="load"
  with-credentials="true"
  on-success="orderStatus = $res; streamState = 'live'"
  on-error="streamState = 'reconnecting'"
  on-reconnect="streamState = 'live'"
>
  <p aria-live="polite">Order {{ orderStatus.id }}: {{ orderStatus.state }}</p>
  <p ng-if="streamState === 'reconnecting'">Reconnecting...</p>
</section>

When the server sends HTML fragments, choose a swap strategy:

<ul
  id="notifications"
  ng-sse="/api/events/notifications"
  trigger="load"
  swap="beforeend"
></ul>

AngularTS parses JSON for on-success and otherwise applies the configured DOM swap. Keep each message small. Send an identifier and changed fields, or a small current-state snapshot, instead of an entire page model.

Use SSE for one-way server updates. Use a WebSocket or WebTransport service when the browser and server both need a long-lived message channel.

Failure path

SSE is one-way and connections can disappear through proxies, deployments, or sleep. Show stale or disconnected state and make repeated events safe. Confirm that your reverse proxy disables response buffering and permits connections to remain open. Keep polling when your hosting path cannot stream responses.

Apply it now

Find one polling loop that only asks whether the server has new data. Write the smallest event payload the server can push, replace the poll with SSE, and show what the user sees before the first event and after the connection fails. Keep polling when intermediaries cannot maintain long-lived HTTP connections.

Verify

Send valid JSON, HTML, malformed data, and a disconnect. Confirm the page shows its initial, live, error, and reconnect states without polling. Reconnect with a Last-Event-ID, test the page through the production proxy, and confirm the server releases timers and subscriptions when the browser disconnects.

9.16 - Move CPU-heavy work off the main thread

Run JavaScript in a Web Worker with ng-worker

Problem

Sorting, compression, or data processing blocks input and rendering on the main browser thread.

Before you start

Profile a realistic input and define a serializable message contract. Browser APIs and DOM nodes cannot be passed to the worker as ordinary values.

Recipe

Point ng-worker at a worker file and bind its result:

<button
  ng-worker="/workers/sort.js"
  data-params="{ items: products }"
  on-result="sortedProducts = $result"
  on-error="sortError = error"
  trigger="click"
>
  Sort products
</button>

<ul>
  <li ng-repeat="product in sortedProducts">{{ product.name }}</li>
</ul>

The worker receives data-params through postMessage:

self.onmessage = (event) => {
  const sorted = event.data.items
    .slice()
    .sort((left, right) => left.name.localeCompare(right.name));

  self.postMessage(sorted);
};

AngularTS assigns the reply to $result and updates the scope expression in on-result. The directive terminates its worker when its scope is destroyed.

Use data-request with a registered shared worker when several callers need correlated request and response messages.

Failure path

Worker startup and cloning can cost more than the task. Ignore stale replies, terminate owned workers, and keep errors visible in the owning UI.

Apply it now

Profile the interaction before adding a worker. Move work only when a measured CPU task blocks the main thread long enough to affect input or rendering. Define a small serializable request and response, test the largest realistic input, and decide how a newer request cancels or ignores an older result.

Verify

Compare the main-thread trace before and after moving the task. Confirm input stays responsive, stale replies are ignored, and the worker terminates with its scope.

9.17 - Load a WebAssembly module

Expose WebAssembly exports to scope with ng-wasm

Problem

A calculation already exists as WebAssembly and the page needs to call its exports without writing module-loading lifecycle code.

Before you start

Have a deployed WebAssembly module and know its exported function contract. The server must send the correct MIME type.

Recipe

Load the module with ng-wasm and name its scope resource with as:

<div ng-wasm src="/wasm/shipping.wasm" as="shipping"></div>

<p ng-if="shipping.status === 'loading'">Loading shipping calculator...</p>
<p ng-if="shipping.status === 'error'">Shipping estimate is unavailable.</p>
<p ng-if="shipping.status === 'ready'">
  Shipping: {{ shipping.exports.quote(cart.weightGrams, destination.zone) }}
  cents
</p>

AngularTS exposes a WasmResource as shipping. Check status before calling the pricing export. The cart and destination stay in normal reactive scope state.

The server must serve the module with the correct WebAssembly MIME type. The directive owns loading and releases its resource when the scope is destroyed.

Keep DOM work in AngularTS. Use WebAssembly for compute-heavy functions with a small, explicit input and output contract.

Failure path

Module loading, memory copies, and MIME failures can erase compute gains. Keep a JavaScript or server fallback when the calculation is required.

Apply it now

Choose WebAssembly only for an existing compute-heavy algorithm, not ordinary UI arithmetic. Measure the JavaScript version, module startup, and data-transfer cost with realistic inputs. Keep the module only when repeated calls recover its loading cost and the input/output boundary stays small.

Verify

Test loading, MIME failure, realistic calculations, repeated calls, and scope removal. Confirm the measured gain exceeds module and transfer costs.

9.18 - Edit a server record in place

Load an editor, submit JSON, and keep server validation beside the field

Problem

A user should edit one record without leaving the page. The server owns validation and returns the saved record.

Before you start

The page already has the record ID and summary. The endpoint accepts JSON and returns field errors with status 422.

Working example

<article
  ng-init='profile = {"id":42,"name":"Ada"}; editing = false; errors = {}'
>
  <p ng-if="!editing">{{ profile.name }}</p>
  <button ng-if="!editing" ng-click="editing = true">Edit name</button>

  <form
    ng-if="editing"
    ng-put="/api/profiles/{{ profile.id }}"
    on-success="profile = $res; editing = false; errors = {}"
    on-error="errors = $res"
  >
    <input name="name" ng-model="profile.name" required />
    <span ng-if="errors.name">{{ errors.name }}</span>
    <button type="submit">Save</button>
    <button type="button" ng-click="editing = false">Cancel</button>
  </form>
</article>

Server contract

PUT /api/profiles/42
Content-Type: application/json

{"name":"Ada Lovelace"}

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{"name":"That name is already used"}

What AngularTS wires

The form creates JSON from named controls. A 2xx response replaces profile; a 4xx response becomes $res and stays beside the editor.

Failure path

Keep the editor open after validation or network failure. Do not replace the user’s input with the old server value until they cancel.

Apply it now

Choose one record users currently open on another page just to change one or two fields. Define its update URL, successful JSON body, and one realistic 422 body.

Verify

Edit, reject, correct, and save the record. Confirm focus and entered values survive the rejected request and the final summary uses the server response.

9.19 - Delete a row without rebuilding its list

Let the server authorize deletion and remove only the successful row

Problem

Deleting one item should not recreate the list, lose scroll position, or remove the row before the server authorizes the action.

Before you start

The server must authorize deletion and return success only after the record is gone. Give the rendered row a stable DOM ID that is unique on the page.

Working example

<ul>
  <li id="task-42">
    <span>Send invoice</span>
    <button
      ng-delete="/api/tasks/42"
      data-target="#task-42"
      swap="delete"
      on-error="deleteError = $res.message"
    >
      Delete
    </button>
  </li>
</ul>
<p ng-if="deleteError">{{ deleteError }}</p>

Server contract

Return 204 No Content only after authorization and deletion succeed. Return a 403 or 409 JSON body such as {"message":"This task is locked"} when the row must remain.

What AngularTS wires

The click sends DELETE. The delete swap removes the selected row only after a successful response. Errors update scope instead of touching the list.

Failure path

Disable or mark the button while the request runs when duplicate deletion matters. Always enforce ownership and authorization on the server.

Apply it now

Find a delete action that refreshes an entire collection. Give each row a stable DOM ID and make the endpoint return an error that explains why deletion can fail.

Verify

Delete the middle row, then reject another deletion. Confirm sibling nodes, focus, scroll position, and the rejected row remain unchanged.

9.20 - Search while the user types

Debounce requests and ignore responses for an older query

Problem

Search should feel immediate without sending a request for every keystroke or letting an older response replace newer results.

Before you start

The search endpoint must accept a bounded query and echo its normalized query in the JSON response. Decide the minimum useful query length and result limit.

Working example

<section ng-init="query = ''; results = []">
  <input
    type="search"
    name="query"
    ng-model="query"
    ng-get="/api/search?q={{ query }}"
    data-trigger="input"
    data-delay="250"
    on-success="$res.query === query && (results = $res.items)"
  />

  <p ng-if="query && !results.length">No results for {{ query }}.</p>
  <ul>
    <li ng-repeat="result in results">{{ result.name }}</li>
  </ul>
</section>

Server contract

Echo the normalized query with the results: {"query":"boots","items":[...] }. The equality check prevents a slow response for an earlier query from winning.

What AngularTS wires

data-delay waits for a pause. URL interpolation reads the current model, and on-success applies only the response that still matches it.

Failure path

Render request errors separately from an empty result. Add a minimum query length when broad searches are expensive, and enforce limits on the server.

Apply it now

Pick the search box with the highest request count. Record requests per query and time to useful results before and after adding the delay and stale-response check.

Verify

Type two queries quickly and delay the first response in DevTools. Confirm the URL contains the latest query and the late response never replaces newer results.

9.21 - Load one select from another

Request valid choices when a parent selection changes

Problem

A city, model, or category list depends on an earlier selection and should not be loaded before it is needed.

Before you start

The parent values must use stable server identifiers. The child endpoint must validate the parent identifier and return only choices valid for it.

Working example

<form
  ng-init="countries = [{code:'LV', name:'Latvia'}, {code:'EE', name:'Estonia'}]; cities = []"
>
  <select
    name="country"
    ng-model="country"
    ng-options="item.code as item.name for item in countries"
    ng-get="/api/cities?country={{ country }}"
    on-success="cities = $res; city = undefined"
  ></select>

  <select
    name="city"
    ng-model="city"
    ng-options="item.id as item.name for item in cities"
    ng-disabled="!cities.length"
  ></select>
</form>

Server contract

GET /api/cities?country=LV returns a bounded JSON array such as [{"id":1,"name":"Riga"}]. Reject unknown country codes on the server.

What AngularTS wires

A select uses change as its default HTTP trigger. The response replaces the available cities and clears a city that belonged to the previous country.

Failure path

Clear dependent values before showing new choices. Show a retry message when the request fails instead of leaving stale choices enabled.

Apply it now

Find a form that initially downloads every possible option. Move one dependent list behind its parent selection and compare initial payload size.

Verify

Change the parent twice. Confirm the child value clears, stale choices cannot be submitted, and the request includes only an allowed parent identifier.

9.22 - Load the next page near the viewport

Append server-rendered rows and replace the paging marker

Problem

A long feed should load another server-rendered page shortly before the user reaches the end without rebuilding existing rows.

Before you start

The server must expose normal paginated URLs and return HTML fragments with stable row identity. Keep explicit pagination as the non-JavaScript and keyboard fallback.

Working example

<ul id="orders">
  <li>Order 1001</li>
  <li>Order 1002</li>
</ul>
<div
  id="next-orders"
  ng-viewport
  data-viewport-once
  data-viewport-margin="300px 0px"
  on-enter="nextPage = 2"
  ng-get="/orders?page={{ nextPage }}"
  data-latch="{{ nextPage }}"
  data-target="#next-orders"
  swap="outerHTML"
>
  Loading more orders...
</div>

Server contract

Return new <li> rows followed by the same marker configured for page 3. Omit the marker on the last page. Keep a normal paginated URL so refresh and sharing work.

What AngularTS wires

The viewport changes nextPage; the latch sends the request; outerHTML replaces the old marker with rows and the next marker. Existing rows stay alive.

Failure path

Return a retry button when loading fails. Do not trap keyboard users in an endless feed; provide a normal pagination link and a reachable page footer.

Apply it now

Choose one feed where users commonly request page 2. Measure its rendered row count, DOM size, and requests before replacing the current pagination behavior.

Verify

Scroll with the Network panel open. Confirm each page loads once, rows are not recreated, the final response removes the marker, and the fallback URL still works.

9.23 - Update immediately and roll back on failure

Show a local change first while preserving the previous server value

Problem

A reversible action such as completing a task should feel immediate, but a rejected request must restore the last confirmed value.

Before you start

Choose a reversible, low-risk action and retain the last confirmed server value. The endpoint must return the canonical value or a clear conflict response.

Working example

<form
  ng-init='task = {"id":42,"complete":false}; confirmed = false'
  ng-put="/api/tasks/{{ task.id }}"
  on-success="task = $res; confirmed = $res.complete; saveError = undefined"
  on-error="task.complete = confirmed; saveError = $res.message"
>
  <label>
    <input name="complete" type="checkbox" ng-model="task.complete" />
    Complete
  </label>
  <button type="submit">Save</button>
  <span ng-if="saveError">{{ saveError }}</span>
</form>

Server contract

Return the canonical task after success. Return 409 Conflict with {"message":"Task changed on another device"} when the submitted version is stale.

What AngularTS wires

The checkbox updates local rendering immediately. confirmed changes only after a successful response and supplies the rollback value after failure.

Failure path

Use optimism only for reversible changes. Payments, destructive operations, and permission changes should wait for server confirmation.

Apply it now

Choose one low-risk action that currently waits on the network. Name the confirmed value, the optimistic value, and the exact response that requires rollback.

Verify

Test success, offline failure, and conflict. Confirm rollback uses the last confirmed server value rather than a guessed default.

9.24 - Return users to login after a session expires

Route a failed private request and preserve the intended destination

Problem

A user opens a private action after their cookie session expires. The application must show login rather than leaving a broken panel.

Before you start

Register the login router state and configure the request credential transport. Decide how the login flow stores and restores the intended destination.

Working example

<section>
  <button
    ng-get="/api/account"
    on-success="account = $res"
    on-error="sessionError = $res.message"
    data-state-error="login"
  >
    Refresh account
  </button>
  <p ng-if="sessionError">{{ sessionError }}</p>
</section>

Server contract

The private endpoint returns 401 with {"message":"Your session expired"}. The login state records the intended state and returns there after successful login.

What AngularTS wires

The directive exposes the JSON error and asks the router to enter login. Configure cookie credentials and route policy once; do not add auth logic to every button.

Failure path

data-state-error runs for every failed status, not only 401. Use this direct pattern only for an endpoint whose failures all require login; otherwise centralize status-aware handling in the HTTP or security layer.

Apply it now

Expire a real session, then activate one private request and one private URL. Decide where the intended destination is stored and which failures should not route to login.

Verify

Confirm the login page appears, credentials are not placed in URLs or logs, and a successful login returns to the original destination exactly once.

9.25 - Cache reference data without sharing user data

Cache stable lookup lists and invalidate them deliberately

Problem

Country, currency, or category lists are downloaded repeatedly even though they change rarely and are identical for every user.

Before you start

Provide an application cache implementing the linked cache-store contract. Confirm the endpoint is independent of user identity before sharing entries.

Working example

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

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

const countries = $rest<Country, string>('/api/reference/countries', Country, {
  backend: referenceBackend,
});
<label>
  Country
  <select
    ng-model="shipping.country"
    ng-options="country.code as country.name for country in countries"
  ></select>
</label>

The cached result is ordinary application state. Bind it like any other list; the template does not need to know which cache policy supplied it.

Server contract

The endpoint returns stable IDs, supports conditional HTTP caching, and never varies by the signed-in user. Publish a version change when administrators update the list.

What AngularTS wires

The cached backend reads through the supplied RestCacheStore. Successful writes invalidate matching collection and entity entries automatically.

Failure path

Do not place account, permission, price, or personalized responses in a shared cache. Use network-first when stale data can block a current operation.

Apply it now

Find a repeated GET in the Network panel. Prove its response is user-independent, choose an acceptable stale lifetime, and identify the write that invalidates it.

Verify

Load the same form twice, test offline fallback, update the reference list, and sign in as another user. Confirm the cache never crosses a user-specific boundary.

9.26 - Cancel a file upload and handle server rejection

Send FormData, abort through timeout, and distinguish cancellation

Problem

A large upload needs cancellation and useful server errors. The current fetch transport does not expose upload progress events.

Before you start

Register the controller on the application module and attach it to the form. Set server-side size, content, authorization, and storage limits before accepting files.

Working example

class UploadController {
  static $inject = ['$http'];

  constructor($http) {
    this.$http = $http;
  }

  async send(file) {
    this.cancellation = Promise.withResolvers();
    const body = new FormData();
    body.append('file', file);

    try {
      await this.$http.post('/api/uploads', body, {
        timeout: this.cancellation.promise,
      });
    } catch (error) {
      if (error.xhrStatus !== 'abort') this.error = error.data.message;
    }
  }

  cancel() {
    this.cancellation?.resolve();
  }
}
<form
  ng-controller="UploadController as upload"
  ng-submit="upload.send(upload.files[0])"
>
  <input type="file" name="file" ng-model="upload.files" required />
  <button type="submit">Upload</button>
  <button type="button" ng-click="upload.cancel()">Cancel</button>
  <p ng-if="upload.error">{{ upload.error }}</p>
</form>

Server contract

Validate size, MIME signature, filename, authorization, and storage limits on the server. Return 413 for size limits and 422 JSON for rejected content.

What AngularTS wires

Resolving the promise supplied as timeout aborts the underlying request. An aborted request rejects with xhrStatus === 'abort'.

Failure path

Do not set Content-Type manually for FormData; the browser adds the multipart boundary. Do not advertise progress: uploadEventHandlers is ignored by the fetch transport today.

Apply it now

Choose the largest upload your application accepts. Define its limit, cancellation behavior, retry behavior, and the message returned for each server rejection.

Verify

Cancel during transfer, reject an oversized file, and retry a valid file. Confirm an abort shows no failure message and no partial object becomes accessible.

9.27 - Load a dialog from the server

Swap server-rendered dialog content and restore focus on close

Problem

A confirmation dialog needs current server data, but its trigger must keep focus and remain usable if loading fails.

Before you start

The trigger must remain in the DOM while the fragment is open. The server fragment owns its accessible title, initial focus, close behavior, and authorization result.

Working example

<button
  ng-el="dialogTrigger"
  ng-get="/account/delete-dialog"
  data-target="#dialog-host"
  swap="innerHTML"
  on-error="dialogError = $res.message"
>
  Delete account
</button>
<div id="dialog-host"></div>
<p ng-if="dialogError">{{ dialogError }}</p>

Server contract

Return a labelled dialog fragment with a focused cancel button. Its close action calls dialogTrigger.focus() before removing the fragment. Return 403 JSON when the current user may no longer perform the action.

What AngularTS wires

The response is inserted and compiled in the trigger’s scope, so the fragment can use dialogTrigger, event directives, validation, and another HTTP directive.

Failure path

Keep the trigger in place while loading and failure occur. The fragment must trap focus when modal, close on Escape, restore focus, and expose a visible title.

Apply it now

Choose one modal whose content is stale or duplicated in client code. Move its title, warning, and authorization decision to one fragment endpoint.

Verify

Open by keyboard, cancel, reopen, reject authorization, and complete the action. Confirm focus always returns to the trigger and the background is not interactive.

9.28 - Understand the similarity to HTMX

Compare the server-first approaches without assuming compatibility

Problem

AngularTS HTTP directives look familiar if you know HTMX. You want to understand the shared approach without assuming the APIs behave alike.

Before you start

Start with the AngularTS HTTP directive reference. Treat HTMX knowledge as background for the server-first design, not as documentation for AngularTS.

Similar design questions

Design questionAngularTS mechanism
Trigger a requestng-get, ng-post, ng-put, ng-delete, data-trigger
Choose the destinationdata-target
Choose insertion behaviorswap
Render a server fragmentReturn HTML; AngularTS inserts and compiles it
Read JSONAssign $res in on-success or on-error
Show request stateloading, loading-class, bindings
Navigate after successdata-state-success
Prefetch from intentEvent trigger plus hidden scope state
Lazy load near the viewportng-viewport plus an HTTP directive
Keep local interaction stateScope, controller, or typed component view

Where the approach feels similar

Both approaches encourage useful questions for click-to-edit, active search, polling, lazy loading, server validation, fragment swaps, pagination, and prefetching: who renders the result, what triggers the request, where it goes, and what failure looks like.

Do not infer compatibility

AngularTS does not promise HTMX syntax or behavior. It has its own attributes, scope expressions, lifecycle, router, security configuration, response handling, and tests. Similar names or outcomes do not make an HTMX example an AngularTS example.

Failure path

Do not copy an HTMX attribute, extension, event, header, or history option and assume AngularTS recognizes it. Build from the AngularTS reference, preserve the normal server endpoint, and test error and navigation behavior independently.

Apply it now

Choose one server interaction in your application. Define its trigger, request, response type, target, local state, fallback, and failure behavior using only AngularTS documentation. Then note which design questions happen to feel familiar from HTMX.

Verify

Test the normal link or form fallback, direct server endpoint, successful swap or JSON assignment, failed response, keyboard path, and browser history. Do not use an HTMX result as evidence that AngularTS behaves the same way.

9.29 - Design an HTTP response for the UI

Choose status, content type, and body before writing the directive

Problem

A directive can only handle success, validation, navigation, and swaps correctly when the server response has a deliberate contract.

Before you start

Choose whether the server or browser renders the result. Write down what the user should see after success, invalid input, expired authentication, conflict, and unexpected failure.

Choose the response

SituationResponse contract
Saved data is needed locally2xx application/json with the canonical saved record
Only success matters204 No Content
Server owns the replacement UI2xx text/html with one fragment
User can correct submitted values422 application/json keyed by field name
Authentication expired401 application/json with a safe user message
User lacks permission403 application/json
Submitted version is stale409 application/json with conflict details
Server failed unexpectedly5xx application/json with a public message ID

Use on-success or on-error for JSON. Use data-target and swap for HTML. Do not return HTML error pages to a JSON interaction or 200 for rejected form data merely to avoid the error path.

Failure path

Never expose stack traces, database errors, tokens, or internal identifiers in a public error body. Log a correlation ID on the server and return only that ID with a safe message.

Apply it now

Choose one endpoint used by a cookbook recipe. Write its method, URL, success status, success content type, failure statuses, and one real body for each path before changing the template.

Verify

Inspect every response in the Network panel. Confirm its status and content type match the body, an empty success is actually empty, and each failure reaches the intended UI path.

9.30 - Make a server-driven interaction accessible

Preserve keyboard behavior, announce loading, and move focus deliberately

Problem

A request updates the page visually, but keyboard and screen-reader users also need an operable trigger, announced status, and predictable focus.

Before you start

Use a native button or link for the trigger. Choose a stable result container and decide whether focus should remain on the trigger or move to the new result.

Working example

<section ng-init="requestStatus = 'Results not loaded'">
  <button
    ng-get="/account/orders"
    data-target="#order-results"
    swap="innerHTML"
    loading
    on-success="requestStatus = 'Orders loaded'; orderResults.focus()"
    on-error="requestStatus = $res.message"
    aria-describedby="order-status"
  >
    Load orders
  </button>

  <p id="order-status" role="status" aria-live="polite">
    {{ requestStatus }}
  </p>
  <div id="order-results" ng-el="orderResults" tabindex="-1"></div>
</section>

Server contract

Return a heading and order rows as one HTML fragment. Return JSON with a short, actionable message for errors so the live region can announce it.

What AngularTS wires

The native button keeps keyboard activation. loading exposes request state, role="status" announces text changes, and ng-el provides the stable result element without a selector search.

Failure path

Do not move focus on failure or for background refreshes. Never replace the focused trigger with a swap unless the response provides the next focus target.

Apply it now

Run one server-driven interaction using only a keyboard and a screen reader. Name the trigger, loading announcement, error announcement, and final focus location before changing markup.

Verify

Activate with Enter and Space, delay the response, reject it, and return an empty result. Confirm status is announced once, focus is visible, and tab order remains logical.

9.31 - Audit a server-driven interaction before shipping

Check authorization, CSRF, HTML trust, caching, uploads, and errors

Problem

A form or HTTP directive works, but its security depends on decisions spread between markup, request configuration, the endpoint, and storage.

Before you start

Choose one real state-changing request. Record its authenticated user, resource, method, credential transport, response type, cache path, and uploaded content.

Audit the interaction

SurfaceShipping check
AuthorizationServer verifies this user may act on this resource
Cookie requestServer validates CSRF protection on every state-changing method
Bearer credentialToken never appears in URL, HTML, storage logs, or error output
HTML responseServer escapes untrusted values before AngularTS compiles it
JSON errorBody contains safe field messages, not stack or database details
CachePrivate entries are partitioned and cleared on logout
UploadServer checks size, MIME signature, filename, and storage limits
RedirectDestination is allowlisted rather than copied from user input

Client validation, hidden buttons, route policy, and disabled controls improve the user experience. None of them authorize a server operation.

Failure path

Use 401 when authentication is missing, 403 when the authenticated user lacks permission, and 422 for correctable input. Do not turn a security rejection into a successful response just to simplify the template.

Apply it now

Capture one real request in the Network panel and answer every row using the server implementation, not assumptions from the UI. Add a server test for each answer that can regress.

Verify

Repeat the request signed out, as another user, without the CSRF proof, with tampered IDs, with malicious text, and with an oversized upload when applicable. Confirm every rejection is safe and leaves no unauthorized state change.

9.32 - Keep a form useful before AngularTS starts

Build a normal server form first, then add faster in-page behavior

Problem

A request should still work when JavaScript is slow, unavailable, or fails to start.

Before you start

Make the endpoint accept a normal form submission and return a complete page. Only then add AngularTS behavior to improve the interaction.

Start with the browser’s form behavior

<form
  action="/orders"
  method="post"
  ng-post="/orders"
  data-state-success="orders"
  on-error="errors = $res"
>
  <label>
    Quantity
    <input name="quantity" type="number" min="1" required />
  </label>
  <button type="submit">Place order</button>
  <p ng-if="errors.quantity">{{ errors.quantity }}</p>
</form>

Without AngularTS, the browser posts the form to /orders. With AngularTS, the HTTP directive submits it without a page reload and the router enters orders after success. The endpoint must accept both the browser’s form encoding and the enhanced request body.

Server contract

For a normal browser submission, return a complete success or validation page. For the enhanced request, return 2xx on success or a JSON error object such as {"quantity":"Choose at least one item"} with 422. Decide how the server distinguishes these requests; do not depend on an undocumented client header.

Failure path

Keep native labels, required fields, methods, actions, and submit buttons. A network failure may prevent either form of submission, but it must not leave a control that only works after JavaScript wiring.

Apply it now

Choose one important form. Remove JavaScript mentally and check whether its action, method, controls, and server response still complete the task.

Verify

Submit once with JavaScript disabled and once with AngularTS running. Test valid input, invalid input, a signed-out session, and a server error in both paths.

9.33 - Debug an HTTP interaction from the browser

Trace an element, its scope, its request, and the resulting UI state

Problem

A click or submission produces the wrong result and you need to find whether the problem is the element, scope, request, response, or rendering.

Before you start

Open the Elements, Console, and Network panels. Select the element that owns the HTTP directive so it is available as $0 in the console.

Follow one interaction

const scope = angular.getScope($0);
scope.order;

Check the directive attributes on $0, inspect the values used by its expressions, then perform the interaction with Network recording enabled. Read the request method, URL, payload, status, content type, and response before looking at the rendered result.

After the response, inspect the scope again. If the response is correct but the scope is not, check the directive assignment such as $res, on-success, or on-error. If scope is correct but the page is not, inspect the binding or conditional directive that renders it.

Failure path

Do not debug only the successful request. Preserve the failed response in the Network panel and inspect the error branch, including status-specific behavior for 401, 403, 409, 422, and 5xx responses.

Apply it now

Take one interaction that currently feels opaque and write down the value at each step: element, scope before request, request, response, scope after request, and DOM result. The first unexpected value identifies the layer to fix.

Verify

Repeat the trace after the fix. Confirm the request occurs once, the correct branch runs, and a failed response leaves the scope and DOM in a usable state.

9.34 - Control repeated requests

Measure duplicate work, then delay, throttle, cache, or cancel it

Problem

Typing, scrolling, hovering, or repeated clicks can start more requests than the user needs.

Before you start

Record the interaction in the Network panel. Count requests, transferred bytes, server time, and stale responses. Do not add request control without seeing the problem first.

Choose the smallest control

What you observeFirst response
Fast typing starts a request per keyAdd a short delay after input settles
Scroll or resize fires continuouslyThrottle the event
The same read repeats unchangedReuse an appropriate public or user-partitioned cache
An old search overwrites a newer oneReject stale results or cancel the old request
Hover prefetch repeatsGuard it with loaded and loading state
Double-click repeats a writeDisable while pending and make the server idempotent

Use a promise-valued $http timeout to cancel an obsolete programmatic request. Treat xhrStatus === "abort" as cancellation rather than a server failure. HTTP directive flows should usually prevent another trigger while the current request is pending instead of creating custom cancellation code.

Server contract

Search and list endpoints must tolerate reordered reads. Write endpoints must protect against duplicate effects with an idempotency key, version, or another server-side rule when repetition could charge, send, or mutate twice.

Failure path

Always clear loading state after success, error, or cancellation. Never cache an authorization failure or leak user-specific data through a shared cache.

Apply it now

Find the noisiest interaction in the Network panel. Apply only the first matching row, then measure it again before combining techniques.

Verify

Trigger the interaction rapidly, use a slow network profile, and force responses to arrive out of order. Confirm the newest intent wins and writes happen once.

9.35 - Show validation errors returned by the server

Keep validation authoritative on the server and render field errors in place

Problem

A form needs immediate, useful errors without duplicating the server’s validation rules in browser code.

Before you start

Make the endpoint return 422 with a JSON object whose keys match form control names. Use short messages that are safe to show directly to the user.

Render the error object

<form
  ng-post="/account/profile"
  on-success="profile = $res; errors = {}"
  on-error="errors = $res"
>
  <label ng-class="{ error: errors.email }">
    Email
    <input
      name="email"
      type="email"
      value="{{ profile.email }}"
      aria-invalid="{{ errors.email !== undefined }}"
      ng-keyup="errors.email = undefined"
    />
    <span ng-if="errors.email">{{ errors.email }}</span>
  </label>
  <button type="submit">Save profile</button>
</form>

The same error object drives the message, error class, and accessibility state. Browser validation can improve feedback, but the server still checks every value.

Server contract

Return the saved profile with 2xx. Return a body such as {"email":"Use a work email address"} with 422 for correctable input. Reserve 401, 403, 409, and 5xx for their distinct meanings.

Failure path

If an error is not a field error, show a form-level message instead of assigning it to an unrelated control. Never return stack traces or database messages.

Apply it now

Pick one form that reimplements server validation in JavaScript. Make the server return named errors and bind those errors beside the matching controls.

Verify

Submit empty, malformed, duplicate, unauthorized, and valid values. Confirm the right field is identified and keyboard focus remains usable after every response.

9.36 - Protect a cookie-authenticated form from CSRF

Send a server-issued proof with every state-changing form request

Problem

A form uses authentication cookies, so another site may be able to cause the browser to send an unwanted state-changing request.

Before you start

Configure CSRF protection on the server. Generate a token tied to the user or session and render it into the page. Cookie SameSite settings help, but they do not replace server-side CSRF validation.

Submit the server-issued token

<form ng-post="/account/email" on-error="errors = $res">
  <input
    type="hidden"
    name="csrf_token"
    value="SERVER_RENDERED_TOKEN"
  />
  <label>
    New email
    <input name="email" type="email" required />
  </label>
  <button type="submit">Change email</button>
</form>

Replace SERVER_RENDERED_TOKEN in the server template. AngularTS serializes the named hidden control with the rest of the form; the server validates it before changing state.

Server contract

Reject a missing, expired, or mismatched token without performing the operation. Rotate tokens according to the server framework’s policy and compare them using its maintained CSRF implementation rather than custom cryptography.

Failure path

Return a safe 403 response when the proof fails. Do not automatically retry the write because a fresh token may require a new authenticated page response.

Apply it now

Inspect every cookie-authenticated POST, PUT, PATCH, and DELETE endpoint. Start with account, payment, and permission changes.

Verify

Send the request with the valid token, no token, a token from another session, and an expired token. Confirm only the valid request changes state.

9.37 - Confirm a destructive action in place

Require an explicit second action before sending a delete request

Problem

A single accidental click can delete important data.

Before you start

Make deletion authorization and resource ownership checks authoritative on the server. Decide whether the operation is reversible and say so in the interface.

Ask for a second explicit action

<article ng-init="confirming = false; deleted = false" ng-if="!deleted">
  <h2>Quarterly report</h2>
  <button type="button" ng-click="confirming = true">Delete</button>
  <div ng-if="confirming" role="alert">
    <p>This permanently deletes the report.</p>
    <button
      type="button"
      ng-delete="/api/reports/42"
      on-success="deleted = true"
      on-error="deleteError = $res"
    >
      Delete permanently
    </button>
    <button type="button" ng-click="confirming = false">Cancel</button>
    <p ng-if="deleteError">{{ deleteError.message }}</p>
  </div>
</article>

The first button changes only local state. The second button sends the request and names the exact consequence.

Server contract

Return 204 after deletion. Return 404 when the resource no longer exists and 403 when the user may not delete it. Make repeated deletion safe when possible.

Failure path

Keep the confirmation visible after failure so the user can read the message or cancel. Do not remove the item until the server confirms deletion.

Apply it now

Find the most damaging one-click action in the application and split intent from execution. Use a specific label such as “Delete report,” not “Yes.”

Verify

Test keyboard-only confirmation, cancellation, double activation, a missing resource, and a permission rejection. Confirm only one authorized delete occurs.

9.38 - Handle two people editing the same record

Detect stale updates and let the user compare the current server value

Problem

One person can overwrite a change saved by someone else while both are editing the same record.

Before you start

Give each record a version, revision, or strong ETag. Require the client to send the version it originally loaded with every update.

Send the version with the edit

<form
  ng-put="/api/articles/42"
  on-success="article = $res; conflict = undefined"
  on-error="conflict = $res"
>
  <input name="version" type="hidden" value="{{ article.version }}" />
  <label>
    Title
    <input name="title" value="{{ article.title }}" required />
  </label>
  <button type="submit">Save article</button>
  <aside ng-if="conflict.current">
    <p>Someone saved a newer version: {{ conflict.current.title }}</p>
    <button
      type="button"
      ng-click="article = conflict.current; conflict = undefined"
    >
      Load newer version
    </button>
  </aside>
</form>

Server contract

Update only when the submitted version matches the stored version. Otherwise return 409 with a safe representation of the current record. Increment the version atomically with the successful update.

Failure path

Do not silently retry a conflicting write. Preserve the user’s draft while showing the current server value so they can compare or merge intentionally.

Apply it now

Add a version check to one record that is commonly edited by multiple people. Start with content, inventory, scheduling, or permission records.

Verify

Open the same record in two sessions. Save different values in both and confirm the second save receives 409 without overwriting the first.

9.39 - Let the user retry a failed read

Keep the last useful state and retry only when the user asks

Problem

A temporary network or server failure leaves a section empty or forces a full page reload.

Before you start

Keep reads safe and idempotent. Decide whether old data is more useful than an empty screen while a refresh fails.

Keep recovery next to the result

<section ng-init="orders = []; loadError = undefined">
  <button
    type="button"
    ng-get="/api/orders"
    on-success="orders = $res; loadError = undefined"
    on-error="loadError = $res"
  >
    Refresh orders
  </button>
  <p ng-if="loadError" role="alert">
    Orders could not be refreshed. Try again.
  </p>
  <p ng-if="!loadError && orders.length === 0">No orders yet.</p>
  <ul>
    <li ng-repeat="order in orders">{{ order.number }}</li>
  </ul>
</section>

The existing list remains visible if refresh fails. The user controls when another request is sent.

Server contract

Return a JSON array with 2xx. Use 401 for an expired session and 5xx for a temporary server failure. Include a safe request ID in unexpected errors.

Failure path

Do not retry writes automatically. Avoid unlimited automatic read retries; they can amplify an outage and consume mobile data while the user cannot intervene.

Apply it now

Choose one useful read that currently disappears on failure. Preserve its last successful value and put a retry control beside the error.

Verify

Load successfully, then force offline mode and retry. Confirm the old result stays visible, the error is announced, and each click sends exactly one request.

9.40 - Paginate a large result on the server

Keep URLs shareable while replacing only the result region

Problem

A large collection is slow to query, transfer, render, and navigate.

Before you start

Make the server return a bounded page and stable next and previous URLs. Prefer a cursor when records change frequently; use page numbers when random access is important and the data is stable enough.

Keep real navigation URLs

<section>
  <div id="orders">
    <p>Orders 21–40</p>
  </div>
  <nav aria-label="Order pages">
    <a
      href="/orders?page=1"
      ng-get="/orders?page=1"
      data-target="#orders"
      swap="innerHTML"
    >
      Previous
    </a>
    <a
      href="/orders?page=3"
      ng-get="/orders?page=3"
      data-target="#orders"
      swap="innerHTML"
    >
      Next
    </a>
  </nav>
</section>

The href remains useful for opening a page in another tab and for normal navigation before AngularTS starts. The HTTP directive can replace only the list when the enhanced interaction runs.

Server contract

Return a complete page for normal navigation and a trusted list fragment for the enhanced request according to the application’s explicit response policy. Keep sort and filter values in every pagination URL.

Failure path

Leave the current page visible if the next page fails. Do not advance the visible page label or browser URL until the requested result succeeds.

Apply it now

Measure the largest rendered list. Move its limit, ordering, and cursor to the server before adding virtual scrolling or client-side collection machinery.

Verify

Open next and previous links normally, in a new tab, with JavaScript disabled, and with AngularTS running. Confirm the same records and ordering appear.

9.41 - Download a generated file with the browser

Use normal navigation for files instead of loading bytes into application state

Problem

A report or export needs to download without turning a large binary response into scope state or manually constructing a browser blob.

Before you start

Make the server stream the file with the correct Content-Type and a safe Content-Disposition filename. Authorize the download on every request.

Let the browser own the response

<form action="/reports/orders.csv" method="get">
  <label>
    From
    <input name="from" type="date" required />
  </label>
  <label>
    To
    <input name="to" type="date" required />
  </label>
  <button type="submit">Download CSV</button>
</form>

A native form lets the browser stream, name, save, and expose download progress without retaining the file in AngularTS memory.

Server contract

Validate dates, cap the export size, escape spreadsheet-formula prefixes when creating CSV, and stream rows instead of building the entire file in memory.

Failure path

For long exports, create a background job and return a status page rather than holding an HTTP request indefinitely. Show authorization or validation errors as a normal server page.

Apply it now

Find one download implemented with $http, base64, or a manually created blob. Replace it with a normal authorized download when no client transformation is required.

Verify

Test small, empty, large, unauthorized, and maliciously named exports. Confirm memory stays stable and the downloaded filename and content type are correct.

9.42 - Replace a region with trusted server HTML

Keep rendering on the server without compiling untrusted markup

Problem

The server already renders a useful partial and returning JSON would duplicate the same presentation rules in a client template.

Before you start

Choose a stable target element. Treat every returned fragment as executable UI: escape untrusted values on the server and allow only application-owned markup.

Swap the fragment into a named region

<section>
  <button
    type="button"
    ng-get="/account/summary"
    data-target="#account-summary"
    swap="innerHTML"
  >
    Refresh summary
  </button>
  <div id="account-summary" aria-live="polite">
    <p>Summary not loaded.</p>
  </div>
</section>

AngularTS wires directives and bindings in the returned application-owned fragment. That makes server escaping and content ownership security requirements, not optional cleanup.

Server contract

Return text/html containing valid fragment markup. Keep scripts, inline event handlers, user-authored templates, and unsanitized rich text out of the response.

Failure path

Keep the old region on failure and show an error beside the refresh control. Never swap an authentication page or generic proxy error into the target.

Apply it now

Choose one JSON endpoint whose client template simply recreates existing server HTML. Return the existing partial directly and remove the duplicate renderer.

Verify

Test names containing <, >, quotes, and markup-like text. Confirm they render as text, the fragment has balanced tags, and its controls work after replacement.

9.43 - Show a request ID when an operation fails

Give users and operators one safe identifier for tracing a failure

Problem

A user reports “Save failed,” but operators cannot connect that report to one server request among thousands.

Before you start

Generate or accept a request ID at the trusted server edge, carry it through logs and downstream calls, and return it in safe unexpected-error responses.

Render the identifier with the error

<form ng-post="/api/invoices" on-error="saveError = $res">
  <label>
    Customer
    <input name="customer" required />
  </label>
  <button type="submit">Create invoice</button>
  <div ng-if="saveError" role="alert">
    <p>The invoice could not be created.</p>
    <p ng-if="saveError.requestId">
      Support code: {{ saveError.requestId }}
    </p>
  </div>
</form>

The user sees a stable support code, not internal diagnostics.

Server contract

Return a body such as {"message":"Request failed","requestId":"req_..."} for unexpected errors. Log the same ID with route, status, duration, and safe context. Never put credentials, personal data, stack traces, or SQL in the response.

Failure path

If the client supplies an ID, validate its format before logging it. Keep an independently generated internal trace identifier when crossing trust boundaries.

Apply it now

Take one high-value write and follow its request ID from the response to every relevant server log. Remove any step that requires searching by timestamp alone.

Verify

Force a controlled 5xx, copy the displayed code, and find exactly one request path in logs. Confirm the response exposes no implementation details.

9.44 - Check a server interaction before shipping

Walk one feature through behavior, failure, access, and operations

Problem

An interaction works in the happy path but its production behavior has not been checked as one complete system.

Before you start

Choose one real user action and use its production-like endpoint and permissions. Open the Elements, Console, and Network panels.

Walk the whole path

Check these in order:

StepQuestion
PageDoes the useful server-rendered page work before enhancement?
InputAre labels, names, constraints, and server validation aligned?
RequestIs the method correct and sent only once?
AccessDoes the server check authentication, authorization, and CSRF?
ResponseAre status, content type, and body appropriate?
StateDoes success update only the intended scope and DOM?
FailureCan the user understand, retry, cancel, or recover safely?
ConcurrencyCan stale or duplicate requests corrupt state?
AccessibilityAre status and focus understandable without a pointer?
OperationsCan a request ID connect the visible failure to server logs?

Failure path

Treat any unanswered row as unfinished implementation rather than documentation debt. Fix the smallest owning layer: browser markup, AngularTS wiring, endpoint, storage transaction, or operational logging.

Apply it now

Run the table against the newest state-changing interaction. Record evidence for each answer: a browser trace, server test, accessibility check, or log lookup.

Verify

Repeat with invalid input, no permission, slow networking, duplicate activation, stale data, and a controlled server failure. Ship when every outcome is deliberate.

9.45 - Recover from a rate-limited request

Respect 429 responses without hiding retries or multiplying traffic

Problem

An endpoint limits repeated requests, but the page reports a generic failure or immediately retries and makes the limit worse.

Before you start

Make the server return 429 and a Retry-After header. Use rate limits to protect a resource, not as the only defense against abusive or unauthorized use.

Keep retry under the user’s control

<section ng-init="limitError = undefined">
  <button
    type="button"
    ng-get="/api/reports/preview"
    on-success="preview = $res; limitError = undefined"
    on-error="limitError = $res"
  >
    Refresh preview
  </button>
  <p ng-if="limitError" role="alert">
    {{ limitError.message }} Try again after {{ limitError.retryAfter }}.
  </p>
  <div ng-if="preview">{{ preview.summary }}</div>
</section>

Do not run a tight automatic retry loop. Keep the last useful result visible and tell the user when another attempt can succeed.

Server contract

Return 429, Retry-After, and a safe body such as {"message":"Preview limit reached","retryAfter":"14:30"}. Apply the limit to the correct user, tenant, token, or resource rather than only an IP address.

Failure path

Treat 429 differently from authentication, permission, validation, and server failures. Do not cache the response longer than the stated limit window.

Apply it now

Find the endpoint most likely to receive bursts. Confirm the page exposes the limit instead of silently creating more traffic.

Verify

Exceed the limit, inspect the header and body, wait for the window, and retry. Confirm no background loop continues while the request is limited.

9.46 - Run slow work as a background job

Start work quickly, expose status, and download the finished result

Problem

A report, import, or media conversion takes too long for one request and becomes fragile behind browser, proxy, or deployment timeouts.

Before you start

Give the job a durable ID, owner, state, progress value, and expiry policy. Make starting the same job twice safe when duplicate work would be expensive.

Start the job, then ask for status

<section>
  <form ng-post="/api/exports" on-success="job = $res">
    <label>
      Format
      <select name="format"><option value="csv">CSV</option></select>
    </label>
    <button type="submit">Create export</button>
  </form>
  <div ng-if="job">
    <p>Status: {{ job.status }}</p>
    <button
      type="button"
      ng-get="/api/exports/current"
      on-success="job = $res"
    >
      Refresh status
    </button>
    <a ng-if="job.downloadUrl" href="{{ job.downloadUrl }}">Download</a>
  </div>
</section>

Use manual refresh first. Add measured polling or server-sent events only when the user benefits from continuous progress.

Server contract

Return 202 with the accepted job representation. The status endpoint returns queued, running, completed, failed, or cancelled. Authorize status and download URLs against the job owner on every request.

Failure path

Keep a failed job record with a safe explanation and request ID. Do not expose worker stack traces or leave failed uploads and generated files indefinitely.

Apply it now

Find the longest request in production traces. Move it to a job when its work can continue independently of the browser connection.

Verify

Test duplicate starts, worker restart, cancellation, failure, expiry, another user’s job ID, and download after completion.

9.47 - Make a critical write safe to repeat

Prevent double charges, sends, and submissions with an idempotency key

Problem

A double click, reconnect, proxy retry, or impatient refresh can submit the same critical operation more than once.

Before you start

Generate a unique operation key on the server-rendered page. Scope it to the authenticated user and operation, and store the first result atomically.

Submit the operation key with the form

<form ng-post="/api/payments" on-error="paymentError = $res">
  <input
    type="hidden"
    name="idempotency_key"
    value="SERVER_RENDERED_OPERATION_KEY"
  />
  <label>
    Amount
    <input name="amount" inputmode="decimal" required />
  </label>
  <button type="submit">Pay once</button>
  <p ng-if="paymentError" role="alert">{{ paymentError.message }}</p>
</form>

Disabling the button improves feedback but cannot provide this guarantee. The server key protects retries from every source.

Server contract

For the same user, operation, key, and payload, return the stored first result. Reject reuse of the key with a different payload. Expire records only after the operation can no longer be retried safely.

Failure path

Do not mint a new key merely because the response was lost. Reuse the operation key until the server confirms whether the first request completed.

Apply it now

Add idempotency to the operation with the highest cost of duplication: payment, invitation, email, provisioning, or irreversible workflow transition.

Verify

Send identical requests concurrently, disconnect after sending, and retry with the same key. Confirm one effect and one stable result are recorded.

9.48 - Build a multi-step form on the server

Save each step independently instead of keeping a fragile browser-only draft

Problem

A long form needs several steps, but refreshing, navigating back, or changing devices must not discard completed work.

Before you start

Create a server-side draft with an owner and current step. Validate and save each step independently. Make every step addressable with a normal URL.

Enhance a normal step form

<form
  action="/checkout/address"
  method="post"
  ng-post="/checkout/address"
  data-state-success="checkout-payment"
  on-error="errors = $res"
>
  <label>
    Delivery address
    <textarea name="address" required></textarea>
  </label>
  <p ng-if="errors.address">{{ errors.address }}</p>
  <button type="submit">Continue to payment</button>
</form>

The server saves the address before advancing. AngularTS can enter the next router state after success, while the native action remains a working fallback.

Server contract

Return 422 for step errors, redirect a normal successful submission to the next URL, and return 2xx for the enhanced request. Reject steps the user is not yet allowed to complete.

Failure path

Keep the saved draft when a later step fails. Never trust a hidden step number as proof that earlier requirements or payment checks succeeded.

Apply it now

Choose the longest form and split it at points where the server can save a useful draft. Keep related fields together rather than creating arbitrary page counts.

Verify

Refresh every step, use the Back button, open the draft on another device, submit steps out of order, and resume after a failed request.

9.49 - Apply one action to several records

Submit explicit selections and return a result for every record

Problem

Users need to update several records without opening and submitting each one individually.

Before you start

Choose a maximum batch size. Authorize every selected record independently and decide whether the operation is atomic or may partially succeed.

Submit explicit selected controls

<form ng-post="/api/tasks/archive" on-success="result = $res">
  <fieldset>
    <legend>Tasks to archive</legend>
    <label><input name="task_42" type="checkbox" /> Draft invoice</label>
    <label><input name="task_43" type="checkbox" /> Review contract</label>
  </fieldset>
  <button type="submit">Archive selected tasks</button>
  <p ng-if="result">Archived {{ result.archived }} tasks.</p>
  <ul><li ng-repeat="error in result.errors">{{ error.message }}</li></ul>
</form>

Named controls make the submitted selection inspectable in the Network panel. For dynamic lists, use the server’s documented array representation consistently.

Server contract

Return a count and per-record failures. If partial success is allowed, make the response identify exactly which records changed. If it must be atomic, perform all authorization and validation before committing any record.

Failure path

Do not report the whole batch as successful when some records failed. Preserve failed selections so the user can correct or retry them.

Apply it now

Find a repetitive list action and define its batch limit, authorization rule, transaction behavior, and response before adding selection controls.

Verify

Test no selection, the maximum size, duplicates, mixed permissions, missing records, partial failure, and a repeated submission.

9.50 - Archive first and offer restore

Make a destructive-looking action recoverable when the domain allows it

Problem

Users need to remove an item from active work, but permanent deletion is too easy to regret.

Before you start

Model archived state on the server. Define retention, visibility, uniqueness, restore permissions, and the separate conditions for permanent deletion.

Keep restore beside the result

<section ng-init="archived = false">
  <button
    ng-if="!archived"
    type="button"
    ng-post="/api/projects/42/archive"
    on-success="archived = true"
  >
    Archive project
  </button>
  <div ng-if="archived" role="status">
    <p>Project archived.</p>
    <button
      type="button"
      ng-post="/api/projects/42/restore"
      on-success="archived = false"
    >
      Restore
    </button>
  </div>
</section>

The page changes only after each server operation succeeds.

Server contract

Make archive and restore idempotent. Record who changed the state and when. Keep permanent deletion as a separate, more restricted operation.

Failure path

If restore fails, keep the archived message and explain the conflict or permission problem. Do not imply that a client-side undo can reverse a committed server action.

Apply it now

Replace one permanent delete with archive and restore where legal and operational requirements allow retention.

Verify

Archive twice, restore twice, restore after a naming conflict, test another user, and confirm archived records stay out of active queries.

9.51 - Handle a permission rejection without hiding it

Let the server decide access and give the user a useful next step

Problem

The interface hides controls based on known permissions, but permissions can change and direct or stale requests still reach the server.

Before you start

Authorize every operation on the server using the authenticated user and current resource. Client visibility is presentation, not enforcement.

Render the server rejection

<form
  ng-put="/api/team/42/settings"
  on-success="settings = $res; accessError = undefined"
  on-error="accessError = $res"
>
  <label>
    Team name
    <input name="name" value="{{ settings.name }}" required />
  </label>
  <button type="submit">Save settings</button>
  <p ng-if="accessError" role="alert">
    {{ accessError.message }}
  </p>
</form>

A disabled or hidden button can reduce confusion when permission is already known, but the rejection path remains necessary.

Server contract

Return 401 when authentication is missing and 403 when the authenticated user lacks permission. Keep the message safe when even resource existence is private.

Failure path

Do not redirect every 403 to login; signing in again cannot grant a missing permission. Offer a useful next step such as returning, requesting access, or contacting an administrator.

Apply it now

Open one privileged action in two sessions, remove permission in one, and submit the stale form in the other. Make the resulting rejection understandable.

Verify

Test signed out, wrong role, wrong tenant, removed permission, deleted resource, and authorized access. Confirm no rejected request changes state.

9.52 - Keep search and filters in the URL

Make filtered results reloadable, shareable, and usable without JavaScript

Problem

Filters exist only in browser state, so refresh, sharing, Back, and opening a new tab lose the current result.

Before you start

Define stable query parameter names and defaults on the server. Return the same result for the same authorized URL.

Start with a real GET form

<form
  action="/orders"
  method="get"
  ng-get="/orders"
  data-target="#orders"
  swap="innerHTML"
>
  <label>Status <select name="status"><option>Open</option></select></label>
  <label>Search <input name="q" type="search" /></label>
  <button type="submit">Apply filters</button>
</form>
<section id="orders" aria-live="polite">
  <p>Choose filters to update the result.</p>
</section>

The native GET form defines the canonical URL. Enhancement may replace the result region, but it must preserve a URL that reproduces the same filters.

Server contract

Parse, validate, normalize, and cap query values. Include normalized filters in pagination links and render them back into the controls on complete pages.

Failure path

Leave the current result visible when an enhanced request fails. Do not write an unconfirmed filter state into browser history.

Apply it now

Refresh the most-used filtered list. Move every lost filter into a named GET control and make the resulting URL reproduce the page.

Verify

Submit normally, copy the URL, open it in another tab, use Back and Forward, and repeat with JavaScript disabled and AngularTS running.

9.53 - Log out with a state-changing form

End the server session, clear private state, and avoid logout links

Problem

Logout is implemented as a GET link or only clears browser state, leaving the server session active or allowing unwanted cross-site logout requests.

Before you start

Provide a server POST endpoint that invalidates the session and rotates relevant credentials. Decide which private caches and client state must be discarded.

Submit logout as a protected form

<form
  action="/logout"
  method="post"
  ng-post="/logout"
  data-state-success="signed-out"
>
  <input
    type="hidden"
    name="csrf_token"
    value="SERVER_RENDERED_TOKEN"
  />
  <button type="submit">Log out</button>
</form>

The normal form works without client routing. The enhanced form can enter a public signed-out state after the server confirms invalidation.

Server contract

Invalidate the session, clear or expire authentication cookies, and return a public response. Make repeated logout safe. Never use GET for this state change.

Failure path

If the server cannot confirm logout, do not claim the session ended. Avoid keeping private response data in a cache that another signed-in user can inherit.

Apply it now

Inspect the current logout request and subsequent Back navigation. Remove any private page or cached data that remains usable after a confirmed logout.

Verify

Log out normally and with AngularTS, repeat the request, use Back, open a private URL, and sign in as another user in the same browser.

9.54 - Keep requests same-origin unless they must cross origins

Configure CORS and credentials deliberately for a separate API origin

Problem

The page and API use different origins, introducing preflight, credential, cookie, caching, and deployment behavior that same-origin requests avoid.

Before you start

Ask whether the application server can proxy the API under the page’s origin. Prefer that simpler setup unless direct browser access to the other origin is a real requirement.

Configure the server, not the browser workaround

Allow only known application origins, required methods, and required headers. When cookies are involved, use explicit origins and credentials; wildcard origin responses cannot safely represent authenticated access. Keep CSRF protection for cookie-authenticated state changes.

Server contract

Handle preflight requests consistently, vary cached responses by origin when needed, and return the same authorization and validation statuses as a same-origin endpoint. Treat CORS as browser access policy, not user authorization.

Failure path

Do not disable browser security, reflect arbitrary Origin values, put secrets in URLs, or convert a CORS failure into a JSONP-style executable response.

Apply it now

List every production origin that can call the API and every credential it sends. Remove unused origins, headers, and methods from the server policy.

Verify

Test the allowed production origin, an unlisted origin, preflight, credentials, cache behavior, and a state-changing request without CSRF proof.

9.55 - Handle a uniqueness race on submit

Let the database decide uniqueness and return the conflict to the field

Problem

The page checks that a name is available, but another request claims it before the user submits.

Before you start

Enforce uniqueness with a database constraint. Availability checks improve feedback but can never reserve the value unless the server explicitly does so.

Return the conflict to the field

<form ng-post="/api/teams" on-error="errors = $res">
  <label ng-class="{ error: errors.slug }">
    Team address
    <input
      name="slug"
      required
      aria-invalid="{{ errors.slug !== undefined }}"
      ng-keyup="errors.slug = undefined"
    />
    <span ng-if="errors.slug">{{ errors.slug }}</span>
  </label>
  <button type="submit">Create team</button>
</form>

Server contract

Attempt the insert and translate the known uniqueness violation into 409 or a field-oriented 422 body such as {"slug":"That address is already used"}. Do not expose database error text.

Failure path

Preserve every other form value. Ask the user to choose another value instead of retrying the same conflicting write automatically.

Apply it now

Find one pre-submit uniqueness check and confirm the final write still handles a database constraint violation as an expected outcome.

Verify

Submit the same value concurrently from two sessions. Confirm one succeeds, one gets the field error, and no duplicate record is stored.

9.56 - Submit money without floating-point surprises

Send a decimal string and perform currency arithmetic on the server

Problem

An amount changes by a cent because browser or server code treats currency as a binary floating-point number.

Before you start

Choose the currency, accepted decimal precision, rounding rule, minimum, and maximum on the server. Never infer currency only from the user’s locale.

Submit the entered decimal text

<form ng-post="/api/invoices" on-error="errors = $res">
  <label>
    Amount in EUR
    <input
      name="amount"
      inputmode="decimal"
      autocomplete="off"
      required
    />
  </label>
  <input name="currency" type="hidden" value="EUR" />
  <p ng-if="errors.amount">{{ errors.amount }}</p>
  <button type="submit">Create invoice</button>
</form>

Server contract

Parse the amount with a decimal or money type, validate its scale, and store minor units or an exact decimal together with the currency. Return formatted display text separately from the canonical value when needed.

Failure path

Reject ambiguous separators and unsupported precision with 422. Do not silently round an amount when that changes what the user authorized.

Apply it now

Trace one amount from input through JSON, application code, database, and output. Remove every binary floating-point conversion from that path.

Verify

Test 0.01, large values, too many decimal places, negative values, comma input, and a currency with a different minor-unit rule.

9.57 - Submit dates and times with an explicit time zone

Distinguish calendar dates, local times, and exact instants

Problem

A date or appointment moves by a day or hour when users and servers use different time zones.

Before you start

Decide whether each value is a calendar date, a local wall-clock time, or an exact instant. These are different domain values and need different storage rules.

Send the local value and zone together

<form ng-post="/api/appointments" on-error="errors = $res">
  <label>
    Local start
    <input name="localStart" type="datetime-local" required />
  </label>
  <label>
    Time zone
    <select name="timeZone" required>
      <option value="Europe/Riga">Europe/Riga</option>
    </select>
  </label>
  <p ng-if="errors.localStart">{{ errors.localStart }}</p>
  <button type="submit">Schedule</button>
</form>

Server contract

Resolve local time with an IANA zone, reject nonexistent daylight-saving times, and define how ambiguous times are chosen. Store exact instants in UTC while retaining the zone when future local scheduling depends on it.

Failure path

Do not append Z to a local value or assume the server’s zone. Return 422 when the local time cannot identify the intended instant.

Apply it now

Classify every date field in one workflow as date, local time, or instant. Rename ambiguous fields and include the missing zone where required.

Verify

Test users in two zones, both daylight-saving transitions, leap day, midnight, and a date-only value that must never shift.

9.58 - Keep identifiers as opaque strings

Avoid rounding, parsing, and leaking meaning through record IDs

Problem

A large numeric ID is rounded in JavaScript or application code starts depending on structure that should be private to the server.

Before you start

Define public identifiers as strings even when the database uses numbers. Treat them as labels, not values for arithmetic or ordering.

Submit the identifier unchanged

<form ng-post="/api/invitations" on-error="errors = $res">
  <input
    name="accountId"
    type="hidden"
    value="acct_9007199254740993"
  />
  <label>Email <input name="email" type="email" required /></label>
  <button type="submit">Invite</button>
</form>

Server contract

Accept and return the identifier as a string. Validate its syntax, then authorize the referenced resource independently. An unguessable ID is not permission.

Failure path

Reject malformed IDs without coercing them. Avoid messages that reveal private resource existence when the requester lacks access.

Apply it now

Search one API response for identifier fields typed as numbers. Convert public IDs to strings before they cross the server boundary.

Verify

Round-trip IDs larger than JavaScript’s safe integer, leading-zero IDs, UUIDs, and malformed strings without changing their text.

9.59 - Revalidate a cached response with ETag

Return 304 when unchanged instead of transferring the same representation

Problem

The browser repeatedly downloads a response that rarely changes.

Before you start

Confirm the request is safe and cacheable. Decide whether the representation is public, user-specific, or private before choosing cache headers.

Let HTTP perform revalidation

Return a stable strong or weak ETag with the representation. On a later request, compare If-None-Match and return 304 without a body when it still matches. Normal navigation and standards-based HTTP clients can then reuse cached bytes.

Server contract

Change the ETag whenever the selected representation changes. Send appropriate Cache-Control and Vary headers. Partition authenticated content and never mark private user data as shared public cache content.

Failure path

Do not use 304 when authorization changed or when the selected representation differs by language, encoding, origin, or user and the cache key ignores it.

Apply it now

Use the Network panel to find the largest repeated unchanged read. Add validators at its server renderer before introducing a custom application cache.

Verify

Request once, revalidate, change the record, and revalidate again. Confirm 304 only while the body is unchanged and no private response crosses users.

9.60 - Keep a useful page during planned maintenance

Return a clear 503 response while preserving public navigation and recovery

Problem

A deployment or dependency outage turns the application into an indefinite spinner, proxy error, or misleading authentication failure.

Before you start

Prepare a small server-rendered maintenance page that does not depend on the database or application service being maintained.

Return the correct temporary status

Use 503 Service Unavailable and a realistic Retry-After header. Explain what is unavailable, whether existing data is safe, and when the user should retry. Keep status and support links on dependencies that remain available.

Server contract

Health and load-balancer behavior must remove unhealthy instances without caching the maintenance page as a permanent success. Static assets required by the page must remain reachable or be embedded conservatively.

Failure path

Do not return 200 for a failed application page, redirect repeatedly, or expose deployment details. Avoid automatic browser retries that create a recovery surge.

Apply it now

Open the application with its database unavailable in a production-like setup. Replace the first raw failure with the smallest dependable maintenance response.

Verify

Test planned maintenance, an unexpected dependency outage, crawler behavior, cache headers, recovery, and multiple browser tabs retrying together.

9.61 - Return users safely after sign-in

Preserve a destination without creating an open redirect

Problem

The login flow accepts an arbitrary return URL, allowing attackers to send users from a trusted sign-in page to a malicious site.

Before you start

Prefer a server-side return target stored in the session. If a destination must travel through the form, restrict it to known local routes or an allowlist.

Submit a local destination

<form action="/login" method="post" ng-post="/login" on-error="errors = $res">
  <input name="return_to" type="hidden" value="/account/orders" />
  <label>Email <input name="email" type="email" autocomplete="username" /></label>
  <label>
    Password
    <input name="password" type="password" autocomplete="current-password" />
  </label>
  <button type="submit">Sign in</button>
</form>

Server contract

Resolve the destination against an allowlist or accept only normalized local paths. Reject protocol-relative URLs, encoded scheme tricks, control characters, and destinations outside the application.

Failure path

Use a safe default page when the destination is missing or invalid. Never copy an unchecked query or form value into the redirect response header.

Apply it now

Trace every return, next, redirect, and continue parameter to the server code that validates its final destination.

Verify

Test a valid local path, absolute external URL, //host, encoded separators, backslashes, and nested redirect parameters.

9.62 - Return one safe error shape

Make expected failures predictable without hiding their HTTP meaning

Problem

Every endpoint returns errors differently, so forms contain endpoint-specific parsing and unexpected failures leak internal details.

Before you start

Define a small envelope with a safe message, optional field errors, a stable code, and a request ID. Keep HTTP status as the primary category.

Bind predictable members

<form ng-post="/api/profile" on-error="error = $res">
  <label>
    Display name
    <input name="name" required />
  </label>
  <p ng-if="error.fields.name">{{ error.fields.name }}</p>
  <p ng-if="error.message" role="alert">{{ error.message }}</p>
  <p ng-if="error.requestId">Support code: {{ error.requestId }}</p>
  <button type="submit">Save</button>
</form>

Server contract

Keep the envelope stable across 401, 403, 409, 422, 429, and 5xx, but preserve those statuses. Use machine codes only for behavior the client actually needs; do not mirror exception class names.

Failure path

If an intermediary returns HTML or an empty body, show a generic safe fallback. Error rendering must not throw another exception while parsing an unknown body.

Apply it now

Compare three endpoints’ failure bodies. Replace accidental differences with the smallest common envelope rather than a large universal error object.

Verify

Exercise every documented status plus a proxy HTML error and malformed body. Confirm the page remains usable and exposes no internal data.

9.63 - Make full-page and fragment responses explicit

Let one server route return the representation the request actually needs

Problem

The same URL serves normal navigation and an enhanced region update, but the server sometimes returns a full page where a fragment is expected.

Before you start

Define an explicit request policy using Accept, a documented request header, or separate routes. Do not infer intent from fragile user-agent checks.

Keep normal navigation available

<a
  href="/account/orders"
  ng-get="/account/orders"
  data-target="#orders"
  swap="innerHTML"
>
  Refresh orders
</a>
<section id="orders" aria-live="polite">
  <p>Current orders appear here.</p>
</section>

Server contract

Return a complete document for normal navigation and a valid application-owned fragment for the enhanced request. Set Content-Type, cache variation, and error responses consistently for both representations.

Failure path

Never swap a complete login, maintenance, or proxy page into the region. Detect authentication and server failures by status before rendering the body.

Apply it now

Capture the normal and enhanced requests for one route. Write down exactly which request signal chooses each representation.

Verify

Open the link normally, in a new tab, and through the enhanced interaction. Test signed-out and maintenance responses in all three cases.

9.64 - Render user-authored rich text safely

Sanitize stored markup and keep it separate from application templates

Problem

Users can author formatted content, but inserting their HTML as an AngularTS fragment could compile attacker-controlled directives or executable markup.

Before you start

Choose a maintained allowlist sanitizer and a deliberately small formatting policy. Sanitize on the server before storage or rendering, not only in the browser.

Keep content inside an application-owned shell

<article>
  <header><h1>{{ post.title }}</h1></header>
  <div id="post-body" class="rich-text">
    SERVER_RENDERED_SANITIZED_CONTENT
  </div>
</article>

The shell may use AngularTS bindings. The user-authored body is sanitized content, not a template and not a source of directives.

Server contract

Allow only required elements and attributes, normalize URLs, block executable schemes, and add safe link behavior. Re-sanitize old content when policy defects are fixed.

Failure path

Do not trust HTML because it came from your database, Markdown renderer, staff account, or another service. Stored content can remain dangerous for years.

Apply it now

Trace one rich-text field from input to output and identify the exact sanitizer, policy, version, and test suite protecting it.

Verify

Test scripts, event attributes, SVG, malformed nesting, encoded URLs, CSS escape paths, and ordinary formatting that must remain intact.

9.65 - Add Content Security Policy in report-only mode

Measure executable-content dependencies before enforcing a restrictive policy

Problem

The application has no browser policy limiting where scripts, styles, frames, and connections may come from.

Before you start

Inventory real production resources and choose a report endpoint that removes sensitive URL and user data before storage.

Start by observing

Send Content-Security-Policy-Report-Only from the server. Begin with narrow directives for default-src, script-src, style-src, img-src, font-src, connect-src, frame-ancestors, base-uri, and form-action. Use nonces or hashes for deliberate inline content instead of broad unsafe allowances.

Server contract

Generate a fresh nonce per response when used, attach it only to trusted server scripts, and keep the policy consistent across normal, error, login, and maintenance pages.

Failure path

Do not copy a restrictive example directly into enforcement. Missing origins can break authentication, uploads, workers, WebTransport, or error reporting.

Apply it now

Run report-only policy on one production route, classify every violation, and remove unnecessary resource origins before adding exceptions.

Verify

Exercise every application feature, browser extension noise, errors, and signed-out pages. Enforce only after expected traffic produces no unexplained violations.

9.66 - Harden an authentication session cookie

Keep session credentials out of JavaScript and restrict browser delivery

Problem

Authentication state is readable by browser scripts or sent more broadly than the application requires.

Before you start

Use the server framework’s maintained session implementation. Store only an opaque session identifier in the browser, not authorization state or personal data.

Use Secure, HttpOnly, and an appropriate SameSite value. Keep Path and Domain as narrow as the deployment allows. Rotate the session after sign-in and privilege changes, and expire it on logout.

Server contract

Validate expiry and revocation on every authenticated request. Protect state-changing cookie requests from CSRF. Avoid accepting the same credential in both cookies and URLs.

Failure path

Do not fall back to local storage when a script cannot read an HttpOnly cookie; that unreadability is the protection. A secure cookie still requires XSS defenses.

Apply it now

Inspect the production Set-Cookie response and one authenticated request. Explain every attribute and remove any unnecessary domain or lifetime.

Verify

Test HTTPS, logout, expiry, session rotation, subdomains, cross-site requests, and an attempted document.cookie read.

9.67 - Build a password-manager-friendly sign-in form

Use normal controls and autocomplete values before adding custom behavior

Problem

Custom sign-in controls prevent password managers, keyboard submission, or browser security features from recognizing the form.

Before you start

Use a normal server POST endpoint over HTTPS. Keep labels, control names, and a submit button in the document.

Use standard autocomplete semantics

<form action="/login" method="post" ng-post="/login" on-error="errors = $res">
  <label>
    Email
    <input name="email" type="email" autocomplete="username" required />
  </label>
  <label>
    Password
    <input
      name="password"
      type="password"
      autocomplete="current-password"
      required
    />
  </label>
  <p ng-if="errors.message" role="alert">{{ errors.message }}</p>
  <button type="submit">Sign in</button>
</form>

Server contract

Use a generic authentication error, rate-limit abuse, rotate the session after success, and keep account recovery separate. New-password forms use autocomplete="new-password".

Failure path

Do not disable paste, invent text controls that imitate passwords, reveal whether an account exists, or clear the username after a failed attempt.

Apply it now

Run the sign-in form with a browser password manager and keyboard only. Remove custom behavior that blocks recognition or submission.

Verify

Test saved credentials, multiple accounts, failed login, Enter submission, password reveal controls, and sign-in with JavaScript disabled.

9.68 - Keep secrets out of errors and logs

Return safe diagnostics while preserving enough context to trace failures

Problem

Request bodies, headers, URLs, or exceptions can copy credentials and personal data into browser errors and long-lived logs.

Before you start

Classify credentials and sensitive fields. Define allowlisted log fields instead of trying to redact an unlimited set after serialization.

Show only a safe support reference

<form ng-post="/api/connections" on-error="connectionError = $res">
  <label>API key <input name="apiKey" type="password" required /></label>
  <button type="submit">Connect</button>
  <p ng-if="connectionError" role="alert">
    Connection failed. Support code: {{ connectionError.requestId }}
  </p>
</form>

Server contract

Never return or log the submitted key. Log an allowlisted operation name, safe resource ID, status, duration, and request ID. Redact downstream exception data before it crosses service boundaries.

Failure path

Do not place secrets in query strings, analytics events, breadcrumbs, DOM data attributes, or client-side persistent state. Treat screenshots as exportable data.

Apply it now

Trigger one failed credential submission in a safe environment and follow it through browser tools, proxy logs, application logs, and error reporting.

Verify

Search every captured artifact for the exact submitted secret and related personal values. The search must return nothing.

9.69 - Focus the first field rejected by the server

Move keyboard users directly to a field-level validation problem

Problem

The server returns validation errors below the visible area and keyboard users do not know which control needs attention.

Before you start

Expose important controls with ng-el and return field errors using control names. Focus only after the failed response, not while the user is typing.

Focus a known field after rejection

<form
  ng-post="/account/profile"
  on-error="errors = $res; emailInput.focus()"
>
  <label>
    Email
    <input ng-el="emailInput" name="email" type="email" required />
  </label>
  <p ng-if="errors.email" id="email-error">{{ errors.email }}</p>
  <button type="submit">Save profile</button>
</form>

For several fields, let the response handler choose the first rejected control in document order. Keep the visible message associated with its input.

Server contract

Return stable field names and safe messages with 422. Form-level failures must target a form summary rather than an arbitrary input.

Failure path

Do not steal focus for background refreshes or move it repeatedly as messages render. If the control is hidden, reveal it before focusing.

Apply it now

Submit the longest form from its last field and note where focus remains. Move it to the first actionable server error.

Verify

Use keyboard and screen reader navigation with one error, several errors, a hidden section, and a form-level server failure.

9.70 - Turn an empty result into the next action

Distinguish no data from loading and failure, then explain what to do

Problem

An empty collection renders a blank region that looks broken and gives no path forward.

Before you start

Separate first-use emptiness, filter emptiness, permission limits, loading, and request failure. They require different messages and actions.

Render the empty case deliberately

<section>
  <ul ng-if="projects.length > 0">
    <li ng-repeat="project in projects">{{ project.name }}</li>
  </ul>
  <div ng-if="projects.length === 0">
    <h2>No projects yet</h2>
    <p>Create the first project to organize this work.</p>
    <a href="/projects/new">Create a project</a>
  </div>
</section>

Server contract

Return an empty collection as a successful result. Do not use 404 for a valid collection with no records. Preserve active filters so the empty explanation can offer clearing them when appropriate.

Failure path

Never render the empty state while the request failed or has not completed; doing so falsely tells the user that their data is gone.

Apply it now

Find one blank list and write the exact reason it can be empty. Add the next useful action for that reason.

Verify

Test first use, filters with no matches, permission-limited data, slow loading, request failure, and a non-empty result.

9.71 - Preserve focus when replacing server HTML

Keep the trigger stable or move focus to a deliberate result heading

Problem

A fragment replacement removes the focused element or leaves keyboard users outside the newly displayed content.

Before you start

Choose whether focus should remain on a stable trigger or move to the result. Add a programmatically focusable heading only when moving focus is useful.

Keep the target stable and focus its heading

<section>
  <button
    ng-get="/account/summary"
    data-target="#summary-body"
    swap="innerHTML"
    on-success="summaryHeading.focus()"
  >
    Load summary
  </button>
  <article>
    <h2 ng-el="summaryHeading" tabindex="-1">Account summary</h2>
    <div id="summary-body"><p>Summary not loaded.</p></div>
  </article>
</section>

Server contract

Return only the body fragment so the stable heading and its ng-el reference are not removed. Use valid application-owned HTML.

Failure path

Keep focus on the trigger after failure and announce the error nearby. Do not move focus for silent prefetch or background refreshes.

Apply it now

Run one fragment swap using only the keyboard and observe the active element before and after. Make that transition deliberate.

Verify

Test success, failure, repeated replacement, screen reader announcements, and a user who activates the control with keyboard or pointer.

9.72 - Deliver a feature flag with the server page

Avoid a second request and keep authorization separate from rollout

Problem

The page waits for a second request before deciding which small interface variation to show, causing flicker and inconsistent first rendering.

Before you start

Evaluate the flag on the server using the authenticated request and rollout policy. Treat the result as presentation, never as permission.

Initialize the evaluated result

<section ng-init="flags = { compactCheckout: false }">
  <a ng-if="flags.compactCheckout" href="/checkout/compact">
    Use compact checkout
  </a>
  <a ng-if="!flags.compactCheckout" href="/checkout">
    Continue to checkout
  </a>
</section>

Render the evaluated boolean into the server template. Do not expose targeting rules, experiment secrets, or unrelated flags.

Server contract

Keep both paths authorized and compatible with current data. Record the evaluated variant with operational events needed to compare rollout health.

Failure path

Choose a safe default when flag evaluation fails. Removing a UI path does not make its endpoint unavailable to direct requests.

Apply it now

Find one startup request used only for feature configuration. Move the required evaluated values into the initial server response.

Verify

Test both variants, evaluation failure, direct endpoint access, rollback, cached pages, and users moving between rollout groups.

9.73 - Record who changed sensitive data

Write an audit event in the same server operation as the state change

Problem

A sensitive record changed, but the application cannot explain who changed what, when, or through which operation.

Before you start

Define which domain actions require audit history, who may read it, retention, redaction, and the difference between audit events and diagnostic logs.

Ask for context when the domain needs it

<form ng-post="/api/accounts/42/suspend" on-error="errors = $res">
  <label>
    Reason for suspension
    <textarea name="reason" required></textarea>
  </label>
  <button type="submit">Suspend account</button>
</form>

Server contract

Derive actor, tenant, request ID, time, and affected resource on the server. Record the domain action and safe before-and-after facts in the same transaction or a reliable transactional outbox.

Failure path

Do not trust actor or timestamp fields from the form. Avoid placing credentials, unbounded record snapshots, or unnecessary personal data in the audit event.

Apply it now

Choose one permission, billing, account, or data-export action and prove its state change cannot commit without its required audit event.

Verify

Test success, transaction rollback, retry, impersonation where supported, redaction, retention, and unauthorized audit-log access.

9.74 - Roll out a request contract without breaking old pages

Expand, migrate, and contract server behavior across cached browser versions

Problem

A deployment changes a field or response shape while users still have pages, workers, or tabs running code from the previous release.

Before you start

Measure how long old clients and cached pages remain active. Identify producers, consumers, integrations, queued jobs, and persisted data using the contract.

Use an expand-and-contract rollout

First make the server accept old and new requests. Then deploy clients that use the new shape. Observe adoption and errors. Migrate stored data and background producers. Remove old behavior only after the supported compatibility window.

Server contract

Ignore additive response fields safely, preserve field meaning, and use a new field or endpoint for incompatible semantics. Keep rollback possible throughout rollout.

Failure path

Do not rename or repurpose a field in one deployment, assume all tabs refresh, or couple database migration completion to a single frontend release instant.

Apply it now

Take the next contract change and write its expand, client migration, observation, data migration, and removal steps before implementation begins.

Verify

Run old client against new server, new client against compatible old behavior, mixed workers, rollback, delayed jobs, and a tab kept open across deployment.

9.75 - Find a beginner recipe

Start with inspection, server data, DOM wiring, events, forms, and HTTP

Start here

Work through these recipes in order until you reach the problem you need to solve. Each one assumes only the earlier browser and HTML concepts.

What you needRecipe
Inspect a running pageInspect the application
Find an element’s scopeSelect elements through the scope
Include initial server dataPass data from the server
Change element content or classesUpdate the DOM
Replace an elementReplace DOM
Render server HTML in a regionSwap server HTML
React to stateRun effects
Handle browser and scope eventsHandle events
Submit user inputBuild forms
Call a JSON endpointUse REST services
Keep the page useful without JavaScriptEnhance progressively
Compare the server-driven approachUnderstand the HTMX similarity

Choose the next area

Continue with the forms, HTTP, DOM, navigation, security, or performance finder instead of reading the cookbook from top to bottom.

9.76 - Find a form recipe

Solve validation, uploads, conflicts, multi-step flows, and sensitive input

Build the basic flow

ProblemRecipe
Submit controls and display responsesBuild forms
Render named server errorsShow server validation
Focus a rejected controlFocus the first error
Upload and cancel a fileUpload with cancellation
Search while typingBuild active search
Load one choice from anotherBuild dependent selects
Save several stepsBuild a multi-step form

Protect the write

ProblemRecipe
Protect a cookie-authenticated formSend CSRF proof
Prevent duplicate critical effectsUse an idempotency key
Confirm permanent deletionConfirm deletion
Preserve another person’s editHandle edit conflicts
Handle a uniqueness raceReturn uniqueness errors

Represent input correctly

ProblemRecipe
Submit currencySend money safely
Submit local timeSend dates with zones
Submit several recordsApply a bulk action
Build sign-in controlsSupport password managers
Harden the session formHarden session cookies

9.77 - Find an HTTP recipe

Choose statuses, bodies, caching, retries, fragments, and origin policy

Define the response first

ProblemRecipe
Choose status, body, and content typeDesign an HTTP response
Keep error bodies predictableUse an error envelope
Return a page or a fragmentNegotiate content explicitly
Swap trusted server markupReplace a region with server HTML

Control requests

ProblemRecipe
Recover from a failed readOffer manual retry
Stop duplicate or stale requestsControl repeated requests
Respect a server limitHandle rate limiting
Move slow work out of the requestRun a background job
Avoid retransferring unchanged dataRevalidate with ETag
Stream a generated file to diskUse a browser download

Handle deployment boundaries

ProblemRecipe
Call JSON servicesUse REST services
Serve a temporary outageReturn maintenance mode
Call another originConfigure cross-origin access

9.78 - Find a DOM recipe

Select, update, replace, animate, and focus application-owned elements

Wire elements to scope

ProblemRecipe
Expose an element without queryingSelect elements through scope
Change text and classesUpdate the DOM
Replace a nodeReplace DOM
Replace a region with server markupSwap server HTML

Build common interactions

ProblemRecipe
Edit one record in placeBuild inline editing
Remove a confirmed rowDelete a row
Update before a request completesUse an optimistic update
Load a server-rendered dialogOpen a server dialog

Keep updates usable

ProblemRecipe
Announce and focus request resultsMake requests accessible
Explain an empty collectionDesign an empty state
Keep focus after replacementPreserve focus after swaps

9.79 - Find a security recipe

Protect requests, sessions, redirects, content, credentials, and permissions

Protect server operations

ProblemRecipe
Audit a complete interactionRun the security audit
Protect cookie-authenticated writesSend CSRF proof
Enforce current permissionsHandle permission rejection
Prevent duplicate critical writesUse idempotency
End an authenticated sessionLog out safely

Protect browser content

ProblemRecipe
Validate a return destinationPrevent open redirects
Render user-authored formattingSanitize rich text
Restrict executable resourcesDeploy Content Security Policy
Restrict session deliveryHarden session cookies
Remove credentials from diagnosticsRedact secrets

Protect destructive actions

ProblemRecipe
Confirm permanent removalConfirm deletion
Prefer a reversible operationArchive and restore

9.80 - Find a performance recipe

Reduce requests, rendering, transfer, and main-thread work after measuring

Reduce network work

ProblemRecipe
Avoid repeated unchanged readsCache reference data
Use HTTP validatorsRevalidate with ETag
Delay, throttle, or cancel requestsControl repeated requests
Prefetch before a clickPrefetch a result

Reduce rendering work

ProblemRecipe
Page a large collectionPaginate on the server
Load more near the viewportBuild infinite scrolling
React to visibilityUse viewport observation
Keep simple reference dataUse caching

Move specialized work

ProblemRecipe
Receive incremental server changesUse server-sent events
Move CPU work off the main threadUse a worker
Run measured native computationUse WebAssembly
Move slow server work out of a requestRun a background job

Reduce shipped JavaScript

ProblemRecipe
The measured runtime contains unused featuresBuild a smaller production runtime

9.81 - Find a data correctness recipe

Preserve identity, precision, concurrency, validation, and audit history

Represent values correctly

ProblemRecipe
Keep public IDs unchangedUse opaque identifiers
Preserve currency precisionSubmit money safely
Preserve time meaningSend dates with zones
Seed initial server valuesPass server data

Protect concurrent changes

ProblemRecipe
Detect a stale editHandle edit conflicts
Handle a unique constraint raceReturn uniqueness errors
Make a write repeatableUse idempotency
Report partial batch resultsApply bulk actions

Preserve history and compatibility

ProblemRecipe
Keep useful cached recordsCache reference data
Keep state across an expired sessionHandle session expiration
Record a sensitive changeWrite an audit trail
Change a contract safelyUse a compatible rollout

9.82 - Find a navigation recipe

Keep URLs, server pages, router states, sessions, and redirects coherent

Start with server navigation

ProblemRecipe
Keep forms useful before enhancementUse progressive enhancement
Preserve search in the addressKeep filters in the URL
Preserve list navigationPaginate on the server
Return safely after sign-inValidate redirects

Add browser-owned state where needed

ProblemRecipe
Define application routesUse the router
Enter a state after sign-inHandle authentication
Enter the next saved form stepBuild a multi-step form
Recover from an expired sessionHandle session expiration
Enter a public state after logoutLog out safely

9.83 - Find an operations recipe

Trace failures, survive outages, control rollout, and explain sensitive changes

Diagnose a request

ProblemRecipe
Follow element, scope, request, and DOMDebug an HTTP interaction
Connect an error to server logsShow a request ID
Keep credentials out of telemetryRedact secrets
Check a whole featureRun the shipping check

Operate degraded systems

ProblemRecipe
Tell users when to retryHandle rate limits
Move long work to a worker processRun a background job
Serve a planned outageReturn maintenance mode

Change production safely

ProblemRecipe
Deliver a rollout decisionUse server feature flags
Explain a sensitive state changeRecord an audit trail
Support mixed release versionsRoll contracts out compatibly

9.84 - Find an advanced browser recipe

Use effects, viewport events, streams, workers, WebAssembly, and specialized UI

React to browser and scope changes

ProblemRecipe
Run cleanup-aware reactionsUse effects
Handle browser and scope eventsHandle events
React to element visibilityObserve the viewport
Stream server updatesUse server-sent events
Send and receive live messagesUse WebSocket

Add specialized execution

ProblemRecipe
Move computation off the main threadUse a worker
Call measured native computationUse WebAssembly
Remove unused framework codeBuild a smaller runtime
Cancel a large uploadControl an upload
Call an API on another originConfigure cross-origin access

Build specialized interaction

ProblemRecipe
Append results near the viewportBuild infinite scrolling
Update before the server respondsUse optimistic updates
Load a modal from the serverOpen a server dialog
Render sanitized authored contentHandle rich text

Integrate another framework

ProblemRecipe
Choose an integration boundaryChoose a bridge
Publish transient cross-root messagesUse the event bus
Read reflected web-component stateObserve Fluent UI attributes
Share current reactive stateUse an application model

9.85 - Exchange live messages with WebSocket

Build a bidirectional chat channel with reconnect and cleanup

Problem

The browser must send messages while the server pushes new messages over the same long-running connection. Repeated HTTP requests or SSE only solve half of that conversation.

Before you start

Use WebSocket only when both sides need to send independently. Prefer SSE when the browser only receives updates, and ordinary HTTP when messages are infrequent.

AngularTS exposes WebSocketService, WebSocketConfig, and the managed WebSocketConnection. The managed connection handles message decoding, retry policy, heartbeats, and explicit cleanup.

For a custom production runtime, include websocketModule from @angular-wave/angular.ts/runtime/realtime. Do not include the aggregate realtimeModule unless the same runtime also uses SSE and WebTransport.

Open a chat connection

Keep the connection in one controller or service. Update the scope-owned view model from callbacks and close the connection with the scope.

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

  messages = [];
  draft = '';
  status = 'connecting';

  constructor($websocket, $scope) {
    this.socket = $websocket('wss://example.com/chat', {
      protocols: ['chat.v1'],
      retryDelay: 1_000,
      maxRetries: 10,
      onOpen: () => ($scope.chat.status = 'connected'),
      onMessage: (message) => $scope.chat.messages.push(message),
      onClose: () => ($scope.chat.status = 'reconnecting'),
    });
    $scope.on('$destroy', () => this.socket.close());
  }

  send() {
    const text = this.draft.trim();
    if (!text || this.status !== 'connected') return;
    this.socket.send({ type: 'chat.message', id: crypto.randomUUID(), text });
    this.draft = '';
  }
}

angular.createModule('chat', []).controller('ChatController', ChatController);

send() serializes the object as JSON. Give every client message an ID so the server can ignore a duplicate after a reconnect. Validate the message type, size, authorization, and payload on the server before broadcasting it.

<section ng-controller="ChatController as chat">
  <p>Connection: {{ chat.status }}</p>
  <ol aria-live="polite">
    <li ng-repeat="message in chat.messages">{{ message.text }}</li>
  </ol>
  <form ng-submit="chat.send()">
    <label>Message <input ng-model="chat.draft" /></label>
    <button type="submit" ng-disabled="chat.status !== 'connected'">
      Send
    </button>
  </form>
</section>

Use wss:// from an HTTPS page. At the server or reverse proxy, enable the HTTP upgrade, authenticate the handshake, check the request origin, cap frame and queue sizes, and enforce idle timeouts. Cookies can authenticate the handshake; a short-lived connection ticket is safer than a reusable token in the URL.

Failure path

A reconnect creates a new transport, not a continuation of the old one. The server may have accepted a message before the connection disappeared, so retry only messages with stable IDs and deduplicate them server-side. Resume incoming messages from a server cursor when gaps matter.

Do not queue without a limit while offline. Disable writes or keep a small, visible outbox. When the queue reaches its limit, reject new entries and tell the user instead of allowing memory use to grow.

Apply it now

Choose one interaction where either side must speak without waiting for the other, such as chat, collaborative presence, or live control. Define the message types and IDs first, then add reconnect and resume behavior before building the interface.

Verify

Send a valid message, malformed JSON, an oversized frame, and an unauthorized message. Interrupt the connection before and after server acceptance. Confirm duplicates are ignored, missing messages resume, the offline queue stays bounded, reconnect attempts stop after scope destruction, and the production proxy keeps the upgrade open.

9.86 - Integrate AngularTS with another framework

Choose DOM ownership, messages, observed attributes, or shared reactive state

Problem

An AngularTS module must coexist with a web component or another UI framework without both runtimes fighting over the same DOM and state.

Before you start

Give each framework its own root element. One framework owns rendering and cleanup inside that root. Integrate at the root boundary instead of compiling the same subtree twice.

Choose the smallest bridge

What crosses the boundaryUse
A transient message that something happenedThe event bus
State reflected by a custom element attributeng-observe
Current domain state needed by both sidesA shared model

Use browser custom events directly when one element and its immediate owner are the only participants. Use the event bus when producers and consumers should not know each other. Use a model when consumers need the current value, not only a notification that it changed.

Failure path

Do not use the event bus as a state store, poll DOM attributes that can be observed, or let two frameworks render the same children. Every subscription, observer, and external framework root needs one clear lifecycle owner.

Apply it now

Choose one existing cross-framework callback and classify what crosses the boundary: DOM state, a transient event, or durable current state. Replace the custom glue with the matching bridge.

Verify

Mount and unmount both roots repeatedly. Confirm messages are delivered once, attribute state remains current, models update both consumers, and no listener or observer survives its owner.

9.87 - Send messages across framework roots

Use the application event bus for asynchronous cross-boundary notifications

Problem

A component in another framework needs to notify an AngularTS module without holding its controller, scope, or DOM implementation.

Before you start

Name the event after something that happened, such as product:selected. Keep the payload small and define its shape at the integration boundary.

Publish outside and subscribe inside AngularTS

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

app.controller('SelectionController', [
  '$eventBus',
  '$scope',
  function SelectionController($eventBus, $scope) {
    this.selected = undefined;
    $eventBus.subscribe(
      'product:selected',
      (product) => (this.selected = product),
      $scope,
    );
  },
]);

externalCatalog.onSelected((product) => {
  angular.eventBus.publish('product:selected', product);
});

The injected $eventBus and angular.eventBus expose the same application bus. Delivery is asynchronous through a microtask and listeners run in subscription order. Passing a scope as the subscription context removes the listener when the scope is destroyed and skips queued delivery to that destroyed scope.

Use the right event mechanism

Use scope events for parent and child communication in one scope tree. Use DOM custom events for an element and its owner. Use the event bus for decoupled modules, workers, realtime services, and non-Angular framework roots.

Failure path

The bus does not retain the latest payload and does not own state. A late subscriber does not receive an earlier event. Subscriber failures are sent to the configured exception handler.

Apply it now

Replace one global callback registry with a named event bus topic. Give the subscription a scope owner or call the returned unsubscribe function explicitly.

Verify

Publish before and after subscribing, destroy the owning scope before queued delivery, publish twice, and mount the external root twice. Confirm the expected listener runs once per publication and is removed on teardown.

9.88 - Read Fluent UI custom-element state with ng-observe

Mirror reflected web-component attributes into AngularTS scope

Problem

A custom element owns its internal behavior and reports current state by changing an attribute. AngularTS needs that value without taking over the element’s DOM.

Before you start

Confirm the component reflects the required state to an attribute. ng-observe watches attributes with MutationObserver; it does not poll JavaScript-only properties.

Observe the Fluent UI attribute

This example is taken from the repository’s Fluent UI observe demo.

<section>
  <fluent-select
    ng-observe-current-value="selectedSection"
    title="Select a section"
  >
    <fluent-option value="Beginning">Beginning</fluent-option>
    <fluent-option value="Middle">Middle</fluent-option>
    <fluent-option value="End">End</fluent-option>
  </fluent-select>
  <p>Selected section: {{ selectedSection }}</p>
</section>

ng-observe-current-value="selectedSection" watches the current-value attribute and writes its value to selectedSection on the current scope. If the directive value is omitted, the camel-cased attribute name becomes the scope name: ng-observe-aria-checked writes to ariaChecked.

The same shipped demo observes activeid on fluent-tabs, value on fluent-radio-group, and aria-checked on fluent-switch.

Failure path

Use the exact reflected attribute name documented by the web component. If the component changes only a JavaScript property, use its custom event or an adapter instead. The observer disconnects automatically when the scope is destroyed.

Apply it now

Open the custom element in the Elements panel and interact with it. Identify the attribute that actually changes, then bind only that attribute with ng-observe.

Verify

Check the initial reflected value, change the selection, set the same value again, and destroy the AngularTS root. Confirm scope follows real attribute changes and the observer is disconnected on teardown.

9.89 - Share current state through an application model

Give AngularTS and another framework one reactive domain object

Problem

Two framework roots need the current shopping-cart state. Sending only item:added events makes late consumers reconstruct state and handle missed messages.

Before you start

Choose one owner for the model and its mutations. Retrieve the same registered model from the AngularTS application injector instead of copying it into each framework store.

Register one shopping-cart model

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

shop.model('cart', () => ({
  items: [],
  currency: 'EUR',
}));

shop.controller('CartSummary', [
  'cart',
  function CartSummary(cart) {
    this.cart = cart;
  },
]);

Inject cart into AngularTS controllers, services, or component views. A non-Angular root can retrieve the same instance from the injector associated with the shared application root.

const host = document.querySelector('[ng-app="shop"]');
const cart = angular.getInjector(host).get('cart');

externalProductGrid.onAdd((product) => {
  cart.items.push(product);
});

Nested object and array mutations are reactive. AngularTS interpolation, watchers, repeats, and component views consuming the model receive the updated cart. Framework roots managed by the same application context receive the same named model instance.

Render the shared value

<aside ng-controller="CartSummary as summary">
  <p>Cart items: {{ summary.cart.items.length }}</p>
  <ul>
    <li ng-repeat="item in summary.cart.items">{{ item.name }}</li>
  </ul>
</aside>

Failure path

Do not maintain a second cart and synchronize it with event messages. Do not put view-only details such as open menus or focused controls into the domain model. Use the event bus for transient notifications and the model for current state.

Apply it now

Choose one concept shared across roots, such as cart, session, playback, or workspace selection. Register it once and replace copied stores with that model.

Verify

Add an item from each framework, mount a consumer after items already exist, mutate a nested array, and destroy one root. Confirm every live consumer sees the same cart and destroyed views stop observing it.

9.90 - Build a smaller production runtime

Compose, bundle, measure, and verify only the AngularTS features an application uses

Problem

The production JavaScript bundle is larger than its measured budget even though the application uses only a small part of AngularTS. Importing the full runtime ships directives and services that this page never uses.

Before you start

Optimize the server response, images, fonts, and application code before maintaining a custom runtime. Use this recipe only after a production bundle report shows that unused AngularTS features are material.

createAngular accepts an AngularComposition and returns an AngularRuntime. Its core contains compilation, scopes, controllers, expressions, interpolation, and exception handling. You explicitly add every directive, service, filter, and optional RuntimeModule used by your application.

Create one runtime entry

Create src/runtime.ts. Import package subpaths directly so the bundler can discard modules that are not reachable from this entry.

import { createAngular } from '@angular-wave/angular.ts/runtime';
import { ngControllerDirective } from '@angular-wave/angular.ts/directive/controller';
import { ngEventDirectives } from '@angular-wave/angular.ts/directive/events';

const angular = createAngular({
  directives: {
    ngController: ngControllerDirective,
    ngClick: ngEventDirectives.ngClick,
  },
});

class CounterController {
  count = 0;

  increase() {
    this.count += 1;
  }
}

angular
  .createModule('counterApp', [])
  .controller('CounterController', CounterController);

const root = document.querySelector('#counter-app');
if (!root) throw new Error('Missing counter application root');
angular.bootstrap(root, ['counterApp']);

export { angular };

Register the exact directive names used by the HTML. A custom runtime does not discover ng-click from a template and add it automatically.

<section id="counter-app" ng-controller="CounterController as counter">
  <strong>{{ counter.count }}</strong>
  <button type="button" ng-click="counter.increase()">Increase</button>
</section>

Import optional modules only when the application uses them. For example, add routerModule from @angular-wave/angular.ts/runtime/router for browser-owned routing. Import sseModule, websocketModule, or webTransportModule from @angular-wave/angular.ts/runtime/realtime when only one live transport is needed. Use machineModule or workflowModule from @angular-wave/angular.ts/runtime/orchestration instead of their aggregate when only one orchestration feature is needed. Do not import the package root from this entry; it restores the general runtime and its auto-bootstrap behavior.

Build it with Vite

Point the production build at the runtime entry. Keep property names intact: HTML expressions and dependency-injection tokens refer to them by name.

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    target: 'es2022',
    minify: 'esbuild',
    sourcemap: 'hidden',
    rollupOptions: {
      input: 'src/runtime.ts',
      output: {
        entryFileNames: 'assets/runtime-[hash].js',
      },
    },
  },
});

Run npm exec vite build. Serve the hashed file with a long-lived immutable cache policy and deploy the generated HTML or manifest that references its new hash. Upload the hidden source map to error monitoring, but do not serve it as a public asset unless that is intentional.

Vite’s default minifier does not mangle object properties. If a different minifier is introduced, do not property-mangle controller members, model members, directive attributes, injection tokens, or names read by server HTML. Use static $inject or array injection for every dependency-bearing function.

Measure the artifact

Compare the custom build with the existing production build under the same target, minifier, and compression settings. Record raw, gzip, and Brotli sizes; the compressed transfer size is the useful network budget. For example, run gzip -9 -c dist/assets/runtime-*.js | wc -c after the build.

Set a CI budget only after recording a stable baseline. Fail on unexpected growth, but allow an intentional increase when it removes more application code or improves runtime behavior. Bundle size alone is not a performance result; also measure startup and the interaction that motivated this work.

Failure path

Server-rendered and remotely loaded templates are not visible to the bundler. Inventory their directives explicitly and compile representative responses in tests. If templates or plugins can introduce unknown AngularTS features, ship the standard runtime for that application instead of discovering missing directives in production.

Do not create several nearly identical runtime entries. Share one composition per application architecture and add a feature deliberately when a tested template needs it. A few saved bytes are not worth inconsistent behavior across pages.

Apply it now

Build the current application and record its compressed runtime size. List the directives, filters, services, and optional modules used by production HTML. Create one explicit runtime entry, remove one unused feature group, and compare both size and startup measurements before removing anything else.

Verify

Compile every shipped server fragment and route template with the production runtime. Exercise bootstrap, forms, errors, lazy content, and teardown from the minified artifact. Confirm omitted services fail in a test, source maps resolve in monitoring, the server sends compressed hashed assets, and the CI size budget detects an intentional temporary increase.

9.91 - Add behavior to server-rendered HTML

Enhance one part of a page without building an SPA

Problem

The server already renders a useful page. One part needs client-side behavior, but the page should still work when JavaScript fails or loads slowly.

Recipe

Keep navigation, forms, and initial content in HTML. Put AngularTS on the smallest element that needs it.

<main>
  <h1>Search products</h1>

  <form
    action="/products"
    method="get"
    ng-app="catalog"
    ng-controller="SearchController as search"
  >
    <label for="query">Search</label>
    <input id="query" name="q" ng-model="search.query" />

    <span aria-live="polite" ng-bind="search.message()"></span>
    <button type="submit">Search</button>
  </form>
</main>
const app = angular.createModule('catalog', []);

app.controller('SearchController', function SearchController() {
  this.query = '';

  this.message = () => {
    const count = this.query.trim().length;
    return count === 0 ? 'Enter a search term' : count + ' characters';
  };
});

The form remains a normal GET request. AngularTS adds feedback, but does not own navigation or the initial result page.

Why this works

The server keeps the model, URLs, validation, authorization, and HTML together. The browser handles only immediate feedback.

This gives users useful content before AngularTS starts and leaves a working fallback if the script does not run. It also avoids creating a second model that must stay synchronized with the server.

If the browser must take over

Keep router content server-rendered while the server can still own the model. Move the model into the browser only when a complex feature needs independent client ownership, such as substantial offline work or a long-lived editing workspace.

At that point the browser owns model consistency and compilation. Typed programmatic views are an optional way to type-check model-to-view bindings; HTML templates remain supported. REST services, explicit cache ownership, and a browser security policy become primary at the same point. Before that threshold, prefer normal HTTP forms and server-rendered fragments.

Make that choice per module. A login form can stay as server-rendered HTML while a separate dashboard ng-app owns a browser model. The dashboard can choose a typed programmatic view without requiring the rest of the application to adopt the same view style.

Verify

  1. Load the page with JavaScript disabled.
  2. Submit the form and confirm the server returns results.
  3. Enable JavaScript and confirm the live message appears.
  4. Confirm the same URL and server response are used in both cases.

Avoid

Do not fetch the initial page through AngularTS only to reproduce HTML the server can already send. Use a client-owned route only when the interaction truly needs long-lived browser state. See Keep the application on the server.

9.92 - Make a component view reactive

Fix state changes that do not update a typed component view

Problem

A component changes its controller state, but a value in its component view stays frozen at the initial value.

Recipe

Pass a reader function for values that can change.

import { button, output } from '@angular-wave/angular.ts';

class CounterController {
  count = 0;
}

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

app.component<CounterController>('counter', {
  controller: CounterController,
  view: ({ controller }) => {
    return [
      button(
        {
          type: 'button',
          onclick: () => {
            controller.count += 1;
          },
        },
        'Add',
      ),
      output(() => controller.count),
    ];
  },
});

Use () => controller.count, not controller.count, for the changing child.

Why this works

controller.count reads the value while the view is being created. The view receives one number and has nothing to read later.

() => controller.count gives the runtime a reader. AngularTS tracks the properties read by that function and updates the affected DOM when they change.

The same rule applies to reactive properties:

button(
  {
    disabled: () => !controller.canSubmit,
  },
  'Save',
);

Verify

  1. Click Add several times.
  2. Confirm the existing output node changes.
  3. Inspect the DOM and confirm the component is not rebuilt for each click.

Avoid

Do not wrap constant strings or numbers in functions. Readers are for values that can change. Keep static children static.

9.93 - Keep repeated rows stable

Preserve row DOM when refreshed data represents the same records

Problem

Refreshing a collection replaces every object. ng-repeat then rebuilds rows, which loses focus, selection, local element state, and animation continuity.

Recipe

Reconcile incoming records with the existing objects before replacing the collection.

interface User {
  id: string;
  name: string;
}

function reconcileUsers(current: User[], incoming: User[]): User[] {
  const existing = new Map(current.map((user) => [user.id, user]));

  return incoming.map((next) => {
    const user = existing.get(next.id);

    if (!user) {
      return next;
    }

    Object.assign(user, next);
    return user;
  });
}

Use the reconciled array after a refresh:

controller.users = reconcileUsers(controller.users, response.data);

Render it normally:

<ul>
  <li ng-repeat="user in users">
    <input ng-model="user.name" />
  </li>
</ul>

Why this works

ng-repeat tracks objects by identity. Reusing the object for the same id lets AngularTS reuse that row. New records get new rows, removed records lose their rows, and existing records keep their DOM.

The server ID decides which record is the same. The repeat directive still uses its built-in identity behavior.

Verify

  1. Focus an input in one row.
  2. Refresh the collection with new objects carrying the same IDs.
  3. Confirm focus stays in that row.
  4. Add and remove records and confirm only those rows change.

Avoid

Do not reconcile records that have no stable identity. Do not reuse an object when the server ID changed. For small primitive arrays where row state does not matter, replacing the collection is simpler.

9.94 - Stop hidden UI from doing work

Choose between removing UI and only hiding it

Problem

A closed panel still runs watchers, observers, timers, or network work because CSS hides it without removing it.

Recipe

Use ng-if when closing the UI should destroy it.

<button type="button" ng-click="detailsOpen = !detailsOpen">
  Toggle details
</button>

<account-details ng-if="detailsOpen"></account-details>

Use ng-show only when the DOM and its state must stay alive:

<video-player ng-show="playerOpen"></video-player>

Why this works

ng-if removes the subtree and destroys its child scope. Reopening it creates a fresh subtree.

ng-show changes visibility. The subtree, scope, form state, media state, and watchers stay alive.

Choose based on what should happen while the UI is closed, not on which directive is shorter.

Verify

  1. Open and close the panel repeatedly.
  2. Confirm ng-if runs component cleanup when the panel closes.
  3. Confirm hidden panels no longer issue requests or handle browser events.
  4. If state must survive, confirm the ng-show version preserves it.

Avoid

Do not use ng-if for a control that opens and closes constantly when rebuilding it is expensive. Do not use ng-show for a large inactive subtree that keeps doing work.

9.95 - Ignore stale request results

Keep older responses from replacing newer search results

Problem

A user searches for cat, then quickly searches for caterpillar. The second request finishes first. The slower cat response arrives later and replaces the correct results.

Recipe

Give each request a sequence number. Apply a response only when it belongs to the newest request.

app.controller('SearchController', function SearchController($http) {
  let latestRequest = 0;

  this.results = [];
  this.loading = false;
  this.error = '';

  this.search = async (query) => {
    const request = ++latestRequest;
    this.loading = true;
    this.error = '';

    try {
      const response = await $http.get('/api/search', {
        params: { q: query },
      });

      if (request === latestRequest) {
        this.results = response.data;
      }
    } catch (error) {
      if (request === latestRequest) {
        this.error = 'Search failed';
      }
    } finally {
      if (request === latestRequest) {
        this.loading = false;
      }
    }
  };
});

Why this works

Network responses can arrive in any order. The sequence number records which request currently owns the screen. Older requests may finish, but they cannot change results, errors, or loading state.

This check is still needed when the transport supports cancellation. A request can finish just before cancellation reaches it.

Verify

  1. Delay short-query responses in the browser network tools.
  2. Search for a short term, then immediately search for a longer term.
  3. Confirm only the longer term changes the screen.
  4. Repeat with a failed older request and confirm it does not replace the latest state.

Avoid

Do not use one shared loading boolean without checking which request finished. An older request can otherwise hide the loading indicator for a newer one.

9.96 - Clean up browser resources

Stop listeners, observers, and timers when a view is removed

Problem

A component adds a browser listener or timer. After navigation or ng-if removes the component, the callback still runs and keeps old state in memory.

Recipe

Create the resource inside the component view and register its cleanup at the same time.

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

class ClockController {
  now = new Date();
}

app.component<ClockController>('clock', {
  controller: ClockController,
  view: ({ controller, onDestroy }) => {
    const timer = window.setInterval(() => {
      controller.now = new Date();
    }, 1_000);

    onDestroy(() => {
      window.clearInterval(timer);
    });

    return time(() => controller.now.toLocaleTimeString());
  },
});

For browser listeners, an AbortController keeps several listeners under one cleanup:

const events = new AbortController();

window.addEventListener('resize', updateSize, {
  signal: events.signal,
});

onDestroy(() => events.abort());

Why this works

Setup and cleanup live next to each other, so a later edit is less likely to add one without the other. AngularTS runs the registered disposer when it removes the component view.

Verify

  1. Open and remove the component several times.
  2. Confirm only one timer or listener runs while it is visible.
  3. Confirm no callback runs after removal.
  4. Use the browser memory tools to confirm removed controllers can be collected.

Avoid

Do not store component timers in a global list. Do not wait for a later route to clean up resources created by the current component.

9.97 - Find the slow part of an interaction

Measure whether delay comes from JavaScript, layout, rendering, or the network

Problem

An interaction feels slow, but changing watchers or rendering code without a trace only moves work around.

Recipe

Record one interaction and optimize the longest measured task.

  1. Reproduce the problem with realistic data and a production build.
  2. Open the browser Performance panel.
  3. Start recording immediately before the interaction.
  4. Stop after the screen reaches its final state.
  5. Find the longest main-thread task or network wait.
  6. Expand that task until you can name the function, layout, paint, or request responsible.
  7. Change one cause and record the same interaction again.

Add marks when the trace does not show where application work starts:

performance.mark('results:update:start');
controller.results = nextResults;

requestAnimationFrame(() => {
  performance.mark('results:update:painted');
  performance.measure(
    'results:update',
    'results:update:start',
    'results:update:painted',
  );
});

Read the result in the browser Performance panel or with:

performance.getEntriesByName('results:update');

Why this works

A slow click can come from unrelated causes: request latency, JavaScript, style calculation, layout, paint, image decoding, or too much DOM. Each cause needs a different fix.

Measuring through the next animation frame includes the browser work needed to show the change, not only the assignment.

Verify

Keep the before and after traces. Compare the same data, browser, build mode, and interaction. Accept the change only when the measured delay improves without moving the cost to another step.

Avoid

Do not use development timings as release numbers. Do not remove reactivity, accessibility, or cleanup code because it looks expensive without a trace that shows it is the cause.

9.98 - Render every request state

Show loading, success, empty, and error states without contradictory UI

Problem

A request-driven screen shows stale content during loading, treats an empty result as an error, or leaves the loading indicator visible after failure.

Recipe

Model request status separately from its data.

app.controller('OrdersController', function OrdersController($http) {
  this.status = 'idle';
  this.orders = [];

  this.load = async () => {
    this.status = 'loading';

    try {
      const response = await $http.get('/api/orders');
      this.orders = response.data;
      this.status = this.orders.length === 0 ? 'empty' : 'ready';
    } catch (error) {
      this.status = 'error';
    }
  };
});

Render each state once:

<section aria-live="polite">
  <p ng-if="orders.status === 'loading'">Loading orders...</p>

  <button ng-if="orders.status === 'error'" ng-click="orders.load()">
    Try again
  </button>

  <p ng-if="orders.status === 'empty'">No orders yet.</p>

  <ul ng-if="orders.status === 'ready'">
    <li ng-repeat="order in orders.orders" ng-bind="order.number"></li>
  </ul>
</section>

Why this works

One status value prevents impossible combinations such as loading and error appearing together. Empty data is a successful response, so it gets its own state instead of sharing the error path.

Keeping the last successful data is also valid. If you do that, show a separate refresh indicator and make it clear that the visible data is old.

Verify

Test all four responses:

  1. A delayed success shows loading, then results.
  2. An empty success shows the empty message.
  3. A failure shows retry and no loading indicator.
  4. Retry can move from error through loading to ready or empty.

Avoid

Do not derive loading from orders.length === 0; empty results are not still loading. Do not clear an error only after the next request succeeds; clear it when retry starts.

10 - Routing

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

Routing connects a browser URL to a screen in your application. Begin with the routing overview, then learn states, URLs, transitions, and route maps in that order.

10.1 - State-based routing in AngularTS applications

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

AngularTS ships a full-featured router ported from UI-Router. Unlike simple URL-based routers, it models your application as a state machine: each screen or workflow step is a named state, and navigation means transitioning from one state to another. URLs are a way to enter a state, not the primary concept.

Router naming, inference, and public-type placement follow the shared API ergonomics contract. Throughout the router documentation, a router state is one named navigation node. A route tree is the module-owned hierarchy made from router states. Machine and workflow states are execution modes and are not router states.

Key concepts

The router is built from five interlocking primitives:

  • States — named nodes in the application state tree. Each state can have a URL, template, component, and resolved data.
  • Views — the rendered output of a state, inserted into a ng-view element in the DOM.
  • Resolves — asynchronous data-fetching functions that run before a state is entered.
  • Transitions — the lifecycle of moving between states with hooks you can intercept.
  • URL matching — an optional layer that maps browser URLs onto states and keeps them in sync.

How routing integrates with the module system

The router exposes these injectable services:

TokenTypePurpose
$stateStateServiceNavigate (go), inspect current state (is, includes, current)
$transitionsTransitionsServiceRegister lifecycle hooks (onBefore, onStart, onSuccess, …)
$stateRegistryAdvanced registryRegister and deregister state declarations at runtime

For ordinary app authoring, $state is usually sufficient. $transitions and $stateRegistry are classified as advanced runtime APIs for policy wiring, late-bound navigation policies, and runtime diagnostics.

Setting up the router

Register your states

Call module.router(declaration) with one RouterModuleDeclaration or a readonly forest of declarations. Every route must have a unique name. A single typed forest keeps registration and route-name inference in one source of truth.

const routes = [
  {
    name: 'home',
    url: '/home',
    template: '<h1>Home</h1>',
  },
  {
    name: 'contacts',
    url: '/contacts',
    templateUrl: 'contacts.html',
    controller: 'ContactsCtrl',
  },
  {
    name: 'contacts.detail',
    url: '/:contactId',
    resolve: {
      contact: [
        '$transition$',
        'ContactService',
        function ($transition$, ContactService) {
          return ContactService.get($transition$.params().contactId);
        },
      ],
    },
    templateUrl: 'contact-detail.html',
    controller: 'ContactDetailCtrl',
  },
] as const satisfies readonly ng.RouterModuleDeclaration[];

const app = angular.createModule('app', []).router(routes);
type AppRoutes = ng.RoutesOf<typeof routes>;

router() has no separate public input or provider type. Its generic TDeclaration is inferred from the literal above; it is not a type users must name. The router is part of the built-in runtime, so the module does not need an ng.router dependency.

Add ng-view to your layout

Place ng-view where you want the active state’s template to render. An unnamed ng-view receives the default view.

<!DOCTYPE html>
<html ng-app="app">
  <body>
    <nav>
      <a ng-state="'home'" data-state-active="active">Home</a>
      <a ng-state="'contacts'" data-state-active>Contacts</a>
    </nav>

    <!-- Active state template renders here -->
    <div ng-view></div>
  </body>
</html>

Use $state.go() in controllers or services to perform programmatic navigation.

angular.getModule('app').controller('ContactsCtrl', [
  '$state',
  function ($state) {
    this.viewContact = function (contactId) {
      $state.go('contacts.detail', { contactId: contactId });
    };
  },
]);

The ng-view directive

ng-view is a viewport directive that renders the template and controller for the currently active state. When a transition completes, the view is swapped in. Multiple named views can coexist:

<div ng-view></div>

<!-- Named views -->
<div ng-view="header"></div>
<div ng-view="content"></div>
<div ng-view="sidebar"></div>

A state targets named views through its views property:

  name: 'dashboard',
  views: {
    'header': { template: '<app-header></app-header>' },
    'content': { templateUrl: 'dashboard.html', controller: 'DashboardCtrl' },
    'sidebar': { component: 'DashboardSidebar' }
  }
});

The ng-view directive emits $viewContentLoading before the DOM is rendered and $viewContentLoaded after. It also supports autoscroll and onload attributes.

Router-wide scroll and focus behavior belongs in typed module config. This keeps accessibility and scroll restoration consistent across the whole route tree without repeating settings on every route:

angular.createModule('demo', []).config({
  $router: {
    scroll: { top: 0 },
    focus: '[data-route-focus]',
  },
});

Use scroll: true or scroll: "top" to reset to the top after every successful navigation, scroll: "preserve" to leave the current position alone, or scroll: { selector: "#content" } to scroll a specific element into view. Use focus: true for the default focus target ([autofocus], [data-router-focus], main, then h1), a selector string, or { selector, preventScroll } for explicit focus behavior.

Executable sample: scroll-focus.html

When $router.viewTransitions is not disabled and the browser exposes document.startViewTransition(), connected routed views commit inside the View Transitions API so CSS can animate the outgoing and incoming route content as one navigation. Detached, ignored, redirected, loading-resume, or explicitly disabled transitions keep the normal synchronous view commit path.

Executable demo: router-view-transition-test.html

Router directives

ng-state

ng-state is the router link primitive. Its value is always an AngularTS expression: quote a static route name and use an identifier or property path for a dynamic route name. A bare identifier is never treated as an implicit route-name string, and ng-state does not use interpolation.

<a ng-state="'home'">Home</a>
<a ng-state="$ctrl.nextRoute">Next</a>
<a
  ng-state="'contacts.detail'"
  ng-state-params="{ contactId: contact.id }"
  data-state-active
>
  Contact {{ contact.name }}
</a>

<a
  ng-state="'users.edit'"
  ng-state-params="{ id: user.id }"
  ng-state-opts="{ inherit: false, reload: true }"
  data-state-active
  data-state-exact
>
  Edit user
</a>
  • data-state-active: track active state (and descendant states).
  • data-state-exact: switch to exact-match behavior.

data-state-active="activeClass" supports both direct and object-based usage in scoped contexts through inherited route context.

ng-state manages data-state-current="true|false" so CSS and scripts can read current-link state directly.

Explore further

States

Define state hierarchies, configure resolves, and navigate with $state.go().

Transitions

Intercept the transition lifecycle with hooks for auth guards, analytics, and more.

URL matching

Configure parameterized URLs, typed parameters, hash mode, and base href.

Resolve

Fetch data asynchronously before a state is entered, with eager and lazy policies.

10.2 - States

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

A state represents a place in an AngularTS application: a page, a nested layout, a modal, or a step in a workflow. States are declared as plain objects and registered through module.router() before bootstrap or $stateRegistry at runtime.

Exact state and router contracts live in TypeDoc:

Route resolves are authored through the resolve property on StateDeclaration. The object and array helper containers are internal typing details and are intentionally not part of the public ng namespace.

Register States

Register most states as one module-owned route tree or forest through module.router().

angular
  .createModule('demo', [])
  .router({
    name: 'home',
    url: '/home',
    component: 'homePage',
  })
  .router({
    name: 'contacts',
    url: '/contacts',
    templateUrl: 'contacts/list.html',
    controller: 'ContactsListCtrl',
    controllerAs: 'vm',
  });

Register states at runtime when a feature is loaded after bootstrap.

angular.getModule('demo').run(["$stateRegistry", ($stateRegistry) => {
  $stateRegistry.register({
    name: 'settings',
    url: '/settings',
    component: 'settingsPage',
  });
}]);

Runtime registration requires the parent state to exist first. If the parent is missing, the state is queued until the parent is registered.

Nest States

Use dot notation or an explicit parent property to create a hierarchy.

angular
  .createModule('demo', [])
  .router({
    name: 'contacts',
    url: '/contacts',
    template: '<div ng-view></div>',
  })
  .router({
    name: 'contacts.list',
    url: '/list',
    templateUrl: 'contacts/list.html',
  })
  .router({
    name: 'contacts.detail',
    url: '/:id',
    templateUrl: 'contacts/detail.html',
  });

Child states inherit the parent URL prefix. A transition to contacts.detail with { id: 42 } produces /contacts/42.

A parent state must provide a ng-view outlet where child views can render.

Use Abstract States

Abstract states cannot be activated directly. Use them to share a URL prefix, resolves, metadata, or layout with child states.

angular
  .createModule('demo', [])
  .router({
    name: 'admin',
    url: '/admin',
    abstract: true,
    template: '<admin-layout ng-view></admin-layout>',
    resolve: {
      currentUser: ['AuthService', (AuthService) => AuthService.currentUser()],
    },
    data: { requiresAuth: true },
  })
  .router({
    name: 'admin.dashboard',
    url: '/dashboard',
    component: 'adminDashboard',
  });

Navigating to admin.dashboard enters both admin and admin.dashboard.

Declare Parameters

URL parameters are parsed from path and query segments.

angular.createModule('demo', []).router({
  name: 'product',
  url: '/products/:category?page&sort',
  component: 'productList',
});

Non-URL parameters belong in the params block.

angular.createModule('demo', []).router({
  name: 'search',
  url: '/search?q',
  params: {
    q: { value: '', squash: true },
    filters: { value: null, type: 'any' },
  },
  component: 'searchPage',
});

Use parameter declarations for defaults, typed values, dynamic params, array params, squashing, inheritance, and raw URL values.

Resolve Data

Resolves fetch or compute data before a state renders. The router waits for required resolves before entering the state.

angular.createModule('demo', []).router({
  name: 'contacts.detail',
  url: '/:contactId',
  resolve: {
    contact: [
      '$transition$',
      'ContactService',
      ($transition$, ContactService) =>
        ContactService.get($transition$.params().contactId),
    ],
    contactHistory: [
      'contact',
      'HistoryService',
      (contact, HistoryService) => HistoryService.forContact(contact.id),
    ],
  },
  templateUrl: 'contact-detail.html',
  controller: [
    '$scope',
    'contact',
    'contactHistory',
    function ($scope, contact, contactHistory) {
      $scope.contact = contact;
      $scope.history = contactHistory;
    },
  ],
});

Use array-style resolves when you need explicit tokens, dependency metadata, or resolve policies.

resolve: [
  {
    token: 'contact',
    deps: ['$transition$', 'ContactService'],
    resolveFn: ($transition$, ContactService) =>
      ContactService.get($transition$.params().contactId),
    policy: { when: 'EAGER', async: 'WAIT' },
  },
];

Use $state.go() for normal application navigation. It supports absolute states, parent-relative states, sibling-relative states, params, and transition options.

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

$state.go('^');

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

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

$state.go('home', {}, { location: 'replace', reload: true });

$state.go() returns a TransitionPromise. When navigation starts immediately, the active Transition is available as .transition; lazy route loading may defer creation of that transition until after the promise has been returned.

Generate links without navigating:

const url = $state.href('contacts.detail', { contactId: 42 });
const absUrl = $state.href(
  'contacts.detail',
  { contactId: 42 },
  { absolute: true },
);

Check active states:

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

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

Route Policies

Prefer policy.navigation for auth/authorization and auth-like policy checks.

policy.transition.canExit handles transition decisions, while policy.transition.dirty centralizes unsaved-change prompts for state leaves.

angular.createModule('editor', []).router({
  name: 'edit',
  url: '/edit',
  component: 'editorPage',
  policy: {
    transition: {
      dirty: {
        when: ({from}) => from.data.dirty,
        prompt: 'Discard unsaved changes?',
        redirectTo: 'discard',
      },
    },
  },
});

In this example, leaving edit prompts for unsaved changes and can redirect to discard when declined. list uses canExit with a transition param to skip the transition in a confirmation flow.

Retry and Fallback

Use retry to control how the router re-runs transiently failing work before the transition continues, and use fallbackTo to route to a recovery state when the final attempt fails.

angular.createModule('demo', []).router({
  name: 'report',
  component: 'reportPage',
  policy: {
    transition: {
      loading: 'loading',
      retry: ({attempt}) => attempt < 2,
      fallbackTo: 'fallback',
    },
  },
  resolve: {report: () => reportApi.load()},
});

transient retries its resolve up to two times (attempt is 0-based in the context), then enters the target only if it eventually resolves. stable shows fallback policy using a static and object target form.

When policy-driven boundaries are declarative, transitions do not require manual $transitions.onBefore/onStart/onError handlers for common retry/fallback behavior.

Use $router config when most routes should share the same transition policy:

angular.createModule('demo', []).config({
  $router: {
    loading: 'loading',
    retry: 2,
    fallbackTo: 'fallback',
    errorBoundary: 'error',
    viewTransitions: true,
  },
});

Route-level policy.transition values override these router-wide defaults for that route subtree. Set loading: false or retry: false on a route when that route should opt out of the default behavior.

Route-level loading/error decisions can be expressed in policy.transition too. Use error to select the boundary for a recoverable transition failure.

angular
  .createModule('editorDemo', [])
  .router({
    name: 'profile',
    url: '/profile',
    template: '<profile/>',
    policy: {
      transition: {
        loading: true,
        errorBoundary: 'error',
      },
    },
  })
  .router({
    name: 'error',
    url: '/error',
    template: '<p>Unable to load this state.</p>',
  });

loading is an executable decision point that currently accepts a boolean, a state target, or an injectable policy.

Loading and error boundaries are UX-level transition concerns. They are evaluated after the inherited security decision is made, so authentication/authorization still happens in $security and route policy decides what loading/error flow the app shows on the router side.

Retention

Use policy.retention for route branches that should deactivate while inactive instead of losing DOM, scope, and component state on every navigation.

angular.createModule('retentionDemo', []).router({
  name: 'workspace',
  abstract: true,
  component: 'workspacePage',
  policy: {
    retention: {mode: 'keep-alive', max: 2, evict: 'lru'},
  },
  children: [
    {name: 'editor', component: 'editorTab'},
    {name: 'preview', component: 'previewTab'},
  ],
});

In this branch, workspace.tabA, workspace.tabB, and workspace.tabC inherit the same retention policy. Leaving a tab deactivates its route tree. Returning to the same retained key reactivates it with the previous scope and DOM state. Because max is 2, entering a third distinct retained tab evicts one cached entry and destroys its retained scope.

Use $router.retention when the entire application or module should share a default retention policy:

angular.createModule('workspaceApp', []).config({
  $router: {
    retention: {
      mode: 'keep-alive',
      max: 5,
      pause: 'schedulers',
      evict: 'lru',
    },
  },
});

Route-level policy.retention overrides the router-wide default for that route subtree. A route can opt out by declaring policy.retention.mode = 'destroy'.

Retention keys default to state name plus route/query params. Use a string key to share a retained instance across params, or an injectable key policy when the cache identity needs app-specific route context.

pause: "schedulers" tells retained route scopes to pause scheduler-bound work while inactive and resume it on reactivation. App-owned model schedulers remain app-context owned; retention applies to route/root-owned work.

Reload the current state or an ancestor subtree through go():

$state.go($state.current, $state.params, { reload: true, inherit: false });
$state.go($state.current, $state.params, {
  reload: 'contacts',
  inherit: false,
});

Executable examples:

10.3 - Routing transitions and lifecycle hooks

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

Every navigation in AngularTS is a Transition—a structured object that describes moving from one state (or set of states) to another. Transitions carry the from-state, the to-state, all parameter values, the tree of entering/exiting/retained states, and the resolve context. You hook into their lifecycle to implement auth guards, loading indicators, analytics, scroll resets, and more.

What is a transition

When you call $state.go('contacts.detail', { contactId: 42 }), the router creates a Transition[ instance. Internally it computes a ](../../../typedoc/classes/Transition.html)TreeChanges object with five paths:

PathDescription
fromAll currently active nodes, from root to the current leaf state
toAll nodes that will be active after the transition
exitingNodes that are active now but will not be active after (in reverse order, deepest first)
retainedNodes that are active both before and after (unchanged)
enteringNodes that are not active now but will be active after (parent first)

The Transition[ exposes these paths through ](../../../typedoc/classes/Transition.html)trans.exiting(), trans.retained(), and trans.entering(), each returning an array of StateDeclaration objects. Use trans.treeChanges(pathname)for the rawPathNode[] arrays.

Each transition has a numeric id and a promise that resolves to the StateDeclaration for the to-state on success, or rejects with an error object on failure.

Transition lifecycle

The router runs hooks in a fixed sequence of phases:

onCreate (synchronous)

Fires during transition construction before the transition is returned. Used internally for view config setup and global state updates. Not available for application hooks.

onBefore (synchronous)

Fires before any state is exited or entered. This is the right place for synchronous guards that should prevent the transition from even starting.

onStart (async)

Fires as the transition starts running—after onBefore is settled but before any states are exited. Eager resolves are fetched here.

onExit (async, deepest first)

Fires for each state being exited, starting with the deepest and moving toward the root.

onRetain (async)

Fires for each state that is being retained (neither entered nor exited).

onEnter (async, shallowest first)

Fires for each state being entered, starting with the parent and moving toward the leaf. Lazy resolves for entering states are fetched before each state’s onEnter.

onFinish (async)

Fires after all onEnter hooks are done. This is the last chance to cancel or redirect before the transition is committed.

onSuccess / onError (synchronous, after commit)

Fires after the transition is fully committed. onSuccess hooks run when the promise resolves; onError hooks run when it rejects. Return values are ignored at this stage.

Registering hooks

All hook registration methods are on the $transitions service (injected as $transitions or typed as ng.TransitionsService). Each method returns a deregistration function.

$transitions.onBefore(
  matchCriteria: HookMatchCriteria,
  callback: (transition: Transition) => HookResult,
  options?: HookRegOptions
): () => void  // deregister function

Hook match criteria

HookMatchCriteria is an object with optional keys to, from, exiting, retained, and entering. Each value is:

  • A state name string or glob ('contacts.**')
  • A function (state, transition) => boolean
  • true to match any state (the default when the key is omitted)
$transitions.onStart({}, callback);

// Matches transitions going to any child of 'admin'
$transitions.onBefore({ to: 'admin.**' }, callback);

// Matches transitions where a specific state is being exited
$transitions.onExit({ exiting: 'contacts.detail' }, callback);

// Matches using a function predicate
$transitions.onStart(
  {
    to: function (state) {
      return state.policy?.navigation?.authenticated === true;
    },
  },
  callback,
);

Hook registration options

  priority: 10,       // higher priority runs first (default: 0)
  bind: myObject,     // `this` inside callback
  invokeLimit: 1      // auto-deregister after N invocations
});

Hook return values (HookResult)

The return value of a hook controls the transition:

Return valueEffect
undefined / trueTransition continues normally
falseTransition is cancelled (aborted)
TargetStateTransition is redirected to the new target
Promise<false>Transition waits for the promise; cancels if it resolves to false
Promise<TargetState>Transition waits for the promise; redirects when it resolves
Rejected PromiseTransition fails with the rejection reason

Note: onSuccess and onError hooks run after the transition is committed. Their return values are ignored—you cannot cancel or redirect from these hooks.

Redirecting from a hook

Return a TargetState created with $state.target():

  // Always redirect 'home' to 'home.dashboard' as a default substate
  return $state.target('home.dashboard');
});

The router internally creates a new transition to the redirect target and chains the promises. If the original transition was triggered by a URL change, the redirect uses location: 'replace' so the original URL is removed from history.

Cancelling a transition

Return false from any hook that runs before onSuccess:

  if (appIsLocked) {
    return false; // abort
  }
});

A cancelled transition is rejected with an internal cancellation error type. The browser URL is reset to the previous location by the built-in updateUrl hook.

Common hook patterns

Authentication guard

This pattern intercepts any transition to a state with a navigation policy and redirects unauthenticated users to the login page.

  .run(['$transitions', '$state', 'AuthService', function ($transitions, $state, AuthService) {

    $transitions.onBefore(
      {
        to: function (state) {
          return state.policy && state.policy.navigation;
        }
      },
      function (transition) {
        if (!AuthService.isAuthenticated()) {
          // Redirect to login, passing the intended destination
          return $state.target('login', {
            redirectTo: transition.to().name
          });
        }
      }
    );

  }]);

Mark states that require authentication:

  name: 'admin.users',
  url: '/users',
  component: 'AdminUsers',
  policy: {
    navigation: {
      authenticated: true
    }
  }
});

Loading indicator

Show a spinner while any transition is in progress:

  .run(['$transitions', '$rootScope', function ($transitions, $rootScope) {

    $transitions.onStart({}, function () {
      $rootScope.isLoading = true;
    });

    $transitions.onSuccess({}, function () {
      $rootScope.isLoading = false;
    });

    $transitions.onError({}, function () {
      $rootScope.isLoading = false;
    });

  }]);

Analytics tracking

Fire a page-view event after every successful navigation:

  .run(['$transitions', 'AnalyticsService', function ($transitions, AnalyticsService) {

    $transitions.onSuccess({}, function (transition) {
      var toState = transition.to();
      AnalyticsService.pageView({
        page: toState.name,
        url: window.location.pathname,
        params: transition.params()
      });
    });

  }]);

Scroll reset

Scroll to the top of the page on each navigation:

  .run(['$transitions', function ($transitions) {

    $transitions.onSuccess({}, function () {
      window.scrollTo(0, 0);
    });

  }]);

Async guard with redirect on failure

Allow users to authenticate mid-transition:

  var AuthService = transition.injector().get('AuthService');

  if (!AuthService.isAuthenticated()) {
    // Return a promise; transition waits for it to settle
    return AuthService.authenticate().catch(function () {
      return $state.target('guest');
    });
  }
});

Transition outcomes

$state.go() is an asynchronous navigation boundary, so it uses normal Promise control flow rather than returning an { ok, status } object. Branch by awaiting the TransitionPromise: fulfillment means navigation completed or was ignored, while rejection carries the router failure. When present, the attached Transition[ records ](../../../typedoc/classes/Transition.html)successanderror() as evidence after settlement.

A transition can finish in one of four states:

ResultDescription
SuccessAll hooks passed, the to-state is now active. transition.success === true.
ErrorA hook threw, returned a rejected promise, or a resolve failed. transition.success === false, transition.error() contains the reason.
IgnoredThe transition targets the same state with the same parameters that are currently active. Treated as successful navigation.
SupersededA newer transition started before this one finished. The older transition is abandoned; the newer one continues.

Redirected transitions are not an error from the caller’s perspective: $state.go() transparently chains to the redirect target’s promise.

The router does not expose a persistence snapshot. Route declarations describe navigation, and retention policy manages live views; neither is a durable machine or workflow snapshot.

Accessing the transition object

The active transition is stored on $router.transition. Inside any hook, the Transition instance is passed as the first argument.

  console.log('Transitioned to:', transition.to().name);
  console.log('From:', transition.from().name);
  console.log('Params:', transition.params());
  console.log('Entering:', transition.entering().map(s => s.name));
  console.log('Exiting:', transition.exiting().map(s => s.name));
  console.log('ID:', transition.id);
});

transition.redirectedFrom() returns the previous transition in a redirect chain. transition.originalTransition() walks the entire chain back to the first transition.

Per-transition hooks

Hooks can also be registered on a specific Transition instance rather than globally on $transitions. These hooks only affect that single transition:

trans.onSuccess({}, function () {
  console.log('This specific transition succeeded.');
});

Warning: Per-transition hooks must be registered before the transition has finished running. Register them synchronously in the same call stack as the navigation, or in an onStart global hook.

Dynamic resolvables in hooks

An onBefore hook can add additional resolve data to the current transition using transition.addResolvable(). The added resolvable is available to hooks and views that run after it:

  transition.addResolvable({
    token: 'requestId',
    resolveFn: function () { return generateRequestId(); }
  });
});

Transition hook ordering: priority and phase

Within a phase (e.g., all onStart hooks), hooks are invoked in descending priority order. The default priority is 0. Built-in hooks like URL update and view activation use priorities in the range 9000–10000, so application hooks with the default priority run before them.

For onExit, hooks are invoked deepest-first (children before parents). For onEnter, hooks are invoked shallowest-first (parents before children). This ordering is fixed regardless of priority.

10.4 - Route maps

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

Route maps are optional TypeScript contracts for applications that want route names, params, and resolves checked at author time. They do not change the runtime router; they describe the route contract that the runtime already enforces.

RouterModuleDeclaration is the only public route-registration shape. router() accepts either one declaration or a readonly array of declarations. TDeclaration in its generated signature captures the exact argument literal for inference; it is not another public type. There is no router<TRouteMap>(...) assertion overload and no public state provider. Put an explicit route map on createModule<TRouteMap>(...) when inference is not the desired boundary.

Use StateService<TRoutes> at injection boundaries where code calls $state.go(...) or $state.href(...).

type AdminRoutes = {
  'admin.users': {
    params: {
      page?: number;
      query?: string;
    };
    resolves: {
      users: { id: string }[];
      total: number;
    };
  };
  'admin.profile': {
    params: {
      userId: string;
    };
  };
};

declare const $state: ng.StateService<AdminRoutes>;

const usersParams: ng.ParamsOf<AdminRoutes, 'admin.users'> = {
  page: 1,
  query: 'active',
};

const usersResolves: ng.ResolvesOf<AdminRoutes, 'admin.users'> = {
  users: [{ id: '1' }],
  total: 1,
};

$state.go('admin.users', usersParams);
$state.go('admin.users');
$state.href('admin.profile', { userId: '42' });

With a route map enabled, TypeScript rejects unknown route names, missing required params, omitted required params, extra params, and invalid resolve maps.

// @ts-expect-error unknown route
$state.go('admin.missing', {});

// @ts-expect-error userId is required
$state.href('admin.profile', {});

// @ts-expect-error profile params cannot be omitted
$state.go('admin.profile');

// @ts-expect-error page is not a profile param
$state.go('admin.profile', { page: 1 });

Derive a route map from router(...)

For module-owned route trees, keep the tree literal as the source of truth and derive the public route map from it.

const adminTree = {
  name: 'admin',
  children: [
    {
      name: 'users',
      url: '/users/{page:int}',
      params: { page: { type: 'int' } },
      resolve: { users: () => [{ id: '1' }] },
    },
  ],
} as const satisfies ng.RouterModuleDeclaration;

angular.createModule('admin', []).router(adminTree);
type AdminRoutes = ng.RoutesOf<typeof adminTree>;
declare const $state: ng.StateService<AdminRoutes>;
$state.go('admin.users', { page: 1 });

RoutesOf infers dot-composed child names, absolute child names, explicit params keys, built-in literal URL parameter value types, custom $router.paramTypes value types, and object-style resolve return values.

Pass the same custom param type map used by $router.paramTypes as the second generic argument. decode return types are used first; if a type has no decode, an is(value): value is T predicate is used as the value type.

Large modules can pin the route contract directly on the module. This keeps later route trees and lazy namespaces route-name aware without exposing provider APIs.

type AdminRoutes = ng.RoutesOf<typeof adminTree>;

angular
  .createModule<AdminRoutes>('admin', [])
  .router(adminTree)
  .lazyState('admin.**', () => import('./admin.routes'));

When the tree is declared inline, router(...) infers a ng.RouterModule<ng.RoutesOf<typeof tree>> return type automatically. Typed router modules check the router(...) tree itself, keep route names checked across chained router(...) calls and lazyState(...) prefixes, and narrow object-style params and resolve declarations to the route contract. Array-style resolves remain available when a route needs explicit resolve metadata such as token, deps, or eager.

If a route map is supplied with angular.createModule<Routes>(...), that explicit module contract is preserved after router(...). This lets a feature module declare one route tree while the route map also includes lazy boundaries or states registered later in the same module.

Use that explicit module contract when states must be registered individually:

type AppRoutes = {
  home: {};
  settings: {};
};

angular
  .createModule<AppRoutes>('app', [])
  .router({ name: 'home', url: '/', component: 'homePage' })
  .router({
    name: 'settings',
    url: '/settings',
    component: 'settingsPage',
  });

The route map is declared once on createModule(). Each router() call is then checked against it. Prefer a single inferred tree or forest when the states are available together, because that avoids repeating route names in a handwritten map.

Typed lazy prefixes accept known route names, parent route prefixes, and the explicit ".**" lazy namespace form. Unknown lazy namespaces are rejected when the module has a route map.

Typed router trees accept known route names and parent prefixes. This lets a module declare an abstract or shell parent such as admin while still rejecting unknown concrete children such as admin.audit when they are not in the module route map.

type Uuid = string & { readonly __uuid: unique symbol };

const paramTypes = {
  uuid: {
    pattern: /[0-9a-f-]{36}/i,
    encode(value: Uuid) {
      return value;
    },
    decode(value: string): Uuid {
      return value as Uuid;
    },
  },
};

const appTree = {
  name: 'item',
  url: '/item/{id:uuid}',
} as const;

angular.createModule('items', []).config({ $router: { paramTypes } }).router(appTree);

type ItemRoutes = ng.RoutesOf<typeof appTree, typeof paramTypes>;
declare const $state: ng.StateService<ItemRoutes>;

$state.go('item', { id: 'b4f6eb70-3d2b-42df-8f14-4c7e8e88c208' as Uuid });

Use ng.Transition<TRoutes, { to: ToName; from: FromName }> when code receives an injected $transition$. params() and params('to') read the destination route params; params('from') can be narrowed when the source route is known.

declare const $transition$: ng.Transition<
  AdminRoutes,
  { to: 'admin.profile'; from: 'admin.users' }
>;

const toParams = $transition$.params();
const fromParams = $transition$.params('from');

toParams.userId.toUpperCase();
fromParams.page?.toFixed();

The same route map should be the source of truth for resolves and routed component inputs. At runtime, routed components already receive resolve values by matching binding names. The route map makes that contract visible to TypeScript without asking application code to touch internal view records.

type AdminUsersRoute = AdminRoutes['admin.users'];
type AdminUsersResolves = ng.ResolvesOf<AdminRoutes, 'admin.users'>;

const usersComponent = {
  bindings: {
    users: '<',
    total: '<',
  },
};

const usersRouteContract: AdminUsersResolves = {
  users: [{ id: '1' }],
  total: 1,
};

const checkedUsersRouteContract =
  usersRouteContract satisfies AdminUsersRoute['resolves'];

void usersComponent;
void checkedUsersRouteContract;

Component contracts at the route boundary

Route maps should be mirrored by app-owned component contracts when you want typed, non-internal binding assertions.

type AdminUsersResolves = ng.ResolvesOf<AdminRoutes, 'admin.users'>;

type AdminUsersBindings = {
  users: AdminUsersResolves['users'];
  total: AdminUsersResolves['total'];
};

function buildUsersViewModel(
  routeResolves: AdminUsersResolves,
): AdminUsersBindings {
  return {
    users: routeResolves.users,
    total: routeResolves.total,
  };
}

The example keeps route resolves and component inputs in one named contract so admin.users consumers can evolve in lockstep. This stays explicit and does not require internal router records.

Lazy route map composition

Lazy route declarations are composed per module rather than centralized in one monolithic map.

type AppCoreRoutes = {
  'app.home': {
    params: {};
  };
};

type LazyAdminRoutes = {
  'admin.users': {
    params: { filter?: string };
    resolves: {
      users: { id: string }[];
    };
  };
};

type AppRoutes = AppCoreRoutes & LazyAdminRoutes;
declare const $state: ng.StateService<AppRoutes>;

const adminHref = $state.href('admin.users', { filter: 'active' });
void adminHref;

This pattern keeps typed navigation available at call sites without forcing a root module to own every lazy route declaration.

Generated docs and namespace shape

The generated public surface exposes route maps as helper contracts:

These types are public authoring helpers only. They are not aliases for built state records, transition internals, retained-view cache entries, or view service records. If a route map needs to include a lazy-loaded module, compose the public route-map types at the TypeScript boundary rather than importing router internals.

ng-state always evaluates its value as an expression. Use a quoted literal route name when the target is static; this keeps examples and editor tooling aligned with the same route-map contract. An unquoted identifier is a scope lookup, not an implicit route-name string. Do not wrap ng-state expressions in interpolation braces.

<a ng-state="'admin.profile'" ng-state-params="{ userId: user.id }">
  Profile
</a>

When a template route is static, mirror that literal route name in the component or docs-example TypeScript boundary. This gives TypeScript the same route-map contract without adding a template compiler.

type StaticNgStateLiteral<
  TRoutes extends ng.RouteMap,
  TName extends Extract<keyof TRoutes, string>,
> = {
  state: TName;
  params: ng.ParamsOf<TRoutes, TName>;
};

const profileLink: StaticNgStateLiteral<AdminRoutes, 'admin.profile'> = {
  state: 'admin.profile',
  params: { userId: '42' },
};

Use an identifier or property-path expression only when the route name is genuinely dynamic. The AngularTS VS Code tooling checks simple TypeScript literal unions such as $ctrl.route: 'admin.profile' | 'admin.users'. A broad string cannot prove a finite target set, so use StateService<TRoutes> in the code that computes that value when it needs author-time validation.

Current boundary:

  • $state.go(...), $state.href(...), params, and resolve maps have executable public type coverage.
  • ng.Transition<TRoutes, { to: ToName; from: FromName }> gives injected $transition$ code typed params(), params('to'), and params('from').
  • ng.RouterModule<TRoutes> lets angular.createModule<TRoutes>(...) and router(...) carry route-name checking into module declarations.
  • ng.RoutesOf<typeof tree> derives a route map from a literal router(...) tree without exposing internal state, transition, or view records.
  • Built-in literal URL parameter types and custom $router.paramTypes value types are inferred from route URL patterns.
  • Editor tooling completes and validates literal ng-state route names and ng-state-params keys in inline templates and templateUrl files. It also validates simple dynamic route-name unions when TypeScript can prove them.

Executable checks:

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

10.5 - URL matching and configuration in AngularTS router

Configure URL parameters, typed path and query params, hash vs HTML5 mode, base href, and custom parameter types in the AngularTS state-based router.

The AngularTS router matches the browser’s URL against registered state declarations and activates the best matching state. URL matching is an optional layer on top of the state machine—states can be navigated to programmatically without any URL involvement—but most applications use URLs to make deep-linking and browser history work correctly.

How URL matching works

When the browser URL changes (or on initial load), the router syncs registered URL rules in priority order and finds the best match using a weighted scoring system. The winning rule’s handler calls $state.go() with the matched state and extracted parameter values.

URL rules are created automatically for every state that has a url property. Rules with the same sort order are ranked by weight so the most specific match wins.

URL parameters

Parameters are declared in the state’s url string. The router uses a UrlMatcher compiled from this pattern to test URLs and extract values.

Path parameters

Use a colon prefix for named path segments:

  name: 'user',
  url: '/users/:userId',
  component: 'UserProfile'
});

// Matches: /users/42 → { userId: '42' }

Use curly braces for parameters with inline type annotations or custom regexps:

  name: 'product',
  url: '/products/{productId:int}',
  component: 'ProductDetail'
});

// Matches: /products/123 → { productId: 123 }  (integer, not string)
// Does NOT match: /products/abc

Custom regexp:

  name: 'article',
  url: '/articles/{slug:[a-z0-9-]+}',
  component: 'Article'
});

Query parameters

Append a ? followed by parameter names. Multiple query params are separated by &:

  name: 'search',
  url: '/search?q&page',
  component: 'SearchResults'
});

// Matches: /search?q=angularts&page=2 → { q: 'angularts', page: '2' }

Typed query params:

  name: 'messages',
  url: '/messages?{before:date}&{after:date}',
  component: 'MessageList'
});

Mixed path and query params:

  name: 'mailbox',
  url: '/messages/:mailboxId?{before:date}&{after:date}',
  component: 'Mailbox'
});

Optional parameters

Give a parameter a default value in the params block and set squash: true to make it optional. When the URL is visited without the parameter, the default value is used. When navigating with the default value, the parameter is omitted from the URL:

  name: 'userList',
  url: '/users/:page',
  params: {
    page: {
      value: '1',   // default value
      squash: true  // remove from URL when value equals default
    }
  },
  component: 'UserList'
});

// /users/    → { page: '1' }  (squashed)
// /users/3   → { page: '3' }

Built-in parameter types

The router ships the following built-in parameter types. Specify the type inline in the URL pattern or in the params block:

string

Default for path parameters. Encodes and decodes as a plain string. Slashes within the value are encoded as ~2F in AngularTS’s patched path type to avoid ambiguity in Angular 1’s $location.

url: '/items/:name';
// { name: 'foo bar' } → /items/foo%20bar

int

Parses URL segments as integers using parseInt. The pattern is /\d+/.

url: '/users/{id:int}';
// /users/42 → { id: 42 }  (number, not string)

bool

Represents boolean values. Encodes true as "1" and false as "0". The pattern matches 1 or 0.

url: '/settings/{darkMode:bool}';
// /settings/1 → { darkMode: true }

json

Encodes arbitrary objects as a JSON string in the URL (URL-encoded). Useful for complex filter objects.

url: '/search?{filters:json}';
// { filters: { status: 'active', role: 'admin' } }
// → /search?filters=%7B%22status%22%3A%22active%22%7D

date

Encodes Date objects as YYYY-MM-DD strings. Parses the string back to a Date on the way in.

url: '/events?{startDate:date}&{endDate:date}';
// { startDate: new Date('2024-01-15') }
// → /events?startDate=2024-01-15

hash

The internal parameter type used for the # (hash/anchor) portion of the URL. Has inherit: false so the hash is not carried forward to child state transitions.

URL configuration with $router

$router config controls global URL matching behavior.

Case sensitivity

URL matching is case-sensitive by default. Allow case-insensitive matching:

angular.getModule('app').config({
  $router: {
    caseInsensitive: true,
  },
});

Strict mode (trailing slashes)

By default, /users/ and /users are distinct. Disable strict mode to treat trailing slashes as equivalent:

angular.getModule('app').config({
  $router: {
    strict: false, // /users/ matches /users
  },
});

Default squash policy

Control the global default for how parameters with default values appear in URLs:

angular.getModule('app').config({
  $router: {
    // 'false' (default): include the default value in the URL
    // 'true': omit default value from URL
    // '~': replace default value with '~' in the URL
    defaultSquash: true,
  },
});

Custom parameter types

Register a custom ParamType before using it in state URL patterns. The type must implement encode, decode, is, equals, and optionally pattern:

angular.createModule('app', []).config({
  $router: {
    paramTypes: {
      intarray: {
        encode: (values) => values.join('-'),
        decode: (value) => value.split('-').map(Number),
        is: (value) => Array.isArray(value) && value.every(Number.isInteger),
        equals: (a, b) =>
          a.length === b.length && a.every((x, i) => x === b[i]),
        pattern: /[0-9]+(?:-[0-9]+)*/,
      },
    },
  },
});

Use the custom type in a state URL:

angular.getModule('app').router({
  name: 'report',
  url: '/reports/{ids:intarray}',
  component: 'Report',
});

// $state.go('report', { ids: [10, 20, 30] }) → /reports/10-20-30
// /reports/10-20-30 → { ids: [10, 20, 30] }

Executable sample: param-types.html

Note: Register custom types in $router.paramTypes before any state that uses them.

Hash mode vs HTML5 mode

AngularTS inherits Angular 1’s $location modes. Configure the mode with module.config({ $location: ... }):

Hash mode (default)

URLs use a hash fragment: http://example.com/app/#/contacts/42. No server configuration is needed; the hash segment is never sent to the server.

angular.getModule('app').config({
  $location: {
    html5Mode: false,
    hashPrefix: '!', // optional: use #!/ instead of #/
  },
});

Generated hrefs are prefixed with # (plus the hash prefix) in hash mode.

HTML5 mode (pushState)

URLs use real paths: http://example.com/contacts/42. The server must return the app’s index.html for all routes.

angular.getModule('app').config({
  $location: {
    html5Mode: {
      enabled: true,
      requireBase: true, // <base href="..."> must be present in <head>
    },
  },
});

In HTML5 mode, generated hrefs use the base path rather than a hash.

Base href

In HTML5 mode, the base href is read from the <base> tag in the document <head>:

<head>
  <base href="/myapp/" />
</head>

If no <base> tag is present, the runtime falls back to window.location.pathname. The base href is used when constructing absolute URLs via $state.href(..., { absolute: true }) and when pushing new history entries.

Reading the current URL

Use $location to read URL components:

$location.getPath(); // "/contacts/42"
$location.getSearch(); // { tab: 'notes' }
$location.getHash(); // "section1"
$location.getUrl(); // "/contacts/42?tab=notes#section1"

Updating the URL

Use $location.setUrl(newUrl) to replace the current URL. The router then syncs the matching state:

$location.setUrl('/contacts/99?tab=history');

URL rule priority and matching weight

When multiple rules could match the same URL, the router scores each match and picks the winner:

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

Exact path matches score higher than prefix matches. Query parameters do not affect path scoring but must all be present if declared without defaults.

Listening for URL changes

Listen for $locationChangeSuccess when code needs URL-level notifications:

const deregister = $rootScope.on('$locationChangeSuccess', () => {
  console.log('URL changed to:', $location.getUrl());
});

// Stop listening
deregister();

When loading states asynchronously, register them before enabling navigation links or calling $state.go() for URL-driven destinations:

angular.getModule('app').run([
  '$stateRegistry',
  '$state',
  function ($stateRegistry, $state) {
    fetch('/api/states')
      .then((r) => r.json())
      .then(function (states) {
        states.forEach((s) => $stateRegistry.register(s));
        $state.go('home');
      });
  },
]);

11 - Animations

Add motion to elements as AngularTS inserts, removes, moves, shows, or hides them.

Start with the animation overview. Use CSS animations for ordinary transitions and JavaScript animations only when the browser animation lifecycle must be controlled directly.

11.1 - CSS-based animations with the AngularTS CSS driver

Write CSS transitions and keyframe animations for AngularTS structural directives using ng-enter, ng-leave, ng-move, and stagger classes.

The CSS animation driver is the default mechanism for animating AngularTS structural directives. When a directive calls $animate.enter(), $animate.leave(), $animate.move(), $animate.addClass(), or $animate.removeClass(), the CSS driver reads the element’s computed transitionDuration and animationDuration after applying the appropriate preparation classes. If it detects a non-zero duration, it manages the full lifecycle: blocking premature transitions, applying active classes after a requestAnimationFrame, listening for transitionend and animationend events, and cleaning up all temporary classes when the animation finishes.

How the CSS driver works

The driver goes through a fixed sequence for every animation:

  1. Preparation classes are added. For a structural event, this means .ng-enter, .ng-leave, or .ng-move. For class-based events, it means .foo-add or .foo-remove.
  2. Transitions are temporarily blocked by applying a large negative transitionDelay inline style. This prevents the browser from computing a transition before the active class is applied.
  3. The driver waits for the next quiet requestAnimationFrame. This forces the browser to flush style calculations, so getComputedStyle() returns accurate timing values.
  4. Active classes are added (e.g., .ng-enter-active). The negative delay is removed at the same time, causing any defined CSS transition to start.
  5. The driver listens for transitionend / animationend. A fallback setTimeout fires at delay + 1.5 * duration to handle browsers that may not fire the event reliably.
  6. Cleanup. All preparation and active classes are removed, inline transition and animation style overrides are reverted, and the AnimateRunner is resolved.

Note: The CSS driver only runs an animation if getComputedStyle() reports a non-zero transitionDuration or animationDuration after the preparation classes are applied. If no duration is detected, the driver skips the animation and immediately resolves the runner.

CSS transitions

The simplest way to animate an element is to define CSS transitions on the preparation classes. The transition must be set on the preparation class (.ng-enter) and the final state on the active class (.ng-enter-active).

.my-element.ng-enter {
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
  opacity: 0;
  transform: translateY(-10px);
}

/* The ending state — applied one rAF later to trigger the transition */
.my-element.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

/* Leave animation — reverse the enter */
.my-element.ng-leave {
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
  opacity: 1;
  transform: translateY(0);
}

.my-element.ng-leave-active {
  opacity: 0;
  transform: translateY(10px);
}

CSS keyframe animations

You can also use @keyframes animations. Define the keyframe animation on the preparation class using the animation shorthand property:

  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes slideOut {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(20px);
  }
}

.my-element.ng-enter {
  animation: slideIn 0.4s ease forwards;
}

.my-element.ng-leave {
  animation: slideOut 0.4s ease forwards;
}

With keyframe animations the active class (.ng-enter-active) is not required to drive the animation, but you can still use it to override or extend properties applied during the active phase.

Class-based transitions (ng-show / ng-hide)

When ng-show or ng-hide adds or removes the ng-hide class, $animate.addClass() and $animate.removeClass() are called internally. The CSS driver applies .ng-hide-add / .ng-hide-add-active for the hide transition and .ng-hide-remove / .ng-hide-remove-active for the show transition.

.my-panel.ng-hide-add {
  transition: opacity 0.25s ease;
  opacity: 1;
}

.my-panel.ng-hide-add-active {
  opacity: 0;
}

/* Fade in when revealed */
.my-panel.ng-hide-remove {
  transition: opacity 0.25s ease;
  opacity: 0;
}

.my-panel.ng-hide-remove-active {
  opacity: 1;
}

The same pattern applies to any class you add or remove via $animate.addClass() or $animate.removeClass(). If you call $animate.addClass(el, 'highlighted'), the driver applies .highlighted-add and .highlighted-add-active.

Example: animating ng-repeat list items

ng-repeat calls $animate.enter() when a new item is added, $animate.leave() when one is removed, and $animate.move() when the list is reordered.

<ul>
  <li class="list-item" ng-repeat="item in items">{{ item.name }}</li>
</ul>
.list-item.ng-leave,
.list-item.ng-move {
  transition: all 0.3s ease;
}

.list-item.ng-enter {
  opacity: 0;
  transform: scale(0.9);
}

.list-item.ng-enter-active {
  opacity: 1;
  transform: scale(1);
}

.list-item.ng-leave {
  opacity: 1;
  transform: scale(1);
}

.list-item.ng-leave-active {
  opacity: 0;
  transform: scale(0.9);
}

.list-item.ng-move {
  opacity: 0.5;
}

.list-item.ng-move-active {
  opacity: 1;
}

Example: slide transition with ng-if

ng-if removes and re-inserts the entire element, so you can animate it with ng-enter / ng-leave transitions:

<div class="drawer" ng-if="drawerOpen">
  <!-- drawer content -->
</div>
  overflow: hidden;
}

.drawer.ng-enter {
  transition: max-height 0.35s ease, opacity 0.35s ease;
  max-height: 0;
  opacity: 0;
}

.drawer.ng-enter-active {
  max-height: 500px;
  opacity: 1;
}

.drawer.ng-leave {
  transition: max-height 0.35s ease, opacity 0.35s ease;
  max-height: 500px;
  opacity: 1;
}

.drawer.ng-leave-active {
  max-height: 0;
  opacity: 0;
}

Staggered animations

When multiple elements animate simultaneously under the same parent — common with ng-repeat — the CSS driver detects a stagger class automatically. Define .ng-enter-stagger with transition-delay and zero transition-duration:

  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0;
  transform: translateY(8px);
}

.list-item.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

/* Stagger: each later item is delayed by 80ms */
.list-item.ng-enter-stagger {
  transition-delay: 0.08s;
  transition-duration: 0s;
}

The driver reads the transitionDelay from .ng-enter-stagger and multiplies it by the item’s index within the batch. The first item starts immediately; each additional item is offset by the stagger delay.

Enter stagger

.list-item.ng-enter-stagger {
  transition-delay: 0.1s;
  transition-duration: 0s;
}

Leave stagger

.list-item.ng-leave-stagger {
  transition-delay: 0.05s;
  transition-duration: 0s;
}

Move stagger

.list-item.ng-move-stagger {
  transition-delay: 0.08s;
  transition-duration: 0s;
}

Performance tips

CSS animations can be expensive when they trigger layout or paint on every frame. Follow these guidelines to keep animations smooth:

Use transform and opacity

transform and opacity are the only properties that browsers can animate entirely on the GPU compositor thread without triggering layout or paint. Prefer these over width, height, top, left, margin, or padding.

Avoid animating box-model properties

Properties like height, padding, and margin force layout recalculation on every frame. Use transform: scaleY() or max-height tricks as alternatives where possible.

Use will-change sparingly

Adding will-change: transform hints to the browser that the element will be animated, creating a new compositor layer. Use it only on elements you know will animate — overuse increases memory consumption.

Limit simultaneous animations

Use targeted animation class names and avoid attaching animation hooks to broad selectors. Animating large numbers of DOM nodes simultaneously causes frame drops on low-powered devices.

.my-element.ng-enter {
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
  opacity: 0;
  transform: translateY(-12px);
  will-change: opacity, transform;
}

.my-element.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

11.2 - JavaScript-based animations with the AngularTS JS driver

Register JavaScript animation handlers with module.animation(), implement enter/leave/move hooks with done callbacks, and use the Web Animations API.

The JavaScript animation driver lets you write fully imperative animations in code. Instead of defining CSS classes, you register a factory function against a CSS class selector. When an element carrying that class goes through a structural or class-based animation event, the driver looks up and invokes the matching handler. This makes the JS driver the right choice when you need precise timing control, want to integrate an animation library such as the Web Animations API, or need to coordinate multiple elements that CSS transitions cannot express.

How the JS driver works

During each animation request, $animate checks registered JavaScript animation factories before falling back to CSS/custom-property animation handling. It inspects the element’s class list against factories registered through module.animation(). If a matching factory is found, it retrieves the singleton handler object from the injector and packages the appropriate lifecycle hook as a runnable operation.

JavaScript animation handlers can define two phases for most events:

  • before* — e.g., beforeAddClass, beforeRemoveClass. Runs synchronously before the DOM change.
  • after* (or the event name itself for enter, move) — runs after the DOM change.

For leave, the hooks are leave (before removal) and afterLeave (after removal). For enter and move, only the after-phase hook is called (named enter / move respectively), because the before-phase does not make sense for elements being inserted.

Note: The JS driver and CSS driver are not mutually exclusive per element, but the animation queue calls invokeFirstDriver() which returns on the first driver that produces a handler. If a JS animation is registered for an element, the CSS driver is skipped for that animation event.

Registering a JS animation

Use module.animation() to associate a factory with a CSS class selector. The selector must begin with .. The factory is an injectable function that returns an object containing lifecycle hook methods.

angular.createModule('app', []).animation('.fade', function () {
  return {
    enter: function (element, done) {
      // animate element in, then call done()
      element.style.opacity = '0';
      requestAnimationFrame(function () {
        element.style.transition = 'opacity 0.3s ease';
        element.style.opacity = '1';
        element.addEventListener('transitionend', function onEnd() {
          element.removeEventListener('transitionend', onEnd);
          done();
        });
      });
    },

    leave: function (element, done) {
      element.style.transition = 'opacity 0.3s ease';
      element.style.opacity = '0';
      element.addEventListener('transitionend', function onEnd() {
        element.removeEventListener('transitionend', onEnd);
        done();
      });
    },
  };
});

The same method can be chained with other module declarations:

angular.createModule('app', []).animation('.slide', function () {
  return {
    enter: function (element, done) {
      /* ... */ done();
    },
    leave: function (element, done) {
      /* ... */ done();
    },
  };
});

Animation lifecycle hooks

The object returned by your factory can implement any combination of these hooks. Unimplemented hooks are simply skipped.

HookSignatureWhen it fires
enter(element, done)After element is inserted into the DOM.
leave(element, done)Before element is removed from the DOM.
afterLeave(element, done)After element is removed from the DOM.
move(element, done)After element is moved to a new position.
beforeAddClass(element, className, done)Before the class is added to the element.
addClass(element, className, done)After the class has been added.
beforeRemoveClass(element, className, done)Before the class is removed.
removeClass(element, className, done)After the class has been removed.
beforeSetClass(element, addedClasses, removedClasses, done)Before the atomic add/remove.
setClass(element, addedClasses, removedClasses, done)After the atomic add/remove.
animate(element, from, to, done)For $animate.animate() calls.

The done callback

Every hook receives a done function as its last argument. You must call done() when the animation finishes — whether that is after a transitionend event, a setTimeout, a Web Animations API finish event, or any other mechanism. If done() is never called, the AnimateRunner associated with this animation will never resolve, blocking any chained work.

  // If you return early (e.g., no animation needed), still call done()
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    done();
    return;
  }
  // Otherwise call done() when the animation actually finishes
  runMyAnimation(element).then(done);
}

You may also return a cleanup function from the hook. This function is invoked if the animation is cancelled before it completes:

  const animation = element.animate(
    [{ opacity: 0 }, { opacity: 1 }],
    { duration: 300, easing: 'ease' }
  );

  animation.onfinish = done;

  // Return a cancel handler
  return function (wasCancelled) {
    if (wasCancelled) {
      animation.cancel();
    }
  };
}

Example: Web Animations API

The Web Animations API provides a clean way to drive animations imperatively. It returns a promise-like Animation object with onfinish and oncancel callbacks.

angular.getModule('app').animation('.pop-in', function () {
  const play = (element, frames, duration, done) => {
    const animation = element.animate(frames, {
      duration,
      easing: 'ease-out',
      fill: 'forwards',
    });
    animation.onfinish = done;
    return (cancelled) => cancelled && animation.cancel();
  };

  return {
    enter(element, done) {
      return play(element, [{ opacity: 0 }, { opacity: 1 }], 350, done);
    },
    leave(element, done) {
      return play(element, [{ opacity: 1 }, { opacity: 0 }], 250, done);
    },
  };
});

Apply the animation by adding the .pop-in class to any element managed by a structural directive:

<div class="panel" ng-if="panelOpen">
  <p>This panel animates in and out with the Web Animations API.</p>
</div>

Example: class-based JS animation

The addClass and removeClass hooks fire when $animate.addClass() or $animate.removeClass() is called — including internally by ng-show and ng-hide. The className argument is the space-separated string of classes being added or removed.

angular.getModule('app').animation('.highlight', function () {
  const animate = (element, from, to, done) => {
    const animation = element.animate(
      [{ backgroundColor: from }, { backgroundColor: to }],
      { duration: 300, fill: 'forwards' },
    );
    animation.onfinish = done;
  };

  return {
    addClass(element, className, done) {
      className === 'active'
        ? animate(element, 'transparent', '#fffbcc', done)
        : done();
    },
    removeClass(element, className, done) {
      className === 'active'
        ? animate(element, '#fffbcc', 'transparent', done)
        : done();
    },
  };
});

Example: coordinating multiple elements

When you need to animate related elements in sequence — for example, a leaving element that exits while an entering element waits — you can share state via closure:

angular.getModule('app').animation('.swap', function () {
  let leaving = Promise.resolve();
  return {
    leave(element, done) {
      let resolve;
      leaving = new Promise((complete) => (resolve = complete));
      const anim = element.animate([{ opacity: 1 }, { opacity: 0 }], {
        duration: 250,
      });
      anim.onfinish = () => {
        resolve();
        done();
      };
    },
    enter(element, done) {
      const start = () => {
        element.animate([{ opacity: 0 }, { opacity: 1 }], {
          duration: 300,
        }).onfinish = done;
      };
      leaving.then(start);
    },
  };
});

Injecting services into animation factories

Animation factory functions participate in dependency injection. List dependencies in the array notation or use $inject:

  return {
    enter: function (element, done) {
      $log.debug('enter animation started');
      // e.g., fetch data to drive animation parameters
      $http.get('/api/animation-config').then(function (response) {
        const duration = response.data.duration || 300;
        element.animate(
          [{ opacity: 0 }, { opacity: 1 }],
          { duration, fill: 'forwards' }
        ).onfinish = done;
      });
    },
  };
}]);

Warning: Long-running asynchronous work inside animation hooks (such as HTTP requests) can make your UI feel sluggish. Prefer pre-fetching animation configuration and caching it rather than fetching it inside a hook on every animation.

Combining JS and CSS animations

If you want the JS driver to apply classes and then let CSS transitions handle the visual animation, you can manipulate classes directly in the hook and detect completion via a transitionend listener. This approach gives you the control of JS hooks with the performance of CSS compositing:

  return {
    enter: function (element, done) {
      element.classList.add('is-entering');

      requestAnimationFrame(function () {
        element.classList.add('is-entering-active');
        element.addEventListener('transitionend', function onEnd(e) {
          if (e.target !== element) return;
          element.removeEventListener('transitionend', onEnd);
          element.classList.remove('is-entering', 'is-entering-active');
          done();
        });
      });
    },
  };
});
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0;
  transform: scale(0.95);
}

.hybrid.is-entering-active {
  opacity: 1;
  transform: scale(1);
}

Tip: When coordinating JS and CSS this way, always use transform and opacity for the animated properties so the browser can run the transition on the compositor thread without layout recalculation.

11.3 - AngularTS animations: CSS and JavaScript drivers overview

Overview of the AngularTS animation system — the $animate service, CSS and JS drivers, structural directive hooks, and class-based transitions.

AngularTS ships a first-class animation system built into the core framework. When you use structural directives such as ng-if, ng-repeat, ng-show, ng-hide, ng-include, or ng-view, the framework automatically coordinates with the $animate service to apply CSS class hooks and invoke registered JavaScript animation handlers at the exact moment DOM changes occur — before and after insertion, removal, or class toggling.

How animations are triggered

Animations in AngularTS are not triggered by calling an animation API directly. Instead, they are a side-effect of normal directive activity. When ng-if removes an element, it calls $animate.leave() internally. When ng-repeat inserts a new item, it calls $animate.enter(). This means you never need to change your directive usage — you only need to provide CSS rules or a registered JavaScript animation for the matching class names.

The $animate service sits between directives and the animation drivers. It queues animation work, deduplicates competing animations on the same element, and dispatches to whichever driver is configured. All animation requests are deferred until after the current digest cycle completes, so DOM changes and class mutations are always applied in a stable, predictable order.

The two animation drivers

AngularTS provides two built-in drivers that are consulted in sequence. The JS driver is checked first; if it returns a handler, the CSS driver is skipped for that element. If no JS handler matches, the CSS driver reads the element’s computed styles to detect transitions or keyframe animations.

CSS driver

Reads transitionDuration, animationDuration, and related computed style properties after applying preparation classes. Handles staggering, delays, and both CSS transitions and @keyframes animations with no JavaScript required.

JS driver

Invokes factory functions registered via module.animation(). Each factory returns an object with lifecycle hooks (enter, leave, move, addClass, removeClass, setClass, animate) that receive a done callback. Suitable for Web Animations API, GSAP, or any imperative animation library.

CSS class hooks

The CSS driver applies a pair of classes for every animation event. The first class (the preparation class) is added immediately; the second class (the active class) is added one requestAnimationFrame later so the browser can compute a transition between the two states. Both classes are removed when the animation completes.

EventPreparation classActive class
enter.ng-enter.ng-enter-active
leave.ng-leave.ng-leave-active
move.ng-move.ng-move-active
addClass foo.foo-add.foo-add-active
removeClass foo.foo-remove.foo-remove-active

During structural animations (enter, leave, move), the element also receives .ng-animate for the full duration of the animation.

For staggered animations — such as list items entering one after another — define a stagger delay class:

.my-list-item.ng-enter-stagger {
  transition-delay: 0.1s;
  transition-duration: 0s;
}

The CSS driver detects .ng-enter-stagger automatically when more than one element is being animated simultaneously under the same parent.

The $animate service API

The $animate service is injectable and provides the full animation API. Every method returns an AnimateRunner that you can use to react to completion or cancel the animation early.

$animate.enter(panel, container);
$animate.leave(panel);
$animate.addClass(panel, 'highlighted');
$animate.setClass(panel, 'active', 'inactive');
$animate.animate(panel, { opacity: 0 }, { opacity: 1 }, 'fade');

Full method signatures

MethodDescription
enter(element, parent?, after?, options?)Insert element into the DOM and trigger an enter animation.
leave(element, options?)Trigger a leave animation, then remove the element.
move(element, parent, after?, options?)Move element within the DOM and trigger a move animation.
addClass(element, className, options?)Add one or more CSS classes with an animation.
removeClass(element, className, options?)Remove one or more CSS classes with an animation.
setClass(element, add, remove, options?)Add and remove classes as a single atomic animation.
animate(element, from, to, className?, options?)Animate from one set of inline styles to another.
cancel(runner)Cancel a running animation; the end state is still applied.

Observing animation completion

Every $animate method returns an animation handle. Use done() when code needs to run after an animation settles, or pass onStart, onDone, and onCancel callbacks in native animation options when the callback belongs to one request.

const handle = $animate.enter(panelEl, hostEl);

handle.done((completed) => {
  console.log('Enter animation settled', completed);
});

Registering JavaScript animations

To register a JavaScript animation, call the module-level .animation() method. The name must be a CSS class selector starting with .:

angular.createModule('app', []).animation('.fade-animation', () => {
  return {
    enter(element, done) {
      element
        .animate([{ opacity: 0 }, { opacity: 1 }], 200)
        .finished.then(done);
    },
    leave(element, done) {
      element
        .animate([{ opacity: 1 }, { opacity: 0 }], 200)
        .finished.then(done);
    },
  };
});

The registered animation is matched against the element’s class list. If the element has .fade-animation when an enter event fires, the enter hook is invoked.

12 - Reference

Find exact directive, service, filter, value, router, animation, and generated TypeScript API contracts.

Reference pages answer exact API questions. If you are learning a concept, start with Get Started or a task guide.

NeedReference
HTML behaviorDirective reference
Injected runtime behaviorService reference
Display transformationsFilters
Injected browser objectsValues
NavigationRouting
MotionAnimations
TypeScript contractsGenerated TypeDoc

Read how to use reference if tokens, signatures, lifecycle notes, or TypeDoc notation are unfamiliar.

12.1 - Find the right documentation

Choose learning, task, API, integration, or troubleshooting material.

Use documentation by intent:

Plural sections such as Directives and Services teach workflows. Singular sections such as Directive and Service are feature reference.

12.2 - How to use the API reference

Read directive syntax, injection tokens, TypeScript signatures, optional values, unions, callbacks, and lifecycle contracts.

Find the API category

An ng-* HTML name is usually a directive. A name injected into a function is a service or value token. A capitalized TypeScript name is a generated type.

Read a TypeScript signature

value?: string means the property is optional and, when supplied, must be a string. string | null means either value is accepted. () => void means a function takes no arguments and returns no useful value. Array<T> means an array whose items have type T.

Check lifecycle and ownership

Before using an API, determine when it is created, when callbacks run, and who releases listeners or resources. Component, directive, scope, route, and application lifetimes are different.

Prefer the public contract

Use exported package members, documented injection tokens, and generated TypeDoc pages. Names marked internal, generated DOM details, and implementation files are not application contracts.

Return to a guide

Once you find the exact signature, use the linked task or concept page to see the API in an application workflow.

12.3 - Error and symptom catalog

Diagnose common AngularTS bootstrap, injection, rendering, routing, and integration failures.

Application does not bootstrap

Check that the bootstrapped module name matches the registered module, the root exists, and AngularTS is loaded once. Do not bootstrap on ng-if, ng-include, or ng-view.

Unknown provider or injection token

Confirm the provider is registered before use, its module is in the dependency graph, and the token spelling is correct. Regenerate language bindings after changing the public namespace.

State changed but the view did not

Read the value through a reactive binding. In typed views, use a reader such as () => controller.value. In keyed collections, call the item reader.

A collection row is recreated

Use stable domain keys. Do not key mutable records by array position when insertion, removal, or sorting is possible.

Confirm an ng-view outlet exists, the state names a registered component or template, and parent/child names match the declaration.

Sanitization or CSP blocks content

Review the boundary instead of disabling security. Identify the source, expected sink, and smallest safe trust policy.

Generated integration files are stale

Run the integration’s generation command and aggregate integration validation. Never hand-edit generated bindings, externs, or parity artifacts.

Browser capability is missing

Detect optional APIs before constructing a service. Select a fallback and show an actionable unsupported state when none exists.

Continue with Troubleshooting.

12.4 - Public feature coverage

Understand how runtime modules map to learning guides and API reference.

Every shipped directive module, service module, filter module, public injection token, and built-in configuration key must map to documentation.

A mapping is either:

  • Dedicated: the page documents that feature directly.
  • Canonical: a broader page owns the feature as part of one workflow.

The mapping is stored in docs/public-feature-docs.json. CI discovers the source surface and fails when a module or public token changes without updated documentation ownership.

Utility, worker, WebAssembly, and transport helpers are documented at their user-facing boundary rather than by internal source file. Generated TypeScript API pages remain the authoritative signature reference; guides explain when and why to use those signatures.

13 - Directive reference

Look up the purpose, syntax, parameters, and examples for each directive shipped by AngularTS.

A directive is an HTML attribute or element that adds framework behavior. If you do not yet know which directive you need, begin with the directive guides. This section is an alphabetical lookup reference.

13.1 - ng-repeat

Complete reference for ng-repeat: iterating arrays and objects, special scope variables, filters, animation, and performance guidance.

ng-repeat instantiates a template for each item in a collection, creating a child scope for every element. It is the primary directive for rendering lists and grids in AngularTS.

Basic syntax

<li ng-repeat="item in items">{{ item.name }}</li>

<!-- Iterate an object (key-value pairs) -->
<tr ng-repeat="(key, value) in user">
  <td>{{ key }}</td>
  <td>{{ value }}</td>
</tr>

ng-repeat

  • Type: expression
  • Required: yes

One of these forms:

  • item in collection — iterate array or array-like
  • (key, value) in object — iterate object properties
  • item in collection | filter:fn — with inline filter

Special scope variables

Every ng-repeat child scope exposes these read-only properties:

$index

  • Type: number

Zero-based position of the item in the collection.

$first

  • Type: boolean

true for the first item ($index === 0).

$last

  • Type: boolean

true for the last item.

$middle

  • Type: boolean

true for items that are neither first nor last.

$even

  • Type: boolean

true when $index is even.

$odd

  • Type: boolean

true when $index is odd.

<li
  ng-repeat="item in items"
  ng-class="{ first: $first, last: $last, odd: $odd }"
>
  {{ $index + 1 }}. {{ item.name }}
</li>

Filtering and sorting

Apply filters inline in the ng-repeat expression:

<li ng-repeat="item in items | filter:searchText">{{ item.name }}</li>

<!-- Filter by object (matches any field) -->
<li ng-repeat="item in items | filter:{ category: 'books' }">
  {{ item.name }}
</li>

<!-- Sort ascending -->
<li ng-repeat="item in items | orderBy:'name'">{{ item.name }}</li>

<!-- Sort descending -->
<li ng-repeat="item in items | orderBy:'-price'">{{ item.name }}</li>

<!-- Limit to first 10 -->
<li ng-repeat="item in items | limitTo:10">{{ item.name }}</li>

<!-- Chain filters -->
<li ng-repeat="item in items | filter:query | orderBy:'name' | limitTo:20">
  {{ item.name }}
</li>

Repeat sibling groups

ng-repeat repeats one host element. Wrap sibling content when one item needs multiple elements:

<div ng-repeat="def in definitions">
  <dt>{{ def.term }}</dt>
  <dd>{{ def.description }}</dd>
</div>

Animating ng-repeat

Add animate to the repeated element. Insertions receive ng-enter, removals receive ng-leave, and reordered nodes receive ng-move:

<button ng-click="items.push({ name: 'New item' })">Add</button>
<button ng-click="items.pop()" ng-disabled="!items.length">Remove</button>
<ul>
  <li ng-repeat="item in items" animate>{{ item.name }}</li>
</ul>

ng-repeat integrates with $animate. Added items receive .ng-enter, removed items .ng-leave, and moved items .ng-move CSS classes:

  transition: opacity 0.3s;
  opacity: 0;
}
.my-list li.ng-enter-active {
  opacity: 1;
}
.my-list li.ng-leave {
  transition: opacity 0.3s;
  opacity: 1;
}
.my-list li.ng-leave-active {
  opacity: 0;
}

Performance guidance

  • Preserve item object identity when updating large collections to maximize DOM reuse.
  • Avoid complex expressions in ng-repeat — compute derived values in the controller.
  • Use limitTo to paginate rather than rendering thousands of items.
  • One-time bind static content: {{ ::item.name }} avoids watches on items that never change.

13.2 - ARIA directives

$aria adds accessibility-supporting defaults for the ARIA attributes that AngularTS manages automatically. It preserves authored ARIA, role, and tabindex attributes and should be treated as framework support for accessible applications, not as a complete WCAG conformance guarantee.

Use module.config({ $aria: ... }) when you need to adjust global ARIA support.

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

app.config({
  $aria: {
    ariaCurrent: true,
    ariaCurrentToken: 'page',
    bindRoleForState: true,
    bindKeydown: true,
    diagnostics: false,
  },
});

ng.AriaConfig checks the standardized aria-current token used by router links. ARIA attributes authored in templates remain native HTML rather than framework-specific TypeScript contracts.

The public attribute-name surface follows the current WAI-ARIA states and properties and the developer-facing MDN ARIA attribute reference.

Use ng-aria-disable on an element when framework-authored ARIA behavior should not apply locally.

Set diagnostics: true in development or tests when you want $aria to warn about common authored mistakes on framework-managed interactive elements: positive tabindex, missing accessible names, missing ARIA reference targets, and interactive elements hidden with aria-hidden="true".

ng-state route links consume $aria defaults. Non-anchor and non-button route links receive role="link", tabindex="0", and Enter/Space keyboard activation when enabled. Links using data-state-active or data-state-exact receive framework-managed aria-current while current unless an authored aria-current value already exists.

Executable examples

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./ng-click-non-native.js";
    </script>
    <style>
      .interactive-card {
        border: 1px solid #cbd5e1;
        border-radius: 6px;
        padding: 12px;
        width: fit-content;
        cursor: pointer;
      }
    </style>
  </head>
  <body ng-app="ariaClickNonNativeDemo">
    <section ng-controller="AriaClickNonNativeCtrl as vm">
      <h2>Non-native ng-click accessibility</h2>
      <p>Count: {{ vm.count }}</p>
      <div class="interactive-card" ng-click="vm.count = vm.count + 1">
        Click or press Enter/Space
      </div>
      <p>
        AngularTS applies framework defaults (`role="button"` and
        `tabindex="0"`), so this non-button element is keyboard accessible without
        manual ARIA wiring.
      </p>
    </section>
  </body>
</html>

Non-native ng-click accessibility

Count: {{ vm.count }}

Click or press Enter/Space

AngularTS applies framework defaults (`role="button"` and `tabindex="0"`), so this non-button element is keyboard accessible without manual ARIA wiring.

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./form-validation.js";
    </script>
    <style>
      .field {
        margin-bottom: 12px;
      }
      .error {
        color: #b91c1c;
      }
      input.ng-invalid.ng-touched {
        border-color: #b91c1c;
      }
    </style>
  </head>
  <body ng-app="ariaFormValidationDemo">
    <section ng-controller="AriaFormValidationCtrl as vm">
      <form name="signupForm" novalidate>
        <div class="field">
          <label>Email</label>
          <input
            name="email"
            type="email"
            ng-model="vm.user.email"
            required
            aria-describedby="email-help"
          />
          <p id="email-help">Enter a valid email to continue.</p>
        </div>

        <div class="field">
          <label>Password</label>
          <input
            name="password"
            type="password"
            ng-model="vm.user.password"
            required
            minlength="8"
          />
        </div>

        <div class="field">
          <button type="button" ng-disabled="signupForm.invalid">Create account</button>
        </div>
      </form>

      <p class="error">
        Form invalid: {{ signupForm.invalid }}
        <br />
        Email invalid: {{ signupForm.email.invalid }}
        <br />
        Password invalid: {{ signupForm.password.invalid }}
      </p>
      <p>
        Notice how `aria-required` and `aria-invalid` are injected by `$aria` when
        form inputs are in their default states.
      </p>
    </section>
  </body>
</html>

Enter a valid email to continue.

Form invalid: {{ signupForm.invalid }}
Email invalid: {{ signupForm.email.invalid }}
Password invalid: {{ signupForm.password.invalid }}

Notice how `aria-required` and `aria-invalid` are injected by `$aria` when form inputs are in their default states.

<!doctype html>
<html>
  <head>
    <script type="module">
      import "../../utils/source-bootstrap.js";
      import "./ng-messages-live.js";
    </script>
    <style>
      .field {
        margin-bottom: 12px;
      }
      .error-box {
        margin-top: 8px;
        padding: 8px 10px;
        border: 1px solid #fecaca;
        background: #fff1f2;
        border-radius: 4px;
        max-width: 360px;
      }
    </style>
  </head>
  <body ng-app="ariaMessagesDemo">
    <section ng-controller="AriaMessagesCtrl as vm">
      <form name="contactForm" novalidate>
        <div class="field">
          <label>Email</label>
          <input
            name="email"
            type="email"
            ng-model="vm.user.email"
            required
            aria-describedby="email-hint"
          />
          <p id="email-hint">Use an address we can message.</p>
          <div
            class="error-box"
            ng-messages="contactForm.email.error"
            ng-show="contactForm.email.dirty || contactForm.submitted"
          >
            <p ng-message="required">Email is required.</p>
            <p ng-message="email">Please provide a valid email.</p>
          </div>
        </div>

        <button type="submit">Submit</button>
      </form>
    </section>
  </body>
</html>

Use an address we can message.

Email is required.

Please provide a valid email.

13.3 - Form directives

API reference for form, ng-form, validation, and ng-messages directives.

AngularTS enhances native HTML forms with a form controller that tracks validity and user interaction state. The form and ng-form directives attach this controller to the scope, and the built-in validators populate it automatically.

Geolocation

The experimental HTML <geolocation> element can write its latest result to ng-model:

<geolocation ng-model="position" ng-el="locationPicker" watch>
  <button type="button">Use my location</button>
</geolocation>

Each native location event replaces position with a plain, serializable GeolocationValue. A failed request clears the model and adds geolocation to the model controller’s error object. Use ng-el when you also need native details such as locationPicker.error or locationPicker.permissionStatus.

The binding is view-to-model only. Changing position does not request browser permission and cannot write to the element’s read-only native position. Attributes such as watch and autolocate keep their browser-defined behavior. Browsers without <geolocation> support display its fallback content.

This element is experimental and has limited browser support. Check the browser compatibility table before relying on it.

form / ng-form

A plain <form> element with a name attribute creates a FormController on the current scope. ng-form does the same but can be used on non-form elements or for nested forms.

<form name="registrationForm">
  <input name="email" ng-model="user.email" required type="email" />
  <button type="submit" ng-disabled="registrationForm.invalid">Register</button>
</form>

The name attribute (here registrationForm) is the scope property under which the controller is available.

FormController properties

valid

  • Type: boolean

true when all child controls are valid.

invalid

  • Type: boolean

true when any child control is invalid.

pristine

  • Type: boolean

true until any control has been changed.

dirty

  • Type: boolean

true after any control has been changed.

submitted

  • Type: boolean

true after the form has been submitted at least once.

pending

  • Type: object

Map of pending async validators. Empty object when none are pending.

error

  • Type: object

Map of validator names to arrays of controls that are failing that validator. E.g., { required: [ctrl1], email: [ctrl2] }.

FormController methods

setPristine()

  • Type: function

Resets the form and all controls to pristine state. Useful after a successful save.

setUntouched()

  • Type: function

Resets the touched state of all controls.

setSubmitted()

  • Type: function

Marks the form as submitted programmatically.

Built-in validators

Apply validators as attributes on <input>, <textarea>, or <select> elements. When validation fails, the validator’s name appears in error.

required

  • Type: none

The field must have a non-empty value. Alias: ng-required="expression" for conditional requirement.

minlength

  • Type: number

Minimum number of characters. Alias: ng-minlength="expression".

maxlength

  • Type: number

Maximum number of characters. Alias: ng-maxlength="expression".

pattern

  • Type: string

JavaScript regex pattern the value must match. Alias: ng-pattern="expression".

min

  • Type: number

Minimum value for type="number" or type="date" inputs.

max

  • Type: number

Maximum value for type="number" or type="date" inputs.

type

  • Type: string

HTML input type. AngularTS adds validators for email, url, number, date, time, week, month, datetime-local.

<form name="userForm">
  <input
    name="username"
    ng-model="user.username"
    required
    minlength="3"
    maxlength="20"
    pattern="[a-zA-Z0-9_]+"
  />

  <input
    name="age"
    ng-model="user.age"
    type="number"
    ng-min="18"
    ng-max="120"
  />
</form>

ng-messages / ng-message

Display validation error messages tied to a field’s error object.

<div ng-messages="userForm.email.error">
  <div ng-message="required">Email is required.</div>
  <div ng-message="email">Please enter a valid email address.</div>
  <div ng-message-default>This field has an error.</div>
</div>

ng-messages

  • Type: expression
  • Required: yes

Expression pointing to an error object (typically formName.fieldName.error).

ng-messages-multiple

  • Type: none

By default, only the first matching message is shown. Add this attribute to show all matching messages simultaneously.

ng-message

  • Type: string
  • Required: yes

The validator key to match against the error object. Shown when error[key] is truthy.

ng-message-exp

  • Type: expression

Same as ng-message but evaluates an expression rather than a string literal.

ng-message-default

  • Type: none

Shown when no other ng-message matches. Useful as a generic fallback.

Complete form example

<form name="signupForm">
  <div>
    <label>Email</label>
    <input name="email" type="email" ng-model="user.email" required />
    <div ng-messages="signupForm.email.error" ng-show="signupForm.email.dirty">
      <div ng-message="required">Required.</div>
      <div ng-message="email">Invalid email format.</div>
    </div>
  </div>

  <div>
    <label>Password</label>
    <input
      name="password"
      type="password"
      ng-model="user.password"
      required
      minlength="8"
    />
    <div
      ng-messages="signupForm.password.error"
      ng-show="signupForm.password.dirty"
    >
      <div ng-message="required">Required.</div>
      <div ng-message="minlength">Must be at least 8 characters.</div>
    </div>
  </div>

  <button type="submit" ng-disabled="signupForm.invalid">Sign Up</button>
</form>

13.4 - ng-app

Bootstrap AngularTS application

Description

Use this directive to auto-bootstrap an AngularTS application. The ng-app directive designates the root element of the application and is typically placed near the root element of the page - e.g. on the <body> or <html> tags.

13.5 - ng-bind

Sync or hydrate textContent of element with an expression

Description

The ng-bind attribute places the text content of the specified HTML element with the value of a given expression, and to update the text content when the value of that expression changes.

Typically, you don’t use ng-bind directly, but instead you use the double curly markup like {{ expression }} which is similar but less verbose.

It is preferable to use ng-bind instead of {{ expression }} if a template is momentarily displayed by the browser in its raw state before it is compiled. Since ng-bind is an element attribute, it makes the bindings invisible to the user while the page is loading.

An alternative solution to this problem would be using the ng-cloak directive.

ng-bind can be modified with a data-lazy data attribute (or shorthand lazy attribute), which will delay update of element content until model is changed. This is useful for rendering server-generated content, while keeping the UI dynamic. In other frameworks, this technieque is known as hydration.

Parameters


ng-bind

  • Type: Expression

  • Restrict: A

  • Element: ANY

  • Priority: 0

  • Description: Expression to evaluate and modify textContent property.

  • Example:

    <div ng-bind="name"></div>
    

Directive modifiers

data-lazy

  • Type: N/A

  • Description: Apply expression once the bound model changes.

  • Example:

    <div ng-bind="name" data-lazy></div>
    <!-- or -->
    <div ng-bind="name" lazy></div>
    

Demo

<section ng-app>
  <!-- Eager bind -->
  <label>Enter name: <input type="text" ng-model="name" /></label><br />
  Hello <span ng-bind="name">I am never displayed</span>!

  <!-- Lazy bind with short-hand `lazy` -->
  <button ng-click="name1 = name">Sync</button>
  <span ng-bind="name1" lazy>I am server content</span>!
</section>

Hello I am never displayed! I am server content!

13.6 - ng-bind-html

Bind an HTML string to an element after the application has sanitized it.

Sets an element’s innerHTML to the evaluated string.

<div ng-bind-html="articleHtml"></div>
$scope.articleHtml = '<strong>Hello</strong> <em>world</em>';

ng-bind-html

  • Type: expression
  • Required: yes

Expression that evaluates to an HTML string. null and undefined clear the element.

Warning: This directive does not sanitize the string. Never bind raw user input or untrusted server content. Sanitize it before assigning it to scope.

13.7 - ng-bind-template

Bind interpolated template text to an element.

Binds a template string that may contain multiple {{ }} interpolation expressions. Useful on attributes or when you need mixed text and expressions in the element’s text content.

<span ng-bind-template="{{ remaining }} of {{ total }} tasks remain"></span>

ng-bind-template

  • Type: string
  • Required: yes

A string literal containing one or more {{ expression }} interpolations. The entire string is interpolated and set as textContent.

13.8 - ng-blur

Handler for blur event

Description

The ng-blur directive allows you to specify custom behavior when an element loses focus.

Parameters


ng-blur

  • Type: Expression

  • Description: Expression to evaluate upon blur event. FocusEvent object is available as $event.

  • Example:

    <div ng-blur="$ctrl.handleBlur($event)"></div>
    

Demo

<section ng-app>
  <input
    type="text"
    ng-blur="count++"
    ng-init="count = 0"
    placeholder="Click or tab away from me"
  />
  Lost focus {{ count }} times
</section>
Lost focus {{ count }} times

13.9 - ng-channel

Subscribe a template to a messaging service topic

Description

Updates element’s content by subscribing to events published on a named channel using $eventBus.

  • If the element does not contain any child elements or templates, the directive will replace the element’s inner HTML with the published value.
  • If the element does contain a template and the published value is an object, the directive will merge the object’s key-value pairs into the current scope, allowing Angular expressions like to be evaluated and rendered.

The directive automatically unsubscribes from the event channel when the scope is destroyed.

Parameters


ng-channel

  • Type: string
  • Description: The name of the channel to subscribe to using $eventBus.

Demo

<div ng-app>
  <!-- With empty node -->
  <div ng-channel="epoch"></div>

  <!-- With template -->
  <div ng-channel="user">Hello {{ user.firstName }} {{ user.lastName }}</div>
</div>

<button
  class="btn btn-dark"
  onclick="angular.eventBus.publish('epoch', Date.now())"
>
  Publish epoch
</button>

<button
  class="btn btn-dark"
  onclick="
    angular.eventBus.publish('user', {
      user: {
        firstName: 'John',
        lastName: 'Smith',
      },
    })
  "
>
  Publish name
</button>
Hello {{ user.firstName }} {{ user.lastName }}

13.10 - ng-class

Dynamically bind one or more CSS classes using expressions.

Description

The ng-class directive allows dynamically setting CSS classes on an HTML element by binding to an expression. The directive supports the following expression types:

  • String — space-delimited class names.
  • Object — keys as class names and values as booleans. Truthy values add the class.
  • Array — containing strings and/or objects as described above.

For complex view state, bind a precomputed class map instead of embedding a long object expression in the template:

<button ng-class="tile.classes"></button>
/**
 * @param {{ state: string, sunk: boolean }} tile
 * @returns {ng.ClassMap}
 */
function tileClasses(tile) {
  return {
    placed: tile.state === 'unit',
    hit: tile.state === 'hit',
    miss: tile.state === 'miss',
    sunk: tile.sunk,
  };
}

When the expression changes:

  • Previously added classes are removed.
  • New classes are added.
  • Duplicate classes are avoided.

Important: Avoid using interpolation ({{ ... }}) in the value of the class attribute together with ng-class. See template and interpolation guidance for details.

Animations

If data-animate attribute is present, the following animations will be applied to the element:

AnimationOccurs
add-classBefore the class is applied to the element
remove-classBefore the class is removed from the element
set-classBefore classes are simultaneously added and removed

ng-class supports standard CSS3 transitions/animations even if they don’t follow $animate service naming conventions.

Parameters


ng-class

  • Type: string | object | array

  • Type alias: ng.ClassValue

  • Description: An expression whose result determines the CSS classes to apply.

  • Example:

    <div ng-class="{ active: isActive, disabled: isDisabled }"></div>
    

Demo

<style>
  .strike {
    text-decoration: line-through;
  }
  .bold {
    font-weight: bold !important;
  }
  .red {
    color: red;
  }
  .has-error {
    color: red;
    background-color: yellow;
  }
  .orange {
    color: orange;
  }
</style>
<section ng-app>
  <p ng-class="{strike: deleted, bold: important, 'has-error': error}">
    Map Syntax Example
  </p>
  <label>
    <input type="checkbox" ng-model="deleted" />deleted (apply "strike" class)
  </label>
  <br />
  <label>
    <input type="checkbox" ng-model="important" />important (apply "bold" class)
  </label>
  <br />
  <label>
    <input type="checkbox" ng-model="error" />error (apply "has-error" class)
  </label>
  <hr />
  <p ng-class="style">Using String Syntax</p>
  <input
    type="text"
    ng-model="style"
    placeholder="Type: bold strike red"
    aria-label="Type: bold strike red"
  />
  <hr />
  <p ng-class="[style1, style2, style3]">Using Array Syntax</p>
  <input
    ng-model="style1"
    placeholder="Type: bold, strike or red"
    aria-label="Type: bold, strike or red"
  /><br />
  <input
    ng-model="style2"
    placeholder="Type: bold, strike or red"
    aria-label="Type: bold, strike or red 2"
  /><br />
  <input
    ng-model="style3"
    placeholder="Type: bold, strike or red"
    aria-label="Type: bold, strike or red 3"
  /><br />
  <hr />
  <p ng-class="[style4, {orange: warning}]">Using Array and Map Syntax</p>
  <input
    ng-model="style4"
    placeholder="Type: bold, strike"
    aria-label="Type: bold, strike"
  />
  <br />
  <label
    ><input type="checkbox" ng-model="warning" /> warning (apply "orange"
    class)</label
  >
</section>

Map Syntax Example




Using String Syntax


Using Array Syntax





Using Array and Map Syntax



13.11 - ng-click

Handler for click event

Description

The ng-click directive allows you to specify custom behavior when an element is clicked.

Parameters


ng-click

  • Type: Expression

  • Restrict: A

  • Element: ANY

  • Priority: 0

  • Description: Expression to evaluate upon click event. PointerEvent object is available as $event.

  • Example:

    <div ng-click="$ctrl.greet($event)"></div>
    

    Event policy attributes can be added to the same element:

    <button ng-click="$ctrl.submit($event)" data-event-prevent data-event-once>
      Submit
    </button>
    

    data-event-prevent, data-event-stop, data-event-capture, data-event-once, and data-event-passive apply to every event directive on the same element.


Demo

<section ng-app>
  <button class="btn btn-dark" ng-init="count = 0" ng-click="count++">
    Increment
  </button>
  <span> count: {{count}} </span>
</section>
count: {{count}}

13.12 - ng-cloak

Hide interpolated templates

Description

The ng-cloak directive is used to prevent the HTML template from being briefly displayed by the browser in its raw (uncompiled) form while your application is loading. Use this directive to avoid the undesirable flicker effect caused by the HTML template display.

The directive can be applied to the <body> element, but the preferred usage is to apply multiple ng-cloak directives to small portions of the page to permit progressive rendering of the browser view.

ng-cloak works in cooperation with the following CSS rule:

@charset "UTF-8";

[ng-cloak],
[data-ng-cloak],
.ng-cloak,
.ng-hide:not(.ng-hide-animate) {
  display: none !important;
}

.ng-animate-shim {
  visibility: hidden;
}

.ng-anchor {
  position: absolute;
}

CSS styles are available in npm distribution:

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@angular-wave/angular.ts/dist/angular.css"
/>

Example

<style>
  @charset "UTF-8";
  .ng-cloak,
  .ng-hide:not(.ng-hide-animate),
  [data-ng-cloak],
  [ng-cloak] {
    display: none !important;
  }
  .ng-animate-shim {
    visibility: hidden;
  }
  .ng-anchor {
    position: absolute;
  }
</style>
<section ng-app ng-cloak>These tags are invisible {{ hello }}</section>

Demo

These tags are invisible {{ hello }}

13.13 - ng-copy

Handler for copy event

Description

The ng-copy directive allows you to specify custom behavior when an element is copied.

Parameters


ng-copy

  • Type: Expression

  • Description: Expression to evaluate upon copy event. ClipboardEvent object is available as $event.

  • Example:

    <div contenteditable="true" ng-copy="$ctrl.greet($event)">Content</div>
    

Demo

<section ng-app>
  <div class="border p-2" ng-copy="copied = true" contenteditable="true">
    Copy text from this box via Ctrl-C
  </div>
  {{ copied }}
</section>
Copy text from this box via Ctrl-C
{{ copied }}

13.14 - ng-cut

Handler for cut event

Description

The ng-cut directive allows you to specify custom behavior when an element is cut.

Parameters


ng-cut

  • Type: Expression

  • Description: Expression to evaluate upon cut event. ClipboardEvent object is available as $event.

  • Example:

    <div contenteditable="true" ng-cut="$ctrl.onCut($event)">
      Cuttable content
    </div>
    

Demo

<section ng-app>
  <div class="border p-2" ng-cut="cut = true" contenteditable="true">
    Cut text from this box via Ctrl-X
  </div>
  {{ cut }}
</section>
Cut text from this box via Ctrl-X
{{ cut }}


13.15 - ng-dblclick

Handler for dblclick event

Description

The ng-dblclick directive allows you to specify custom behavior when an element is double clicked.

Parameters


ng-dblclick

  • Type: Expression

  • Restrict: A

  • Element: ANY

  • Priority: 0

  • Description: Expression to evaluate upon dblclick event. MouseEvent object is available as $event.

  • Example:

    <div ng-dblclick="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <button class="btn btn-dark" ng-init="count = 0" ng-dblclick="count++">
    Increment
  </button>
  <span> count: {{count}} </span>
</section>
count: {{count}}

13.16 - ng-el

Reference to an element

Description

The ng-el directive allows you to store a reference to a DOM element in the current scope, making it accessible elsewhere in your template or from your controller. The reference is automatically removed if the element is removed from the DOM.

Use ng-el for the common case where you want the native element itself:

<section ng-controller="BoardController as $ctrl">
  <canvas ng-el="$ctrl.boardEl"></canvas>
</section>
function BoardController() {
  this.boardEl = null;
}

Use a bare name for simple scope shorthand:

<canvas ng-el="boardEl"></canvas>

Use a full assignable expression for controller-as or object-path refs:

<canvas ng-el="$ctrl.boardEl"></canvas>
<section ng-el="refs.panel"></section>

For component or directive controller references, use ng-ref instead:

<search-box ng-ref="$ctrl.search"></search-box>

ng-ref-read is only a modifier for ng-ref. It is useful when you need an assignable expression and want to force a specific read target:

<canvas ng-ref="$ctrl.boardEl" ng-ref-read="$element"></canvas>

For simple DOM element references, ng-el is the clearer API.

Parameters


ng-el

  • Type: string (optional)

  • Description: Name of the key under which the element will be stored in scope, or an assignable expression such as $ctrl.boardEl. Bare names are treated as shorthand keys. If omitted, the element’s id attribute will be used.

  • Example:

    <div ng-el="box"></div>
    <div ng-el="$ctrl.box"></div>
    <div id="box" ng-el></div>
    

Demo

<section ng-app>
  <div ng-el="$chesireCat"></div>
  <div ng-el id="response"></div>
  <button
    class="btn"
    ng-el="$button"
    ng-click="
      $chesireCat.innerHTML = '🐱';
      response.innerHTML='That depends a good deal on where you want to get to.';
      $button.hidden = true"
  >
    Which way I ought to go?
  </button>
</section>


13.17 - ng-focus

Handler for focus event

Description

The ng-focus directive allows you to specify custom behavior when an element is focused.

Parameters


ng-focus

  • Type: Expression

  • Description: Expression to evaluate upon focus event. FocusEvent object is available as $event.

  • Example:

    <div ng-focus="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <input
    type="text"
    ng-focus="count++"
    ng-init="count = 0"
    placeholder="Click or tab into me"
  />
  Focused {{ count }} times
</section>
Focused {{ count }} times

13.18 - ng-get

Initiates a GET request

Description

The ng-get directive allows you to fetch content via $http service from a remote URL and insert, replace, or manipulate it into the DOM. For DOM manipulation to work, the response must be HTML content. If the server endpoint returns JSON, the response is available to on-success as $res. JSON never mutates the current scope implicitly, and the swap strategy is ignored.

Example

<section>
  <div ng-get="/json" on-success="person = $res">Get</div>
  <!-- Endpoint returns {name: 'Bob'} -->
  {{ person.name }}
</section>

String error responses use the configured DOM swap. JSON errors are available to on-error as $res and require explicit assignment.

Example

<section>
  <div ng-get="/json" on-error="requestError = $res">Get</div>
  <!-- Endpoint returns 404 with {error: 'Not found'} -->
  {{ requestError.error }}
</section>

Additional options for request and response handling can be modified with attributes provided below.

Parameters


ng-get

  • Type: string

  • Description: A URL to issue a GET request to

  • Example:

    <div ng-get="/example">get</div>
    

Modifiers

data-trigger

  • Type: string

  • Description: Specifies the DOM event for triggering a request (default is click). For a complete list, see UI Events. To eagerly execute a request without user interaction, use the “load” event, which is triggered syntheticaly on any element by the directive linking function. This is in contract to the native load, which executes lazily only for window object and certain resource elements.

  • Example:

    <div ng-get="/example" trigger="mouseover">Get</div>
    

data-latch

  • Type: string

  • Description: Triggers a request whenever its value changes. This attribute can be used with interpolation (e.g., {{ expression }}) to observe reactive changes in the scope.

  • Example:

    <div ng-get="/example" latch="{{ latch }}" ng-mouseover="latch = !latch">
      Get
    </div>
    

data-swap

  • Type: SwapMode

  • Description: Controls how the response is inserted

  • Example:

    <div ng-get="/example" swap="outerHTML">Get</div>
    

data-target

  • Type: selectors

  • Description: Specifies the DOM element where a string response should be rendered.

  • Example:

    <div ng-get="/example" data-target=".test">Get</div>
    <div ng-get="/json" on-success="person = $res">{{ person.name }}</div>
    

data-delay

  • Type: delay

  • Description: Delay request by N millisecond

  • Example:

    <div ng-get="/example" delay="1000">Get</div>
    

data-interval

  • Type: delay

  • Description: Repeat request every N milliseconds

  • Example:

    <div ng-get="/example" interval="1000">Get</div>
    

data-throttle

  • Type: delay

  • Description: Ignores later requests for N milliseconds

  • Example:

    <div ng-get="/example" throttle="1000">Get</div>
    

data-loading

  • Type: N/A

  • Description: Adds a data-loading=“true/false” flag during request lifecycle.

  • Example:

    <div ng-get="/example" data-loading>Get</div>
    

data-loading-class

  • Type: string

  • Description: Toggles the specified class on the element while loading.

  • Example:

    <div ng-get="/example" data-loading-class="red">Get</div>
    

on-success

  • Type: Expression

  • Description: Evaluates expression when request succeeds. Response data is available as a $res property on the scope.

  • Example:

    <div ng-get="/example" on-success="message = $res">Get {{ message }}</div>
    

on-error

  • Type: Expression

  • Description: Evaluates expression when request fails. Response data is available as a $res property on the scope.

  • Example:

    <div ng-get="/example" on-error="errormessage = $res">
      Get {{ errormessage }}
    </div>
    

data-state-success

  • Type: string

  • Description: Name of the state to nagitate to when request succeeds

  • Example:

    <ng-view></ng-view>
    <div ng-get="/example" data-state-success="account">Get</div>
    

data-state-error

  • Type: string

  • Description: Name of the state to nagitate to when request fails

  • Example:

    <ng-view></ng-view>
    <div ng-get="/example" data-state-error="login">Get</div>
    

13.19 - ng-hide

Hide an element when an expression is truthy.

Toggles the CSS display property of an element without removing it from the DOM. The element’s scope is always alive, regardless of visibility.

<div ng-hide="isLoading">Content here</div>

ng-show

  • Type: expression
  • Required: yes

When truthy, the element is visible (removes the ng-hide CSS class). When falsy, the element is hidden.

ng-hide

  • Type: expression
  • Required: yes

Inverse of ng-show. When truthy, the element is hidden.

Animation hooks: .ng-hide-add / .ng-hide-add-active when hiding, .ng-hide-remove / .ng-hide-remove-active when showing.

ng-if vs ng-show/hide

Aspectng-ifng-show / ng-hide
DOM presenceRemoved when falseAlways in DOM
Scope lifetimeDestroyed when falseAlways alive
Child watchersRemoved when falseAlways active
Initial render costOnly when trueAlways rendered
Animation eventsng-enter / ng-leaveng-hide-add / ng-hide-remove

Use ng-if when the content is expensive to render or when you want to prevent hidden content from making network requests.
Use ng-show/hide when toggling frequently and you need instant re-display without re-initiali

13.20 - ng-if

Conditionally add or remove an element from the DOM.

Adds or removes an element from the DOM based on the truthiness of an expression. When the element is removed, its scope and all child scopes are destroyed. When re-added, a fresh scope is created.

<div ng-if="user">Welcome back, {{ user.name }}!</div>

ng-if

  • Type: expression
  • Required: yes

When truthy, the element is rendered. When falsy, the element and its scope are destroyed and removed from the DOM.

Animation hooks: .ng-enter / .ng-enter-active when added, .ng-leave / .ng-leave-active when removed.

Note: ng-if creates a child scope. If you bind ng-model inside an ng-if, write to an object property (obj.field) rather than a primitive to avoid scope shadowing issues.

13.21 - ng-include

Include a template

Description

13.22 - ng-inject

Inject dependencies into scope.

Description

The ng-inject directive injects registered injectables (services, factories, etc.) into the current scope for direct access within templates or expressions. This allows access to application state without having to create intermediary controllers.

When applied to an element, the directive reads a semicolon-separated list of injectables’ names from the ng-inject attribute and attempts to retrieve them from the $injector. Each resolved injectable is attached to the current scope under its corresponding name.

Parameters


ng-inject

  • Type: string

  • Restrict: A

  • Description:
    A semicolon-separated list of injectable’ names to attach to current scope.

  • Example:

    <div ng-inject="userService;accountService"></div>
    

Demo

<section ng-app>
  <div ng-inject="$window"></div>
  {{ $window.document.location }}
</section>
{{ $window.document.location }}

13.23 - ng-keydown

Handler for keydown event

Description

The ng-keydown directive allows you to specify custom behavior when pressing keys, regardless of whether they produce a character value.

Parameters


ng-keydown

  • Type: Expression

  • Description: Expression to evaluate upon keydown event. KeyboardEvent object is available as $event.

  • Example:

    <div ng-keydown="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <input
    type="text"
    ng-keydown="count++"
    ng-init="count = 0"
    placeholder="Click here, then press down a key."
  />
  Keydown {{ count }} times
</section>
Keydown {{ count }} times

13.24 - ng-keyup

Handler for keyup event

Description

The ng-keyup directive allows you to specify custom behavior when releasing keys, regardless of whether they produce a character value.

Parameters


ng-keyup

  • Type: Expression

  • Description: Expression to evaluate upon keyup event. KeyboardEvent object is available as $event.

  • Example:

    <div ng-keyup="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <input
    type="text"
    ng-keyup="count++"
    ng-init="count = 0"
    placeholder="Click here, then press down a key."
  />
  Keyup {{ count }} times
</section>
Keyup {{ count }} times

13.25 - ng-load

Handler for load event

Description

The ng-load directive allows you to specify custom behavior for elements that trigger load event.

Note: there is no guarantee that the browser will bind ng-load directive before loading its resource. Demo below is using a large image to showcase itself.

Parameters


ng-load

  • Type: Expression
  • Description: Expression to evaluate upon load event. Event object is available as $event.
  • Example:
<div>
  <img src="url" ng-load="$ctrl.load($event)" />
</div>

Demo

<section ng-app>
  <img
    ng-load="res = 'Large image loaded'"
    width="150px"
    src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Centroamerica_prehispanica_siglo_XVI.svg/1920px-Centroamerica_prehispanica_siglo_XVI.svg.png"
  />
  {{ res }}
</section>
{{ res }}

13.26 - ng-model

Two-way data binding for form controls.

Binds the value of <input>, <textarea>, <select>, and custom controls to a scope expression.

<textarea ng-model="user.bio"></textarea>
<select ng-model="user.role" ng-options="r for r in roles"></select>
<input type="checkbox" ng-model="user.active">

ng-model

  • Type: expression
  • Required: yes

An assignable AngularTS expression. When the user changes the input, the expression is assigned the new value. When the scope value changes, the input is updated.

Model controller (NgModelController)

When ng-model is applied, AngularTS creates an NgModelController accessible as formName.fieldName on the scope. It exposes:

viewValue

  • Type: any

The value as seen by the user in the input (always a string for text inputs).

modelValue

  • Type: any

The value after parsers have run — what is stored on the scope.

valid

  • Type: boolean

true when all validators pass.

invalid

  • Type: boolean

true when any validator fails.

pristine

  • Type: boolean

true until the user has interacted with this field.

dirty

  • Type: boolean

true after the user has changed the value at least once.

touched

  • Type: boolean

true after the field has received and lost focus.

error

  • Type: object

Map of failing validator names to true. E.g., { required: true, minlength: true }.

Parsers and formatters

ng-model processes values through two pipelines:

  • Parsers (parsers): Convert viewValue → modelValue. Applied on user input. Return undefined to mark invalid.
  • Formatters (formatters): Convert modelValue → viewValue. Applied when scope value changes.
  return {
    require: 'ngModel',
    link: function(scope, el, attrs, ngModel) {
      ngModel.parsers.push(function(value) {
        var n = parseInt(value, 10);
        ngModel.setValidity('integer', !isNaN(n));
        return isNaN(n) ? undefined : n;
      });
    }
  };
});

13.27 - ng-model-options

Configure update timing and validation behavior for ng-model.

Configures how and when ng-model reads and writes its value.

updateOn

  • Type: string

Space-separated list of DOM events that trigger a model update. Default is 'default' (uses the element’s natural update event). Common values: 'blur', 'change', 'keyup', 'default'.

<!-- Only update on blur -->
<input ng-model="name" ng-model-options="{ updateOn: 'blur' }" />

<!-- Update on both blur and custom event -->
<input ng-model="name" ng-model-options="{ updateOn: 'blur myEvent' }" />

debounce

  • Type: number | object

Milliseconds to wait after the last change before updating the model. Can be a number (applies to all events) or an object mapping event names to delays.

<!-- 300ms debounce for all events -->
<input ng-model="search" ng-model-options="{ debounce: 300 }" />

<!-- Different debounce per event -->
<input
  ng-model="search"
  ng-model-options="{ debounce: { default: 300, blur: 0 } }"
/>

allowInvalid

  • Type: boolean

When true, the model is updated even when validators fail. Default is false (model is set to undefined on invalid input).

`time

13.28 - ng-mousedown

Handler for mousedown event

Description

The ng-mousedown directive allows you to specify custom behavior when a mouse is pressed over an element.

Parameters


ng-mousedown

  • Type: Expression

  • Description: Expression to evaluate upon mousedown event. MouseEvent object is available as $event.

  • Example:

    <div ng-mousedown="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <button ng-init="count = 0" ng-mousedown="count++">Press Mouse Down</button>
  Mouse Down {{ count }} times
</section>
Mouse Down {{ count }} times

13.29 - ng-mouseenter

Handler for mouseenter event

Description

The ng-mouseenter directive allows you to specify custom behavior when a mouse enters an element.

Parameters


ng-mouseenter

  • Type: Expression

  • Description: Expression to evaluate upon mouseenter event. MouseEvent object is available as $event.

  • Example:

    <div ng-mouseenter="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <div ng-init="count = 0" ng-mouseenter="count++">Mouse Enter</div>
  Mouse Enter {{ count }} times
</section>
Mouse Enter
Mouse Enter {{ count }} times

13.30 - ng-mouseleave

Handler for mouseleave event

Description

The ng-mouseleave directive allows you to specify custom behavior when an element a mouse leaves entire element.

Parameters


ng-mouseleave

  • Type: Expression

  • Description: Expression to evaluate upon mouseleave event. MouseEvent object is available as $event.

  • Example:

    <div ng-mouseleave="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <div ng-init="count = 0" ng-mouseleave="count++">Mouse Leave</div>
  Mouse Leave {{ count }} times
</section>
Mouse Leave
Mouse Leave {{ count }} times

13.31 - ng-mousemove

Handler for mousemove event

Description

The ng-mousemove directive allows you to specify custom custom behavior when a mouse is moved over an element.

Parameters


ng-mousemove

  • Type: Expression

  • Description: Expression to evaluate upon mousemove event. MouseEvent object is available as $event.

  • Example:

    <div ng-mousemove="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <div ng-init="count = 0" ng-mousemove="count++">Mouse Move</div>
  Mouse Move {{ count }} times
</section>
Mouse Move
Mouse Move {{ count }} times

13.32 - ng-mouseout

Handler for mouseout event

Description

The ng-mouseout directive allows you to specify custom behavior when a mouse leaves any part of the element or its children.

Parameters


ng-mouseout

  • Type: Expression

  • Description: Expression to evaluate upon mouseout event. MouseEvent object is available as $event.

  • Example:

    <div ng-mouseout="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <div ng-init="count = 0" ng-mouseout="count++">Mouse Out</div>
  Mouse Out {{ count }} times
</section>
Mouse Out
Mouse Out {{ count }} times

13.33 - ng-mouseover

Handler for mouseover event

Description

The ng-mouseover directive allows you to specify custom behavior when a mouse is placed over an element.

Parameters


ng-mouseover

  • Type: Expression

  • Description: Expression to evaluate upon mouseover event. MouseEvent object is available as $event.

  • Example:

    <div ng-mouseover="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <div ng-init="count = 0" ng-mouseover="count++">Mouse Over</div>
  Mouse Over {{ count }} times
</section>
Mouse Over
Mouse Over {{ count }} times

13.34 - ng-mouseup

Handler for mouseup event

Description

The ng-mouseup directive allows you to specify custom behavior when a pressed mouse is released over an element.

Parameters


ng-mouseup

  • Type: Expression

  • Description: Expression to evaluate upon mouseup event. MouseEvent object is available as $event.

  • Example:

    <div ng-mouseup="$ctrl.greet($event)"></div>
    

Demo

<section ng-app>
  <div ng-init="count = 0" ng-mouseup="count++">Mouse Up</div>
  Mouse Up {{ count }} times
</section>
Mouse Up
Mouse Up {{ count }} times

13.35 - ng-non-bindable

Stops compilation for element

Description

The ng-non-bindable directive tells the framework not to compile or bind the contents of the current DOM element, including directives on the element itself that have a lower priority than ngNonBindable. This is useful if the element contains what appears to be directives and bindings but which should be ignored. This could be the case if you have a site that displays snippets of code, for instance.

ng-non-bindable

  • Type: N/A

  • Description: Stops compilation process for element

  • Priority: 1000

  • Element: ANY

  • Example:

    <section ng-app>
        <div ng-non-bindable>{{ 2 + 2 }}</div>
      </section>
      

    {{ 2 + 2 }}

13.36 - ng-pointer-capture

Capture active pointer streams on an element

Description

The ng-pointer-capture directive captures a pointer stream that starts on the element and releases it when the pointer ends or is cancelled. This is useful for board, canvas, and game-style interfaces where pointermove and pointerup should continue reaching the same element even when the pointer leaves its bounds.

ng-pointer-capture does not implement dragging behavior. It only manages the browser pointer capture lifecycle. Register native pointer listeners from an application directive for drag behavior.

Parameters


ng-pointer-capture

  • Type: boolean attribute

  • Restrict: A

  • Element: ANY

  • Priority: 1

  • Description: Calls setPointerCapture($event.pointerId) on pointerdown, releases capture on pointerup and pointercancel, forgets browser-released pointers on lostpointercapture, and releases active captures when the scope is destroyed.

  • Example:

    <div
      ng-pointer-capture
      pointer-drag
    ></div>
    

Demo

<style>
  .pointer-capture-demo {
    display: grid;
    gap: 0.75rem;
    max-width: 30rem;
  }

  .pointer-capture-demo__board {
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border: 1px solid #cbd5e1;
    background:
      linear-gradient(90deg, rgba(14, 165, 233, 0.12) 1px, transparent 1px),
      linear-gradient(rgba(14, 165, 233, 0.12) 1px, transparent 1px), #f8fafc;
    background-size: 2rem 2rem;
    touch-action: none;
    user-select: none;
  }

  .pointer-capture-demo__marker {
    position: absolute;
    left: 0;
    top: 0;
    width: 1.5rem;
    height: 1.5rem;
    border: 2px solid #fff;
    border-radius: 999px;
    background: #2563eb;
    box-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.22);
    will-change: transform;
  }

  .pointer-capture-demo__status {
    margin: 0;
    color: #475569;
  }
</style>

<section ng-app="pointerCaptureDemo" class="pointer-capture-demo">
  <div ng-controller="PointerCaptureDemo as $ctrl">
    <div
      class="pointer-capture-demo__board"
      ng-pointer-capture
      pointer-drag
    >
      <div
        class="pointer-capture-demo__marker"
        ng-style="$ctrl.markerStyle"
      ></div>
    </div>
    <p class="pointer-capture-demo__status">{{ $ctrl.status }}</p>
  </div>
</section>

<script>
  window.angular
    .createModule('pointerCaptureDemo', [])
    .controller('PointerCaptureDemo', function () {
      this.dragging = false;
      this.status = 'Drag the marker across the board';
      this.markerStyle = {
        transform: 'translate(32px, 32px)',
      };

      this.positionFromEvent = function (event) {
        const rect = event.currentTarget.getBoundingClientRect();
        const radius = 12;
        const x = Math.max(
          radius,
          Math.min(event.clientX - rect.left, rect.width - radius),
        );
        const y = Math.max(
          radius,
          Math.min(event.clientY - rect.top, rect.height - radius),
        );

        this.markerStyle = {
          transform:
            'translate(' + (x - radius) + 'px, ' + (y - radius) + 'px)',
        };
      };

      this.start = function (event) {
        this.dragging = true;
        this.status = 'Pointer captured';
        this.positionFromEvent(event);
      };

      this.move = function (event) {
        if (!this.dragging) {
          return;
        }

        this.status = 'Dragging pointer ' + event.pointerId;
        this.positionFromEvent(event);
      };

      this.end = function (event) {
        if (this.dragging) {
          this.positionFromEvent(event);
        }

        this.dragging = false;
        this.status = 'Pointer released';
      };

      this.cancel = function () {
        this.dragging = false;
        this.status = 'Pointer cancelled';
      };
    })
    .directive('pointerDrag', function () {
      return {
        link(scope, element) {
          const controller = scope.$ctrl;
          const listeners = {
            pointerdown: (event) => controller.start(event),
            pointermove: (event) => controller.move(event),
            pointerup: (event) => controller.end(event),
            pointercancel: () => controller.cancel(),
          };

          for (const [name, listener] of Object.entries(listeners)) {
            element.addEventListener(name, listener);
          }
          scope.on('$destroy', () => {
            for (const [name, listener] of Object.entries(listeners)) {
              element.removeEventListener(name, listener);
            }
          });
        },
      };
    });
</script>

{{ $ctrl.status }}


13.37 - ng-post

Initiates a POST request

Description

The ng-post directive allows you to send data via $http service to a remote URL and insert, replace, or manipulate the server’s response into the DOM. The directive assumes a response will be HTML content. If the server endpoint returns JSON, the response is available to on-success as $res and requires explicit assignment. The swap strategy is ignored for JSON. Unlike its sister ng-get directive, ng-post assumes it is attached to a form, input, textarea, or select element, which act as the source of data for the request payload:

Example

<form ng-post="/register">
  <input name="username" type="text" />
</form>

With form elements, the directive can be registered anywhere inside a form:

Example

<form>
  <input name="username" type="text" />
  <button ng-post="/register">Send</button>
</form>

In case of error, the directive displays a string error in place of the success result. JSON errors are available to on-error as $res. The behavior can be combined with other directivs to create complex form-handling strategies. Below is a form that dissappears in case of success or adds error state in case of validation errors.

Example

<form
  ng-post="/register"
  ng-if="$ctrl.success === false"
  on-success="$ctrl.success = true"
>
  <h2>Register form</h2>

  <label ng-class="{ error: errors.username }">
    Username
    <input
      name="username"
      type="text"
      aria-invalid="{{ errors.username !== undefined}}"
      ng-keyup="errors.username = undefined"
    />
    <span>{{ errors.username }}</span>
  </label>

  <button type="submit">Sign up</button>
</form>

For other input elements, the directive adds form-like behavior. The example below showcases an input acting as a search form:

<input
  name="seach"
  ng-post="/search"
  data-target="#output"
  trigger="keyup"
  placeholder="Search..."
/>

Additional options for request and response handling can be modified with attributes provided below.

Parameters


ng-post

  • Type: string

  • Description: A URL to issue a GET request to

  • Example:

    <div ng-post="/example">post</div>
    

Modifiers

data-enctype

  • Type: string

  • Description: Specifies the content type of form. Defaults to application/json. To send regular URL-encoded data form, use application/x-www-form-urlencoded.

  • Example:

    <form ng-post="/urlencoded" enctype="application/x-www-form-urlencoded">
      <input type="text" name="name" />
    </form>
    

data-form

  • Type: string

  • Description: If placed outside a form element, specifies id of the form to use for datasource.

  • Example:

    <button ng-post="/register" form="register">Send</button>
    <form id="register">
      <input name="username" type="text" />
    </form>
    

data-trigger

  • Type: string

  • Description: Specifies the DOM event for triggering a request (default is click). For a complete list, see UI Events. To eagerly execute a request without user interaction, use the “load” event, which is triggered syntheticaly on any element by the directive linking function. This is in contract to the native load, which executes lazily only for window object and certain resource elements.

  • Example:

    <div ng-post="/example" trigger="mouseover">Get</div>
    

data-latch

  • Type: string

  • Description: Triggers a request whenever its value changes. This attribute can be used with interpolation (e.g., {{ expression }}) to observe reactive changes in the scope.

  • Example:

    <div ng-post="/example" latch="{{ latch }}" ng-mouseover="latch = !latch">
      Get
    </div>
    

data-swap

  • Type: SwapMode

  • Description: Controls how the response is inserted

  • Example:

    <div ng-post="/example" swap="outerHTML">Get</div>
    

data-target

  • Type: selectors

  • Description: Specifies the DOM element where a string response should be rendered.

  • Example:

    <div ng-post="/example" data-target=".test">Post</div>
    <div ng-post="/json" on-success="person = $res">{{ person.name }}</div>
    

data-delay

  • Type: delay

  • Description: Delay request by N millisecond

  • Example:

    <div ng-post="/example" delay="1000">Get</div>
    

data-interval

  • Type: delay

  • Description: Repeat request every N milliseconds

  • Example:

    <div ng-post="/example" interval="1000">Get</div>
    

data-throttle

  • Type: delay

  • Description: Ignores later requests for N milliseconds

  • Example:

    <div ng-post="/example" throttle="1000">Get</div>
    

data-loading

  • Type: N/A

  • Description: Adds a data-loading=“true/false” flag during request lifecycle.

  • Example:

    <div ng-post="/example" data-loading>Get</div>
    

data-loading-class

  • Type: string

  • Description: Toggles the specified class on the element while loading.

  • Example:

    <div ng-post="/example" data-loading-class="red">Get</div>
    

on-success

  • Type: Expression

  • Description: Evaluates expression when request succeeds. Response data is available as a $res property on the scope.

  • Example:

    <div ng-post="/example" on-success="message = $res">Get {{ message }}</div>
    

on-error

  • Type: Expression

  • Description: Evaluates expression when request fails. Response data is available as a $res property on the scope.

  • Example:

    <div ng-post="/example" on-error="errormessage = $res">
      Get {{ errormessage }}
    </div>
    

data-state-success

  • Type: string

  • Description: Name of the state to nagitate to when request succeeds

  • Example:

    <ng-view></ng-view>
    <div ng-post="/example" data-state-success="account">Get</div>
    

data-state-error

  • Type: string

  • Description: Name of the state to nagitate to when request fails

  • Example:

    <ng-view></ng-view>
    <div ng-post="/example" data-state-error="login">Get</div>
    

13.38 - ng-show

Show an element when an expression is truthy.

Toggles the CSS display property of an element without removing it from the DOM. The element’s scope is always alive, regardless of visibility.

<div ng-hide="isLoading">Content here</div>

ng-show

  • Type: expression
  • Required: yes

When truthy, the element is visible (removes the ng-hide CSS class). When falsy, the element is hidden.

ng-hide

  • Type: expression
  • Required: yes

Inverse of ng-show. When truthy, the element is hidden.

Animation hooks: .ng-hide-add / .ng-hide-add-active when hiding, .ng-hide-remove / .ng-hide-remove-active when showing.

ng-if vs ng-show/hide

Aspectng-ifng-show / ng-hide
DOM presenceRemoved when falseAlways in DOM
Scope lifetimeDestroyed when falseAlways alive
Child watchersRemoved when falseAlways active
Initial render costOnly when trueAlways rendered
Animation eventsng-enter / ng-leaveng-hide-add / ng-hide-remove

Use ng-if when the content is expensive to render or when you want to prevent hidden content from making network requests.
Use ng-show/hide when toggling frequently and you need instant re-display without re-initiali

13.39 - ng-state

Bind state declaration data in templates.

Declarative state navigation where both the state name and params come from scope expressions.

<a
  ng-state="'orders.detail'"
  ng-state-params="{ id: order.id }"
  data-state-active
  data-state-exact
>
  Order {{order.id}}
</a>

Use data-state-active when the link should track whether its target state or a child state is active. Add data-state-exact when only the exact target state should count as current. AngularTS writes data-state-current="true" or data-state-current="false" so styling can stay declarative.

ng-state also consumes $aria defaults. A non-native route link such as a div receives role="link", tabindex="0", keyboard activation, and managed aria-current when data-state-active or data-state-exact is present. Authored role, tabindex, and aria-current values are preserved, and ng-aria-disable opts an element out locally.

The ng-state value is always an expression. The quoted literal route name ('orders.detail') is static enough for TypeScript examples and documentation checks to map it back to a known route. Use ng-state="link.state" when the route name itself is dynamic. An unquoted ng-state="orders" reads the orders scope property; it does not name the orders route. Interpolation is not part of the ng-state contract.

Executable sample: state-links.html

ng-state

  • Type: expression
  • Required: yes

Expression that evaluates to a state name string. Use a quoted string literal when the template always targets one route and a direct scope expression when the target is dynamic.

ng-state-params

  • Type: expression

Expression that evaluates to the params object for the state.

$scope.currentParams = { userId: 42 };

13.40 - ng-switch

Switch between exclusive template blocks.

Conditionally renders one of several templates based on the value of an expression. Similar to a JavaScript switch statement.

<div ng-switch="role">
  <div ng-switch-when="admin">Admin panel</div>
  <div ng-switch-when="editor">Editor tools</div>
  <div ng-switch-default>Standard view</div>
</div>

ng-switch

  • Type: expression
  • Required: yes

The value to switch on. Applied to the container element.

ng-switch-when

  • Type: string
  • Required: yes

The value to match against ng-switch. The element is rendered when the switch value equals this.

ng-switch-default

  • Type: none

Rendered when no ng-switch-when matches. No value required.

Multiple ng-switch-when values match the same element:

<div ng-switch="status">
  <div ng-switch-when="pending" ng-switch-when="processing">In progress</div>
  <div ng-switch-when="done">Complete</div>
</div>

Animation hooks: .ng-enter / .ng-leave on matched/unmatched elements.

13.41 - ng-view

Render the active routed view.

Marks the element where the router renders the active state’s template. When the active state changes, the content inside ng-view is replaced with the new state’s template, compiled into a new scope.

<ng-view></ng-view>

<!-- As an attribute -->
<div ng-view></div>

<!-- Named view (for multiple named views in one state) -->
<div ng-view="sidebar"></div>

ng-view

  • Type: string

Optional view name. When omitted, this outlet renders the unnamed (default) view of the active state. When specified, renders the named view from the state’s views configuration.

States define their templates in $stateRegistry:

  name: 'home',
  url: '/home',
  template: '<h1>Home</h1>'
});

// Named views:
$stateRegistry.register({
  name: 'dashboard',
  url: '/dashboard',
  views: {
    '': { template: '<p>Main content</p>' },
    'sidebar': { template: '<nav>Sidebar</nav>' }
  }
});

13.42 - ng-window-* and ng-document-*

Handler for window and document events

Description

The ng-window-* and ng-document-* directives allow you to specify custom behavior for events dispatched from the Window or Document object. The event name is defined by including it in the placeholder of the directive name. Example: ng-window-online binds to the window online event, and ng-document-pointerup binds to the document pointerup event.

Listeners are removed automatically when the owning scope is destroyed.

For standard event names, see Window events and Document events.

Parameters


ng-window-*

  • Type: Expression

  • Description: Expression to evaluate upon event dispatch. Event object is available as $event.

  • Example:

    <div ng-window-message="data = $event.message.date">{{ data }}</div>
    

ng-document-*

  • Type: Expression

  • Description: Expression to evaluate upon document event dispatch. Event object is available as $event.

  • Example:

    <div ng-document-pointerup="$ctrl.endDrag($event)"></div>
    

Demo

<section ng-app>
  In Chrome DevTools: Open <b>Network tab</b> → Select
  <b>Throttling</b> dropdown -> Click <b>Offline</b> checkbox
  <div ng-window-online="online = true">Connected: {{ online }}</div>
  <div ng-window-offline="offline = true">Disconnected: {{ offline }}</div>
  <button ng-document-click="lastDocumentClick = $event.type">
    Listen for next document click
  </button>
  <div>Document event: {{ lastDocumentClick || 'none' }}</div>
</section>
In Chrome DevTools: Open Network tab → Select Throttling dropdown -> Click Offline checkbox
Connected: {{ online }}
Disconnected: {{ offline }}
Document event: {{ lastDocumentClick || 'none' }}

14 - Service reference

Look up injectable AngularTS services and their runtime responsibilities.

Service names beginning with $ are framework-provided injection tokens. Read the service guides first if services or dependency injection are new to you.

14.1 - $anchorScroll

Scroll to document anchors and coordinate hash-driven scrolling.

$anchorScroll scrolls to the element identified by a hash or, when omitted, to the current value of $location.getHash(), according to the rules specified in the HTML spec.

It also watches the URL hash and automatically scrolls to any matched anchor whenever it is changed by $location.

Exact signatures live in TypeDoc:

Configure

Use module.config({ $anchorScroll: ... }) for application-wide scroll policy.

angular.createModule("app", []).config({
  $anchorScroll: {
    autoScrolling: false,
  },
});

Executable sample: anchor-scroll.html

Manual Scrolling

$location.setHash("details");
$anchorScroll("details");

When automatic scrolling is disabled, call $anchorScroll(...) directly after coordinating routing, animation, or focus.

Vertical Offset

Use the yOffset property to specify a fixed or dynamic vertical scroll offset. This is useful when the application has sticky headers.

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
  .createModule('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) {
          // Pass `newHash` to setHash() and
          // $anchorScroll will automatically scroll to it
          $location.setHash('anchor' + x);
        } else {
          // call $anchorScroll() explicitly,
          // since getHash() still returns the same value
          $anchorScroll();
        }
      };
    },
  ]);

window.angular.bootstrap(document.getElementById('demo2'), ['demo2']);
Anchor {{x}} of 5

For detailed method description, see $anchorScroll.

14.2 - $compile

Template compilation service

14.3 - $controller

Controller service

14.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.getModule('app').controller(
  'UserCtrl',
  /** @param {ng.CookieService} $cookie */
  ["$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 with module.config({ $cookie: ... }).

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

Defaults are merged into $cookie.put(), $cookie.putObject(), and $cookie.remove() calls. Per-call options still override configured defaults.

Cookies are limited to roughly 4 KB, including key and value, so avoid storing large objects; prefer identifiers or short tokens.

Executable sample: log-cookie.html

For detailed method description, see CookieService

14.5 - $eventBus

Application-wide event bus service

Description

A messaging service, backed by an instance of EventBus. This implementation is based on the original Google Closure EventBus 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.createModule('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 EventBus


14.6 - $exceptionHandler

Error handling service

Description

$exceptionHandler is the central hook for exceptions that escape detached AngularTS-managed work. This includes DOM and transport events, scheduled tasks, subscriptions, expression evaluation, and lifecycle callbacks whose original caller can no longer receive a thrown value or rejected promise.

The decision is: can the initiating caller still receive the failure? If yes, throw synchronously or reject the returned promise. Use $exceptionHandler only after that caller boundary no longer exists.

Synchronous API validation throws directly. Promise-returning operations reject their returned promise. Expected transport, workflow, routing, and domain failures remain in their typed operation channels.

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.

Configure

Use module.config({ $exceptionHandler: ... }) for application-wide error reporting policy.

angular.createModule('app', []).config({
  $exceptionHandler: {
    handler(error) {
      myReporter.capture(error);
      throw error;
    },
  },
});

Custom handlers should rethrow after reporting. AngularTS treats $exceptionHandler as fail-fast, and swallowing errors can hide broken application state.

Executable sample: exception-handler.html

14.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 module.config({ $http: ... }), or mutate runtime defaults through $http.defaults.

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

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

Executable sample: http.html

Interceptors

Interceptors are registered with module.config({ $http: ... }). Request hooks run in registration order; response hooks run in reverse order.

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

XSRF

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

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

14.8 - $interpolate

Compile interpolation text into reusable expression functions.

14.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.getPath();
$location.getSearch();
$location.getHash();
$location.getUrl();
$location.absUrl();

Update The URL

$location
  .setPath("/settings")
  .setSearch({ tab: "profile" })
  .setHash("details");

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

Use setSearchParam() when only one query parameter should change. Passing null or undefined removes that parameter.

$location.setSearchParam("page", 2);
$location.setSearchParam("filter", null);

$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 });
});

Configure

Use module.config({ $location: ... }) for application-wide URL policy.

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

html5Mode may also be a boolean when only the enabled flag should change:

angular.createModule("app", []).config({
  $location: {
    html5Mode: false,
  },
});

Executable sample: location.html

14.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.getModule('demo').controller('MyController', ["$log", ($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 suppress debug messages unless logging config enables them.

Configure

Use module.config({ $log: ... }) for application-wide logging policy.

angular.createModule('demo', []).config({
  $log: {
    debug: true,
  },
});

debug controls whether $log.debug(...) writes to the console. The default is false.

Beacon delivery

Selected log levels can also be delivered through the browser’s Beacon API. Beacon delivery is disabled unless an endpoint is configured and defaults to the error level when levels is omitted.

angular.createModule('demo', []).config({
  $log: {
    beacon: {
      url: '/api/client-logs',
      levels: ['warn', 'error'],
    },
  },
});

The default serializer sends an application/json Blob containing timestamp, level, and args. Applications that need redaction or additional metadata can register and select a serializer:

angular
  .getModule('demo')
  .factory(
    'clientLogSerializer',
    () => (entry) =>
      JSON.stringify({
        timestamp: entry.timestamp,
        level: entry.level,
        message: String(entry.args[0]),
      }),
  )
  .config({
    $log: {
      beacon: {
        url: '/api/client-logs',
        serializer: 'clientLogSerializer',
      },
    },
  });

Beacon queueing has no response callback or delivery acknowledgement. AngularTS does not retry or persist rejected entries. Failures produce a local warning by default; set failure: "ignore" to suppress it.

Beacon delivery automatically participates in $security; there is no separate logging security switch. AngularTS checks the endpoint and configured request credentials before serializing an entry. A denied remote delivery never suppresses the local log call, and repeated policy denials produce at most one local warning.

Beacon requests cannot carry application-defined Authorization headers, so Bearer and Basic credential policies deny Beacon delivery. Use a cookie endpoint for authenticated Beacon logging. If the endpoint requires custom headers, use $http or fetch() with keepalive: true instead. Redaction and payload shaping remain the responsibility of the configured serializer.

You can also replace the logger implementation:

angular.createModule('demo', []).config({
  $log: {
    logger: () => ({
      log: console.log,
      info: console.info,
      warn: console.warn,
      error: console.error,
      debug: console.debug,
    }),
  },
});

Executable sample: log-cookie.html

Decorating $log

You can also override any $log service method with module.decorator(). Below is a simple example that overrides default $log.error to log errors to both console and a backend endpoint.

Example*
angular
  .getModule('demo')
  .decorator('$log', ["$delegate", "$http", "$exceptionHandler", ($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.getModule('demo').run(["$http", ($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

14.11 - $machine

Reactive state machines with declarative transitions

$machine

$machine creates a small reactive state 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: state 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 states.

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

Machine naming, inference, and public-type placement follow the shared API ergonomics contract.

Create a Machine

The smallest complete game needs only a state tree and a score. Assign the machine to a controller property and let the template call send() directly:

const game = $machine({
  initial: 'ready',
  data: { score: 0 },
  states: {
    ready: { on: { start: { to: 'playing' } } },
    playing: { on: { tap: { to: 'won' } } },
    won: { on: { restart: { to: 'playing' } } },
  },
});
<section ng-controller="TapGameCtrl as $ctrl">
  <p>{{$ctrl.game.state}}: {{$ctrl.game.data.score}} / 3</p>
  <button
    ng-if="$ctrl.game.matches('ready')"
    ng-click="$ctrl.game.send('start')"
  >
    Start
  </button>
  <button
    ng-if="$ctrl.game.matches('playing')"
    ng-click="$ctrl.game.send('tap')"
  >
    Tap
  </button>
  <button
    ng-if="$ctrl.game.matches('won')"
    ng-click="$ctrl.game.send('restart')"
  >
    Play again
  </button>
</section>

state identifies the machine’s active state. State-tree transitions move to the explicit to state, while update mutates reactive game data. A to(context) resolver handles transitions whose target depends on runtime data.

Ownership Patterns

Create machines with $machine(config) and assign the returned object to the owner that should keep the flow alive.

Controller-owned machines are enough for local UI flow data:

app.controller('SessionCtrl', [
  '$machine',
  function ($machine) {
    this.session = $machine({
      initial: 'setup',
      data: { roomId: '' },
      states: {
        setup: {
          on: {
            join: {
              to: 'waiting',
              update({ data, payload }) {
                data.roomId = payload.roomId;
              },
            },
          },
        },
        waiting: {},
      },
    });
  },
]);

AppContext models can own machines that must survive DOM root destruction:

app.model('sessionRuntime', [
  '$machine',
  function ($machine) {
    return {
      session: $machine({
        initial: 'setup',
        data: { roomId: '' },
        states: {
          setup: {
            on: {
              join: {
                to: 'waiting',
                update({ data, payload }) {
                  data.roomId = payload.roomId;
                },
              },
            },
          },
          waiting: {},
        },
      }),
    };
  },
]);

Runtime adapters and game loops can keep the same machine outside the DOM:

app.controller('GameCtrl', [
  '$machine',
  function ($machine) {
    const game = $machine({
      initial: 'playing',
      data: { frame: 0 },
      states: {
        playing: {
          on: {
            tick: {
              update({ data }) {
                data.frame += 1;
              },
            },
          },
        },
      },
    });

    this.game = game;

    requestAnimationFrame(function tick() {
      game.send('tick');
      requestAnimationFrame(tick);
    });
  },
]);

Workflows can own a machine when command execution needs a local flow gate:

app.factory('sessionWorkflowMachine', [
  '$machine',
  function ($machine) {
    return $machine({
      initial: 'idle',
      data: { token: '' },
      states: {
        idle: {
          on: {
            start: {
              to: 'authenticating',
            },
          },
        },
        authenticating: {
          on: {
            authenticated: {
              to: 'ready',
              update({ data, payload }) {
                data.token = payload;
              },
            },
          },
        },
        ready: {},
      },
    });
  },
]);

Register a Named Machine

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

app.machine('sessionMachine', {
  initial: 'setup',
  data: {
    roomId: '',
  },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update({ data, payload }) {
            data.roomId = payload.roomId;
          },
        },
      },
    },
    waiting: {},
  },
});

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

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

function buildSessionMachine(appSettings) {
  return {
    initial: appSettings.initialMode,
    data: {
      roomId: '',
      error: '',
    },
    states: {
      setup: {
        on: {
          join: {
            to: 'waiting',
            update({ data, payload }) {
              data.roomId = payload.roomId;
            },
          },
        },
      },
      waiting: {},
    },
  };
}

buildSessionMachine.$inject = ['appSettings'];

app.machine('sessionMachine', buildSessionMachine);

This is useful when defaults depend on environment, user session data, 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.state; // current state string
session.data; // reactive data object
session.matches('setup'); // true when current state is setup
session.can('join', payload); // true when current state can run join
session.send('join', payload); // structured transition result
session.snapshot(); // structured clone of { state, data }
session.restore(snapshot);

send(type, payload) returns a structured result with ok, status, type, from, to, and an optional denial reason. ok is true when a transition entry exists for the current state, its optional guard passes, and its update hook runs when provided. Missing transitions and blocked guarded transitions return ok: false with a specific status and do not throw. Use $workflow when failures need diagnostics, retry, or recovery.

ok is the broad branch: successful results have status transitioned or updated; failed results have a denial, invalid-event, or missing-transition status. Machine results intentionally do not carry diagnostics. The status is the decision, while workflows own durable failure evidence.

State-tree transition shape:

  • to names the next state or resolves it from readonly transition input
  • omit to for a same-state data update
  • update({ data, payload }) mutates reactive data
  • guard({ data, payload }) blocks the transition when it returns false
  • denied({ data, payload }) may record local denial state

Routing is explicit. Static to is the normal path. Use a synchronous, side-effect-free to(context) resolver when the next state depends on runtime data, such as a game move ending in win, draw, or continued play. AngularTS resolves one target before guard and policy evaluation; hooks and update() receive that same readonly target. Return values from update() are ignored.

can(type, payload) checks the transition table, target resolver, policy, and any optional guard for the current state. It does not run update or denied. Target resolvers and guards should be side-effect-free because templates may call can() repeatedly.

Status values are transitioned, updated, missing-transition, guard-denied, policy-denied, and invalid-event. When policy denies a transition, the result includes the policy decision’s optional reason.

Denial paths are narrow. guard-denied only means the transition guard returned false; transition-local denied() runs only for that guard-denial case. Missing transitions and invalid event names do not run before, update, after, denied, enter hooks, exit hooks, or global transition hooks. policy-denied is reserved for framework policy gates. It does not run denied, mutation hooks, state hooks, global transition hooks, or legacy transition targets.

Transition Policy

Use policy when the application has a cross-cutting synchronous gate that should apply to every transition in a machine:

const session = $machine({
  id: 'session',
  initial: 'setup',
  data: { roomId: '' },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update({ data, payload }) {
            data.roomId = payload.roomId;
          },
        },
      },
    },
    waiting: {},
  },
  policy(context) {
    if (context.type === 'join' && !context.payload.roomId) {
      return { type: 'deny', reason: 'room_required' };
    }

    return 'allow';
  },
});

session.send('join', { roomId: '' }).status; // "policy-denied"

Policy contexts include operation: "machine.transition", machineId, type, from, to, event-narrowed payload, readonly data, an observational machine view, and optional meta. The policy and guard machine view exposes state, data, can(), matches(), and snapshot() but cannot dispatch or restore live state. Policies must be synchronous so can() and send() stay synchronous. Put async authorization, retries, persistence, and recovery in $workflow or another service and then project the result back through a machine event.

State-tree transitions with static to give policy the strongest pre-execution context. Legacy callback transitions do not run their target just to discover a future state, so policy sees to as the current state there.

Guarded Transitions

Use guard 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: '',
  },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          guard({ data, payload }) {
            return !!data.inviteCode && payload.roomId !== '';
          },
          update({ data, payload }) {
            data.roomId = payload.roomId;
          },
          denied({ data }) {
            data.roomId = '';
          },
        },
      },
    },
    waiting: {},
  },
});

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

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

Example: Session Wizard

A machine is also useful for local wizard flow where the UI should only expose legal next actions:

const wizard = $machine({
  initial: 'profile',
  data: { name: '' },
  states: {
    profile: {
      on: {
        next: {
          to: 'complete',
          guard: ({ data }) => data.name.trim() !== '',
        },
      },
    },
    complete: {},
  },
});

Static to keeps the flow inspectable: next, back, and submit declare their route before mutation code runs. The same-state accept event omits to because it only updates wizard data.

Example: Tic Tac Toe

A machine is useful when user actions must follow a small set of legal states. 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 state, so further moves return false.

const game = $machine({
  initial: 'playing',
  data: {board: Array(9).fill('-'), nextPlayer: 'X'},
  states: {
    playing: {on: {move: {
      guard: ({data, payload}) => data.board[payload.index] === '-',
      update({data, payload}) {
        data.board[payload.index] = data.nextPlayer;
        data.nextPlayer = data.nextPlayer === 'X' ? 'O' : 'X';
      },
      to: ({data, from}) => winnerOf(data.board) || from,
    }}},
    xWon: {},
    oWon: {},
    draw: {},
  },
});
    },
  });
}]);
<button
  ng-repeat="cell in $ctrl.game.data.board"
  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. Its to resolver predicts the next state without mutation; a valid move then updates data.board and whose turn is next. An invalid move records lastError in denied, leaving state unchanged. Machine transitions do not roll back data mutations when hooks throw.

Persist With Transition Hooks

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

const persistedGame = $machine({
  ...gameConfig,
  hooks: {
    transition: ({ machine }) =>
      localStorage.setItem('game', JSON.stringify(machine.snapshot())),
  },
});

const saved = localStorage.getItem('game');
if (saved) persistedGame.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 state and data.

Snapshots

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

const snapshot = session.snapshot();

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

A snapshot contains only state 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 accepts persisted input as unknown, validates its shape, and rejects a state that is not declared in states. Restore does not run transition hooks.

The configured initial state and every resolved transition target must also exist in states. Invalid built-in machine states fail at their boundary instead of leaving the machine in an unreachable state.

Hooks

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

const session = $machine({
  initial: 'setup',
  data: { status: 'idle' },
  states: {
    setup: { on: { join: { to: 'waiting' } } },
    waiting: { on: { matched: { to: 'playing' } } },
    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 state changes, enter runs after state changes, and transition runs after state-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/state 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 state 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.

Prefer $machine(config) plus assignment to the controller, model, service, workflow, or runtime adapter that owns the machine. Named machines registered with module.machine() follow the same rule: they bind when a scope proxy observes them.

TypeScript

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

const session = $machine({
  initial: 'setup',
  data: {
    roomId: '',
    error: '',
  } satisfies SessionData,
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update({ data }) {
            data.roomId = 'abc';
          },
        },
      },
    },
    waiting: {},
  },
});

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

Machine definitions are usable without generic parameters. Event names are inferred from the on maps and state names are inferred from states. Payloads remain unknown until an explicit event map supplies their types.

Add an event map when you want strict checked event names and payloads:

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

const session = $machine<SessionData, SessionEvents>({
  initial: 'setup',
  data: { roomId: '', error: '' },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update: ({ data, payload }) => (data.roomId = payload.roomId),
        },
      },
    },
    waiting: { on: { reset: { to: 'setup' } } },
  },
});

The ordinary authoring path does not require extraction helpers. Package authors can import MachineDataOf, MachineEventsOf, MachineEventNamesOf, and MachineStatesOf directly from @angular-wave/angular.ts/services/machine when an adapter needs to derive types from a reusable definition.

14.12 - $parse

URL normalization for HTML5/hashbang modes

14.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' });
await posts.delete(42);

get(), create(), and update() return null when the backend returns no entity. delete() resolves with no value. All backend failures reject instead of being converted into fallback values.

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:

const posts = $rest<Post, number>('/api/posts', Post, {
  backend: new CachedRestBackend({
    network: new HttpRestBackend($http),
    cache: appCache,
    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.

Shared Defaults

Configure shared request defaults before bootstrap when a group of resources should use the same backend behavior.

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

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

URL normalization for HTML5/hashbang modes

14.15 - $rootScope

Root scope service

14.16 - $security

Cross-cutting security policy gate for navigation and request-sensitive operations.

$security centralizes framework security policy decisions so apps can keep cross-cutting concerns out of every service call. The runtime-composed service allows by default and applies typed configuration from app.config({ $security: ... }).

Services consume this policy before sensitive side effects:

  • $http calls check() once and applies credentials from its allow decision.
  • $log synchronously gates Beacon delivery before serializing an entry.
  • Router transitions call check() before state changes proceed.

When no fallback is configured, the fallback decision is allow.

Exact policy signatures live in TypeDoc:

Configure

Use app.config({ $security: ... }) to set your app policy:

angular.createModule('app', []).config({
  $security: {
    fallback: 'deny',
    allowInsecureOrigins: ['http://localhost:3000'],
    isAuthenticated: function isAuthenticated() {
      return sessionStore.currentUser != null;
    },
    credentials: {
      bearer: function getToken() {
        return sessionStorage.getItem('token');
      },
      cookie: true,
      order: ['bearer', 'cookie'],
    },
    permissions: async function permissions(permission, context) {
      return permissionStore.allows(permission, context.to.name);
    },
  },
});

fallback controls unspecified behavior ("allow" | "deny"). Redirects are declared on route policies where a target is required.

Configuring a credential enables its request transport scheme. It does not declare that the current user is authenticated. isAuthenticated supplies that application state as a boolean or contextual async function. The optional credentials.order changes the default bearer, basic, then cookie precedence without requiring a second list of enabled schemes.

allowInsecureOrigins is an explicit allowlist for HTTP origins that may carry sensitive credentials. All other insecure origins remain blocked. Malformed origin strings are ignored, preserving fail-closed credential transport. Invalid policy enums and configuration shapes fail during setup.

credentials.bearer attaches a bearer token to allowed $http requests. Caller-provided Authorization headers are recognized automatically.

credentials.basic accepts username and password value sources and encodes the resulting credentials for the Authorization header.

credentials.cookie tells request services to use browser-managed cookies by enabling credentialed requests instead of manually writing a Cookie header.

Request policy contexts identify their framework transport. HTTP, Beacon, Worker, and Service Worker creation all pass through the same security gate. Beacon participates automatically, but its Web Platform API cannot apply custom headers. Bearer and Basic policies therefore deny Beacon delivery, while an enabled cookie session permits it. Security evaluation remains synchronous inside the Beacon transport so lifecycle delivery is never delayed by a Promise.

permissions receives each required permission and the current navigation context. It may return a boolean or a Promise. A static permission array is a shorthand for applications that do not need contextual authorization.

Route Policy

States can declare inherited navigation policy. The router merges policy from the target state path and passes the effective policy to $security before resolves, controllers, or views run.

angular
  .createModule('admin', [])
  .router({
    name: 'admin',
    url: '/admin',
    abstract: true,
    template: '<admin-layout ng-view></admin-layout>',
    policy: {
      navigation: {
        authenticated: true,
        permissions: ['admin:read'],
        redirectTo: 'login',
      },
    },
  })
  .router({
    name: 'admin.users',
    url: '/users',
    component: 'adminUsers',
  });

authenticated: true is satisfied only when the configured isAuthenticated evaluator resolves true. Credential transport and authentication state remain separate.

Required permissions are resolved by the configured permissions evaluator. If a required permission is missing, navigation is denied or redirected to the route policy’s redirectTo target.

Non-security route policies (loading, error, fallbackTo, retry) run in the same transition pipeline but do not replace authentication/authorization. They are complementary: $security decides whether navigation is allowed for the current principal, and route policies decide how the transition is rendered or recovered when that navigation remains allowed.

Use public: true on a child state to explicitly clear inherited navigation requirements. A public declaration cannot also specify authenticated, permissions, redirectTo, or reason because those fields would be contradictory or ignored.

Security policy has no public provider. Use app.config({ $security: ... }); route matching remains exclusively in the router state tree.

Executable config sample: security.html

Executable route policy sample: policy.html retry-fallback.html

14.17 - $serviceWorker

Service worker lifecycle, update, and message facade

$serviceWorker wraps browser-owned service worker registration, readiness, update observation, controller changes, and page-to-worker messaging.

Use it when the browser owns the page-control lifecycle. Use $worker for page-owned background computation. Use $rest and RestBackend for request and cache composition. Use $workflow for command execution and recovery; a service worker can be an activity boundary, not the workflow engine.

Exact method signatures live in TypeDoc:

Registration

Configure automatic registration on the module when the application has one stable worker script:

angular.createModule('app', []).serviceWorker('/sw.js', {
  scope: '/',
  updateViaCache: 'none',
  autoRegister: true,
});

Or configure the script without automatic registration and start it explicitly:

angular.createModule('app', []).serviceWorker('/sw.js', {
  scope: '/',
  updateViaCache: 'none',
});

$serviceWorker.register();

// Override browser registration options without repeating the configured URL.
$serviceWorker.register({ scope: '/feature/' });

$serviceWorker.ready().then(() => {
  status = $serviceWorker.status;
});

The default policy is observation only. AngularTS does not reload the page, activate a waiting worker, or pick a cache strategy for the app.

Destroying an AngularTS runtime removes its service-worker event listeners and reactive bindings. It does not call unregister(); browser registration remains active until the application explicitly unregisters it.

registrationState and updateState are readonly framework-owned views. Their properties update reactively as the browser registration changes.

Executable sample: service-worker.html

Update Prompt

Observe update state and make activation a user choice:

$serviceWorker.onUpdate((state) => {
  if (state.waiting) {
    showUpdatePrompt = true;
  }
});

If the user opts in, send your own protocol message to the waiting worker. Keep reload separate so the app can save state, finish work, or defer navigation.

function activateUpdate() {
  $serviceWorker.registration?.waiting?.postMessage({
    type: 'app:skip-waiting',
  });
}

$serviceWorker.onControllerChange(() => {
  showReloadButton = true;
});

function reloadAfterUserConfirms() {
  window.location.reload();
}

Messages

Use $serviceWorker.post(...) for simple page-to-worker messages after a controller is available:

$serviceWorker.onMessage((event) => {
  if (event.data.type === 'app:pong') {
    lastWorkerReply = event.data;
  }
});

$serviceWorker.post({
  type: 'app:ping',
}, {
  target: 'controller',
});

For request/response protocols, request() creates a dedicated MessageChannel, transfers its response port, and applies a timeout:

const profile = await $serviceWorker.request<Profile>(
  { type: 'profile:load' },
  { timeout: 5000 },
);

The service-worker message handler responds through event.ports[0]. AngularTS does not add request ids or prescribe an application message envelope.

Security

Registration participates in $security automatically. Bearer and Basic credential policies deny registration because the Web Platform registration API cannot attach application-defined Authorization headers. Cookie credentials remain browser-managed.

REST And Cache

Do not make $rest depend on service workers. REST caching belongs behind RestBackend and RestCacheStore; a service-worker-backed REST cache should be an explicit backend adapter when the app needs that boundary.

Unsupported Browsers

When navigator.serviceWorker is unavailable, $serviceWorker.supported is false, status is unsupported, and async lifecycle methods reject with a stable service-worker error. Applications can render a non-offline experience without special provider wiring.

14.18 - $templateCache

Map object for storing templates

$templateCache is a Map object used by template loading and component rendering.

Configure the cache before bootstrap with module.config({ $templateCache: ... }).

angular.createModule("demo", []).config({
  $templateCache: {
    cache: new Map([["templateId.html", "This is the content of the template"]]),
  },
});

Executable sample: template-cache.html

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.createModule('myApp', []).run(["$templateCache", ($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',
  ["$templateCache", class {
    constructor($templateCache) {
      const tmp = $templateCache.get('templateId.html');
    }
  }],
);

14.19 - $templateRequest

Template request service

$templateRequest fetches templates through $http and stores successful responses in $templateCache.

Configure template request defaults before bootstrap with module.config({ $templateRequest: ... }).

angular.createModule("demo", []).config({
  $templateRequest: {
    httpOptions: {
      headers: {
        "X-Template": "configured",
      },
      withCredentials: true,
    },
  },
});

Executable sample: template-request.html

Usage

$templateRequest("./panel.html").then((template) => {
  $templateCache.set("panel.html", template);
});

$templateRequest assumes template URLs are trusted by the caller. Security policy for resource URLs belongs to $compile.resourceUrlPolicy when templates are loaded through compiler bindings such as ng-include.

14.20 - $url

URL management for state transitions

14.21 - Binding policies

Destination-specific HTML, URL, and script binding policies

Binding policies

AngularTS uses synchronous callbacks configured through $compile to sanitize or validate values at their DOM destination. The former $sce and $sceDelegate services and their configuration keys have been removed.

Configuration

Each callback receives a string and must return a string or throw to reject it. Omitted callbacks retain the framework defaults.

Configuration callbackDestinationDefault
htmlPolicyng-bind-html and HTML propertiesReject nonempty strings
urlPolicyLink URLsBlock dangerous schemes
mediaUrlPolicyMedia URLs and each srcset candidateAllow supported media schemes and image data URLs
resourceUrlPolicyTemplates and embedded resourcesAllow same-origin HTTP(S) resources
scriptPolicyExecutable JavaScript sourceReject nonempty strings
scriptUrlPolicyScript-loading URLsAllow same-origin HTTP(S) resources
angular.createModule("app", []).config({
  $compile: {
    htmlPolicy: (html) => DOMPurify.sanitize(html),
    resourceUrlPolicy: (value) => {
      const url = new URL(value, document.baseURI);
      if (url.origin !== location.origin) {
        throw new Error("External resource rejected");
      }
      return value;
    },
  },
});

Supply your application’s sanitizer implementation, such as DOMPurify, before using this configuration. A policy callback is responsible for the values it approves. Policies are configured before bootstrap and are not injectable services.

Native TrustedHTML, TrustedScript, and TrustedScriptURL values are supported at their corresponding destinations. The framework does not create browser Trusted Types policies for the application.

Executable sample: sce.html.

14.22 - Workflow

Declarative asynchronous operation lifecycles with reactive state, cancellation, diagnostics, snapshots, and recovery.

Workflow

$workflow runs asynchronous operations through declarative lifecycles. A definition states where a command may run and what state and data changes occur while it is pending, after success, and after failure. Application code only requests the operation.

Use $machine for synchronous event-driven state. Use $workflow when an operation can wait, fail, time out, be cancelled, or require recovery.

Create A Workflow

app.workflow('checkout', {
  initial: 'ready',

  data: {
    receipt: null,
    failure: '',
  },

  commands: {
    submit: {
      from: 'ready',
      pending: 'processing',

      execute: ({ input, signal }) =>
        checkoutApi.submit(input, { signal }),

      success: {
        to: 'completed',
        update({ data, output }) {
          data.receipt = output;
          data.failure = '';
        },
      },

      failure: {
        to: 'failed',
        update({ data, diagnostic }) {
          data.failure = diagnostic.message;
        },
      },
    },
  },
});

Inject the named workflow and request the command:

app.controller('CheckoutCtrl', ["checkout", function (checkout) {
  this.checkout = checkout;

  this.submit = async (order) => {
    const result = await checkout.run('submit', order);

    if (!result.ok) {
      console.error(result.diagnostics);
    }
  };
}]);

Templates observe workflow state and data directly:

<button
  ng-click="$ctrl.submit($ctrl.order)"
  ng-disabled="!$ctrl.checkout.can('submit')"
>
  Submit
</button>

<p ng-if="$ctrl.checkout.state === 'processing'">Processing</p>
<p ng-if="$ctrl.checkout.state === 'failed'">
  {{ $ctrl.checkout.data.failure }}
</p>

Command Lifecycle

Every command declares:

  • from: state or states from which the command may run.
  • pending: state entered before execution starts.
  • execute: optional synchronous or asynchronous operation.
  • success: state entered when execution returns.
  • failure: state entered when execution throws or rejects.

pending, success, and failure accept either a state name or an object with to and update:

success: {
  to: 'completed',
  update({ data, input, output, command }) {
    data.receipt = output;
  },
}

Lifecycle updates are the writable boundary for workflow data. The data passed to execute is readonly, including nested plain objects, arrays, maps, and sets. This prevents a failed command from leaving untracked imperative changes.

Immediate Commands

An operation that only changes workflow state can omit execute:

commands: {
  reset: {
    from: ['completed', 'failed'],
    pending: 'resetting',
    success: {
      to: 'ready',
      update({ data }) {
        data.receipt = null;
        data.failure = '';
      },
    },
    failure: 'failed',
  },
}
await checkout.run('reset');

External approvals, WebSocket messages, user actions, and repair requests enter a workflow as commands. Workflows do not expose a separate event or send() API.

Results

run() always resolves to a WorkflowResult:

const result = await checkout.run('submit', order);

if (result.ok) {
  console.log(result.output);
} else if (result.status === 'timeout') {
  console.warn('The operation timed out.');
} else {
  console.warn(result.diagnostics);
}

Successful results have status completed. Failed results use failed, cancelled, timeout, or rejected.

rejected means execution did not fail unexpectedly. The command was unknown, was not allowed from the current state, violated its concurrency declaration, or called reject() with a controlled diagnostic.

Controlled Rejection

Use reject() for an expected business decision:

execute({ input, reject }) {
  if (!input.acceptedTerms) {
    return reject({
      code: 'checkout.termsRequired',
      message: 'Terms must be accepted.',
      recoverable: true,
    });
  }

  return checkoutApi.submit(input);
}

The framework records the diagnostic and applies the declared failure lifecycle.

Cancellation And Timeout

Commands receive an AbortSignal and may register cleanup callbacks:

execute({ input, signal, cleanup }) {
  const request = checkoutApi.submit(input, { signal });

  cleanup(() => request.releaseResources());

  return request;
}

Cancel one command or every running command:

checkout.cancel('submit');
checkout.cancel();

Declare timeout duration and outcome with the command:

submit: {
  from: 'ready',
  pending: 'processing',
  commandTimeout: 5000,
  execute: submitOrder,
  success: 'completed',
  failure: 'failed',
  cancelled: 'cancelled',
  timeout: 'timed-out',
}

When cancelled or timeout is omitted, the command uses its failure lifecycle.

Concurrency And Retry

Concurrency belongs to the declaration:

submit: {
  from: ['ready', 'processing'],
  pending: 'processing',
  concurrency: 'reject',
  retry: 2,
  execute: submitOrder,
  success: 'completed',
  failure: 'failed',
}
  • reject is the default and rejects another run of the same command.
  • queue executes runs of the same command in request order.
  • parallel allows overlapping execution when the declared source states also permit it.
  • retry is the number of automatic retries after unexpected execution failure.

Callers cannot override these properties through run(). Operational behavior therefore remains part of the workflow definition.

Diagnostics And History

workflow.diagnostics contains normalized, serializable evidence from failed operations. workflow.history records command start, completion, and final failure.

workflow.diagnostics;
workflow.history;

Use diagnosticLimit and historyLimit to bound retained entries:

app.workflow('checkout', {
  initial: 'ready',
  data: {},
  diagnosticLimit: 100,
  historyLimit: 500,
  commands: checkoutCommands,
});

Snapshot And Restore

Snapshots contain the workflow id, state, data, diagnostics, and history:

const snapshot = checkout.snapshot();

localStorage.setItem('checkout', JSON.stringify(snapshot));
checkout.restore(JSON.parse(localStorage.getItem('checkout')));

Use migrateSnapshot when persisted data predates the current snapshot shape:

migrateSnapshot(snapshot) {
  if (snapshot.version === 0) {
    return {
      version: 1,
      id: 'checkout',
      state: snapshot.stage,
      data: snapshot.data,
      diagnostics: [],
      history: [],
    };
  }

  return snapshot;
}

Supervisor

A workflow supervisor owns persistence and recovery for a named group:

app.workflowSupervisor('checkoutProcesses', {
  workflows: {
    checkout: checkoutDefinition,
    fulfillment: fulfillmentDefinition,
  },
  persistence: 'indexeddb',
  autoPersist: true,
  autoRecover: true,
});
app.controller('AdminCtrl', ["checkoutProcesses", function (checkoutProcesses) {
  this.checkout = checkoutProcesses.workflow('checkout');
}]);

Supervisors can also use a persistence adapter:

const persistence = {
  async load(id) {
    return database.load(id);
  },
  async save(id, snapshot) {
    await database.save(id, snapshot);
  },
};

autoPersist saves after every settled command. autoRecover restores the latest snapshot and reruns the latest recoverable failed command for each workflow.

Worker Runtime

The optional workflow worker runtime transports run, snapshot, and restore. It does not define another orchestration protocol:

const host = createWorkflowWorkerHost({
  workflows: { checkout },
});

const client = createWorkflowWorkerClient(workerConnection);
const result = await client.run('checkout', 'submit', order);

Runtime API

workflow.id;
workflow.state;
workflow.data;
workflow.diagnostics;
workflow.history;
workflow.can('submit');
workflow.run('submit', order);
workflow.cancel('submit');
workflow.snapshot();
workflow.restore(snapshot);

The runtime intentionally has no send(), manual lifecycle transition, or per-run orchestration options.

15 - Filters

Transform displayed values in templates without changing the stored value.

A filter formats a value for display. For example, a filter can sort a list or convert an object to JSON. Filters do not replace methods that change application state.

15.1 - Async

Render a promise result when it settles.

The async filter returns undefined while a promise-like value is pending. When it settles, AngularTS schedules change detection and returns the resolved value. A rejection is exposed as the settled value so the view can decide how to present it.

<p>{{ profilePromise | async }}</p>

Keep the same promise instance between render passes. Creating a promise in the template starts new work repeatedly; create it in a controller or model instead.

15.2 - Filter collections

Select collection items with a string, object, or predicate.

/**

  • @ngdoc filter
  • @name filter
  • @kind function
  • @description
  • Selects a subset of items from array and returns it as a new array.
  • @param {Array} array The source array.
  • Note: If the array contains objects that reference themselves, filtering is not possible.
  • @param {string|Object|function()} expression The predicate to be used for selecting items from
  • array.
  • Can be one of:
    • string: The string is used for matching against the contents of the array. All strings or
  • objects with string properties in `array` that match this string will be returned. This also
    
  • applies to nested object properties.
    
  • The predicate can be negated by prefixing the string with `!`.
    
    • Object: A pattern object can be used to filter specific properties on objects contained
  • by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
    
  • which have property `name` containing "M" and property `phone` containing "1". A special
    
  • property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match
    
  • against any property of the object or its nested object properties. That's equivalent to the
    
  • simple substring match with a `string` as described above. The special property name can be
    
  • overwritten, using the `anyPropertyKey` parameter.
    
  • The predicate can be negated by prefixing the string with `!`.
    
  • For example `{name: "!M"}` predicate will return an array of items which have property `name`
    
  • not containing "M".
    
  • Note that a named property will match properties on the same level only, while the special
    
  • `$` property will match properties on the same level or deeper. E.g. an array item like
    
  • `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but
    
  • **will** be matched by `{$: 'John'}`.
    
    • function(value, index, array): A predicate function can be used to write arbitrary filters.
  • The function is called for each element of the array, with the element, its index, and
    
  • the entire array itself as arguments.
    
  • The final result is an array of those elements that the predicate returned true for.
    
  • @param {function(actual, expected)|true|false} [comparator] Comparator which is used in
  • determining if values retrieved using `expression` (when it is not a function) should be
    
  • considered a match based on the expected value (from the filter expression) and actual
    
  • value (from the object in the array).
    
  • Can be one of:
    • function(actual, expected):
  • The function will be given the object value and the predicate value to compare and
    
  • should return true if both values should be considered equal.
    
    • true: A shorthand for strict value comparison.
  • This is essentially strict comparison of expected and actual.
    
    • false: A short hand for a function which will look for a substring match in a case
  • insensitive way. Primitive values are converted to strings. Objects are not compared against
    
  • primitives, unless they have a custom `toString` method (e.g. `Date` objects).
    
  • Defaults to false.
  • @param {string} [anyPropertyKey] The special property name that matches against any property.
  • By default `$`.
    
  • */