This is the multi-page printable view of this section. Click here to print.
Core Concepts
1 - Angular Runtime
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
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:
- O(1) lookup: Listeners are stored in a
Map<string, Listener[]>keyed by property name. Whencountchanges, only listeners registered under'count'are scheduled — not every watcher in the application. - Microtask scheduling:
_scheduleListenercallsqueueMicrotask(), which defers the flush until after the current synchronous call stack completes. Multiple changes to the same property in the same tick are coalesced. - Recursive proxying: When you assign an object —
$scope.user = { name: 'Alice' }— the new value is itself wrapped in aProxy. 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 mechanism | Dirty-checking digest loop | ES6 Proxy set trap |
| Watcher lookup cost | O(n) — all watchers checked | O(1) — direct Map lookup by property name |
| Trigger | Manual digest entry required for async code | Automatic — Proxy intercepts every assignment |
| Update granularity | All watchers, all the time | Only bindings for the changed property |
| Nested objects | Shallow by default; $watchCollection needed for deep | Deep — nested objects are automatically proxied |
| Async code | Must manually enter change detection | No 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
$applywrapper 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: trueon 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:
$watchon 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_constantflag 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
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.
Array annotation (recommended)
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
$injectstatic 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
Providerto its name:greeterProviderfor thegreeterservice. 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
| Method | Description |
|---|---|
$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
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 therequiresarray 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:
| Token | Description |
|---|---|
$rootScope | The root of the scope hierarchy |
$compile | Template compiler |
$http | HTTP client |
$interpolate | Template interpolation |
$parse | Expression parser |
$filter | Filter registry |
$animate | Animation support |
$location | URL management |
$machine | Reactive mode machines |
$sce | Strict contextual escaping |
$state | Router state service |
$eventBus | Pub/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
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:
$watchon 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_constantflag 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:
- Broadcasts
$destroydownward to all children. - Removes all watcher registrations for this scope’s ID from the shared
_watchersmap. - Clears all
$onlisteners. - Removes itself from the parent’s
_childrenarray. - Sets
_destroyed = trueand 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
| Method | Description |
|---|---|
$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
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
| Property | Type | Description |
|---|---|---|
_constant | boolean | true if the expression is a literal constant |
_literal | boolean | true if the expression is a simple literal |
_assign | function? | Present for l-value expressions; assigns a value to the context |
_inputs | any[]? | Sub-expressions tracked for change detection |
_decoratedNode | BodyNode | The 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.