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.module() to create or retrieve modules. Passing a dependency array creates a module; passing only the name retrieves one.

const app = angular.module("myApp", ["ng"]);

app.service("UserService", UserService);

app.config(($locationProvider) => {
  $locationProvider.hashPrefix("!");
});

const existing = angular.module("myApp");

Calling angular.module("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"], {
    strictDi: true,
  });
});

The built-in ng module is prepended automatically. Use strictDi when code must be safe for minification.

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

Auto-Bootstrap

angular.init() scans an element or document for ng-app roots. The first root uses the current Angular instance. Additional roots are bootstrapped as sub-applications and stored in angular.subapps.

window.addEventListener("DOMContentLoaded", () => {
  angular.init(document);
});

You usually do not need to call this yourself for static pages because AngularTS runs auto-bootstrap when the script loads. Call it manually when dynamically adding new ng-app roots.

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.

import { defineAngularElement } from "@angular-wave/angular.ts/runtime/web-component";
import { ngClickDirective } from "@angular-wave/angular.ts/directives/events";

defineAngularElement("billing-summary", {
  ngModule: {
    directives: {
      ngClick: ngClickDirective,
    },
    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 integration boundaries 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.$t exposes public injection token strings as a typed object. Prefer it when writing $inject arrays in TypeScript.

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

2 - 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
  };
}]);
  <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 - 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 subsequent get returns the same instance.


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

// 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
}]);

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.module('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.module('myApp', []).controller('UserCtrl', UserCtrl);

Note: The $inject static property on a class is the TypeScript-idiomatic form of array annotation. When present, AngularTS reads it in preference to inspecting function argument names.

Strict DI mode

Passing { strictDi: true } to angular.bootstrap() (or adding the strict-di attribute to the ng-app element) disables implicit annotation. Every injectable must carry explicit annotations. This catches minification bugs at development time.

<div ng-app="myApp" strict-di></div>

Under strict DI, any function that reaches the injector without annotations throws immediately — even if the un-annotated function would have worked in development.

Injectable types

value

A fixed JavaScript value. Registered with $provide.value(). 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.module('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.

Decorating existing services

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

  .config(['$provide', function ($provide) {
    $provide.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

Config blocks can inject providers and constants only. Run blocks have access to all services.

// Config — providers only
app.config(['$httpProvider', function ($httpProvider) {
  $httpProvider.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.

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

Creating and retrieving modules

Use angular.module() to both create and look up modules. The presence of the second argument — the requires array — is what distinguishes creation from retrieval.

TypeScript

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

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

JavaScript

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

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

Warning: Calling angular.module('myApp', []) a second time replaces the existing module. Always pass the requires array only once — at the point of creation — and omit it everywhere else.

Module registration methods

The NgModule instance returned by angular.module() 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(fn)

Runs during the config phase, before any services are instantiated. Only providers and constants can be injected.

.run(fn)

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

.state(definition) / .state(name, definition)

Registers a router state through $stateProvider during the config phase. This is equivalent to using .config(['$stateProvider', ...]), but keeps route declarations in the same fluent module chain as components and services. The method records only a provider-token invocation; it does not import router implementation code into custom runtimes unless that runtime actually includes the router provider and loads a module that calls .state().

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.module('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); },
  };
}]);
class UserService {
  static $inject = ['$http', 'API_BASE', 'logger'];

  constructor(
    private $http: ng.HttpService,
    private base: string,
    private logger: { info: (m: string) => void },
  ) {}

  getAll() {
    this.logger.info('Fetching users');
    return this.$http.get(`${this.base}/users`);
  }
}

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

app.service('userService', UserService);

app.controller('UserListController', [
  '$scope', 'userService',
  function ($scope, userService) {
    $scope.users = [];

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

app.state('users', {
  url: '/users',
  template: '<user-list></user-list>',
});

app.config(['$locationProvider', function ($locationProvider) {
  $locationProvider.html5Mode(true);
}]);

app.run(['logger', function (logger) {
  logger.info('Application started');
}]);

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
$sceStrict contextual escaping
$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:

// Register an injectable reactive mode machine
app.machine('sessionMachine', {
  initial: 'setup',
  data: {},
  transitions: {},
});

// Register machine/workflow config from DI-backed factories
app.value('settings', {
  sessionMode: 'setup',
});

function machineConfig(settings) {
  return {
    initial: settings.sessionMode,
    data: { roomId: '' },
    transitions: {
      setup: {
        join(data, message) {
          data.roomId = message.roomId;

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

machineConfig.$inject = ['settings'];

app.machine('sessionMachine', machineConfig);

function workflowConfig(settings) {
  return {
    id: 'onboarding',
    initial: 'idle',
    data: { sessionMode: settings.sessionMode },
    transitions: {
      idle: {
        start(data) {
          data.sessionMode = 'running';

          return 'running';
        },
      },
    },
  };
}

workflowConfig.$inject = ['settings'];

app.workflow('onboardingWorkflow', workflowConfig);

app.wasm('mathLib', '/wasm/math.wasm', {});

// Register a Web Worker connection
app.worker('backgroundWorker', '/workers/bg.js');

// Register a REST resource backed by $rest
app.rest('Post', '/api/posts', Post);

// Register a persisted service (session, local, or cookie storage)
app.store('prefs', UserPreferences, 'local');

// Register a pre-configured SSE stream
app.sse('notifications', '/events/stream');

// Register a pre-configured WebSocket connection
app.websocket('chat', 'wss://chat.example.com/ws');

These methods all push entries onto the module’s internal invoke queue. They are resolved by the injector during bootstrap exactly like any other $provide registration.

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

$scope.$scopename = 'userPanel';

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

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

6 - 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 subsequent 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 via $interpolateProvider in a config block:

  $interpolateProvider.startSymbol = '{[';
  $interpolateProvider.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 SCE

When binding HTML content directly into the DOM, AngularTS enforces Strict Contextual Escaping (SCE). Plain interpolation always produces text, never HTML. To bind HTML you must use ng-bind-html with a trusted value:

<p>{{ userComment }}</p>

<!-- To render HTML, mark it as trusted first -->
<p ng-bind-html="trustedHtml"></p>
  '$scope', '$sce',
  function ($scope, $sce) {
    // Explicitly trust HTML from a known-safe source
    $scope.trustedHtml = $sce.trustAsHtml('<strong>Bold</strong> text');
  },
]);

Warning: Never call $sce.trustAsHtml() on user-provided input. Only use it on HTML that your application controls — such as server-rendered content that has already been sanitized.

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.