This is the multi-page printable view of this section. Click here to print.
Service API
- 1: $anchorScroll
- 2: $compile
- 3: $controller
- 4: $cookie
- 5: $eventBus
- 6: $exceptionHandler
- 7: $http
- 8: $interpolate
- 9: $location
- 10: $log
- 11: $machine
- 12: $parse
- 13: $rest
- 14: $rootElement
- 15: $rootScope
- 16: $sce
- 17: $templateCache
- 18: $templateRequest
- 19: $url
- 20: $workflow
1 - $anchorScroll
Description
$anchorScroll service scrolls to the element hash or (if omitter) to the current value of $location.getHash(), according to the rules speciffied in the HTML spec.
It also watches the URL hash and automatically scrolls to any matched anchor whenever it is changed by $location. This can be disabled by a setting on $anchorScrollProvider.
Additionally, you can use the yOffset property to specify a vertical scroll-offset (either fixed or dynamic).
Example
<style>
#scrollArea {
height: 100px;
overflow: auto;
}
#bottom {
display: block;
margin-top: 2000px;
}
</style>
<section ng-app="demo">
<div id="scrollArea" ng-controller="ScrollController">
<button class="bottom" ng-click="gotoBottom()">Go to bottom</button>
<a id="bottom"></a> You're at the bottom!
</div>
</section>
window.angular.module('demo', []).controller('ScrollController', [
'$scope',
'$location',
'$anchorScroll',
function ($scope, $location, $anchorScroll) {
$scope.gotoBottom = function () {
// set the location.hash to the id of
// the element you wish to scroll to.
$location.setHash('bottom');
// call $anchorScroll()
$anchorScroll();
};
},
]);
Demo
The example below illustrates the use of a vertical scroll-offset (specified as a fixed value).
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
.module('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) {
// set the $location.hash to `newHash` and
// $anchorScroll will automatically scroll to it
$location.setHash('anchor' + x);
} else {
// call $anchorScroll() explicitly,
// since $location.hash hasn't changed
$anchorScroll();
}
};
},
]);
window.angular.bootstrap(document.getElementById('demo2'), ['demo2']);
Demo
For detailed method description, see $anchorScroll.
2 - $compile
3 - $controller
4 - $cookie
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.module('app').controller(
'UserCtrl',
/** @param {ng.CookieService} $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 using
$cookieProvider.defaults.
Cookies are limited to roughly 4 KB, including key and value, so void storing
large objects; prefer identifiers or short tokens.
For detailed method description, see CookieService
5 - $eventBus
Description
A messaging service, backed by an instance of
PubSub. This implementation is based on
the original
Google Closure PubSub
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.module('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
For detailed method description, see PubSub
6 - $exceptionHandler
Description
$exceptionHandler is the central hook for uncaught exceptions inside an AngularTS application.
The framework routes all errors from synchronous code, async tasks, expression evaluation, and dependency injection through this service.
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.
IMPORTANT: In AngularJS, $exceptionHandler only caught errors in expressions and
logged them to the console, using a type signature of $exceptionHandler(exception, [cause]).
AngularTS treats $exceptionHandler as a single error sink, fails eagerly, and assumes that the Error object or one of its subclasses provides all the context necessary for the client.
For type description, see ng.ExceptionHandler.
For service customisation, see ng.ExceptionHandlerProvider
7 - $http
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 $httpProvider.defaults, or mutate
runtime defaults through $http.defaults.
angular.module('app', []).config(($httpProvider) => {
$httpProvider.defaults.headers.common.Authorization = 'Bearer token';
$httpProvider.defaults.withCredentials = true;
});
Default response transforms strip AngularJS JSON protection prefixes and parse JSON responses automatically.
Interceptors
Interceptors are registered with $httpProvider.interceptors. Request hooks run
in registration order; response hooks run in reverse order.
angular.module('app', []).config(($httpProvider) => {
$httpProvider.interceptors.push(() => ({
request(config) {
config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
return config;
},
responseError(rejection) {
if (rejection.status === 401) {
window.location.href = '/login';
}
return Promise.reject(rejection);
},
}));
});
XSRF
$http reads the configured XSRF cookie and sends it in the configured XSRF
header for trusted origins. Add cross-origin APIs explicitly:
angular.module('app', []).config(($httpProvider) => {
$httpProvider.xsrfTrustedOrigins.push('https://api.example.com');
});
Related
8 - $interpolate
9 - $location
$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.path();
$location.search();
$location.hash();
$location.url();
$location.absUrl();
Update The URL
$location
.path("/settings")
.search({ tab: "profile" })
.hash("details");
Setter methods return $location, so updates can be chained.
Navigation Events
$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 });
});
For provider configuration, see $locationProvider.
10 - $log
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.module('demo').controller('MyController', ($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 logdebugmessages. You can use$logProvider.debugto change this.
For configuration and custom implementations, see $logProvider.
Decorating $log
You can also optionally override any of the $log service methods with
$provide decorator. Below is a simple example that overrides default
$log.error to log errors to both console and a backend endpoint.
Example*
angular.module('demo').config(($provide) => {
$provide.decorator('$log', ($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.module('demo').run(($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
11 - $machine
$machine
$machine creates a small reactive mode 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: current 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 modes.
Use $workflow when the flow also
needs command boundaries, diagnostics, snapshots, restore, retry, or repeat.
Create a Machine
Inject $machine and assign the created machine to a controller or scope
property:
app.controller('SessionCtrl', function ($machine) {
this.session = $machine({
initial: 'setup',
data: {
roomId: '',
error: '',
},
transitions: {
setup: {
join(data, message) {
data.roomId = message.roomId;
return 'waiting';
},
},
waiting: {
matched(data, message) {
data.roomId = message.roomId;
return 'playing';
},
unavailable(data, reason) {
data.error = reason;
return 'setup';
},
},
},
});
});
Docs call current a mode. Transition handlers return a mode string to move
to another mode.
Register a Named Machine
Use module.machine(name, config) when a machine should be injectable by name:
app.machine('sessionMachine', {
initial: 'setup',
data: {
roomId: '',
},
transitions: {
setup: {
join(data, message) {
data.roomId = message.roomId;
return 'waiting';
},
},
},
});
app.controller('SessionCtrl', function (sessionMachine) {
this.session = sessionMachine;
});
You can also provide a resolvable config factory so it can read injectables at registration time:
function sessionMachineConfig(appSettings) {
return {
initial: appSettings.initialMode,
data: {
roomId: '',
error: '',
},
transitions: {
setup: {
join(data, message) {
data.roomId = message.roomId;
return 'waiting';
},
},
},
};
}
sessionMachineConfig.$inject = ['appSettings'];
app.machine('sessionMachine', sessionMachineConfig);
This is useful when defaults depend on environment, user state, 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.current; // current mode string
session.data; // reactive data object
session.matches('setup'); // true when current mode is setup
session.can('join', payload); // true when current mode can run join
session.send('join', payload);
session.snapshot(); // structured clone of { current, data }
session.restore(snapshot);
send(type, payload) returns true when a transition entry exists for the
current mode, its optional guard passes, and its handler runs. Missing
transitions and blocked guarded transitions return false and do not throw.
$machine does not record diagnostics for blocked or missing transitions; use
$workflow when failures need structured evidence, retry, or recovery.
Transition return values:
- return a mode string to change
current - return
false,undefined, an empty string, a non-string value, or no value to stay in the current mode
can(type, payload) checks the transition table and any optional guard for the
current mode. It does not run the transition target. Guards should be
side-effect-free because templates may call can() repeatedly.
Guarded Transitions
Use a transition descriptor 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: '',
},
transitions: {
setup: {
join: {
guard(data, message) {
return !!data.inviteCode && message.roomId !== '';
},
target(data, message) {
data.roomId = message.roomId;
return 'waiting';
},
},
},
},
});
session.can('join', { roomId: 'abc' }); // evaluates guard only
session.send('join', { roomId: 'abc' }); // runs target when guard passes
The original function shorthand remains valid:
join(data, message) {
data.roomId = message.roomId;
return 'waiting';
}
Guards and transitions are synchronous. Put async work, retries, diagnostics,
and command recovery in $workflow,
then use a machine transition to project the resulting mode/data into the UI.
Example: Tic Tac Toe
A machine is useful when user actions must follow a small set of legal modes.
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 mode, so further moves return false.
app.controller('GameCtrl', function ($machine) {
const wins = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
const winnerOf = (board) => {
for (const [a, b, c] of wins) {
const mark = board[a];
if (mark !== '-' && mark === board[b] && mark === board[c]) {
return mark;
}
}
return '';
};
this.game = $machine({
initial: 'playing',
data: {
board: ['-', '-', '-', '-', '-', '-', '-', '-', '-'],
nextPlayer: 'X',
winner: '',
moveCount: 0,
lastError: '',
},
transitions: {
playing: {
move(data, { index }) {
if (
!Number.isInteger(index) ||
index < 0 ||
index >= data.board.length ||
data.board[index] !== '-'
) {
data.lastError = 'invalid_move';
return false;
}
const player = data.nextPlayer;
data.board[index] = player;
data.moveCount += 1;
data.lastError = '';
const winner = winnerOf(data.board);
if (winner) {
data.winner = winner;
return winner === 'X' ? 'xWon' : 'oWon';
}
if (data.moveCount === data.board.length) {
return 'draw';
}
data.nextPlayer = player === 'X' ? 'O' : 'X';
return 'playing';
},
},
},
});
});
<button
ng-repeat="cell in $ctrl.game.data.board track by $index"
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. A valid move mutates
data.board, updates whose turn is next, and returns the next mode. An invalid
move records lastError and returns false, leaving current unchanged.
Machine transitions do not roll back data mutations when they return false or
throw.
Persist With Transition Hooks
Transition hooks are a convenient place to persist the game after each handled
move. The hook runs after current has been updated, so the snapshot contains
the terminal mode when the move ends the game.
app.controller('GameCtrl', function ($machine) {
const storageKey = 'tic-tac-toe';
this.game = $machine({
initial: 'playing',
data: {
board: ['-', '-', '-', '-', '-', '-', '-', '-', '-'],
nextPlayer: 'X',
winner: '',
moveCount: 0,
lastError: '',
},
transitions: {
playing: {
move(data, { index }) {
// Same move logic as above.
},
},
},
hooks: {
transition({ machine }) {
localStorage.setItem(storageKey, JSON.stringify(machine.snapshot()));
},
},
});
const saved = localStorage.getItem(storageKey);
if (saved) {
this.game.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 machine state.
Snapshots
Use snapshot() to capture the durable machine state. A snapshot is a
structured clone of { current, data }:
const snapshot = session.snapshot();
localStorage.setItem('session', JSON.stringify(snapshot));
A snapshot contains only current 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 does not run transition hooks.
Hooks
Use hooks.enter and hooks.exit for mode-specific effects. Use
hooks.transition for logging or cross-cutting work that should run after any
handled transition, including same-mode transitions:
const session = $machine({
initial: 'setup',
data: {
status: 'idle',
},
transitions: {
setup: {
join() {
return 'waiting';
},
},
waiting: {
matched() {
return '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 current changes, enter runs after current changes, and
transition runs after mode-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/current 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 mode 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.
An explicit $machine($scope, config) call is also supported when a caller
wants to bind the machine to a scope immediately, but assigning the machine to a
controller or scope property is usually enough. Named machines registered with
module.machine() follow the same rule: they bind when a scope proxy observes
them.
TypeScript
import { defineMachine } from '@angular-wave/angular.ts';
type SessionData = {
roomId: string;
error: string;
};
type SessionEvents = {
join: { roomId: string };
fail: string;
reset: undefined;
};
const config = defineMachine<SessionData, SessionEvents>({
initial: 'setup',
data: {
roomId: '',
error: '',
} satisfies SessionData,
transitions: {
setup: {
join(data, message) {
data.roomId = message.roomId;
return 'waiting';
},
},
},
});
const session = $machine(config);
session.send('join', { roomId: 'abc' });
session.send('reset');
Machine definitions are strict by default in TypeScript. If no event map is
provided, send() has no valid event names and transition maps accept no event
keys. Use defineMachine<Data, Events>() for checked event names and payloads,
or opt into ng.MachineEventMap when you intentionally need dynamic event
names.
12 - $parse
13 - $rest
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' });
const deleted = await posts.delete(42);
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:
import {
CachedRestBackend,
HttpRestBackend,
} from '@angular-wave/angular.ts/services/rest';
import type {
RestCacheStore,
RestResponse,
} from '@angular-wave/angular.ts/services/rest';
class MapRestCacheStore implements RestCacheStore {
private cache = new Map<string, RestResponse<unknown>>();
async get<T>(key: string): Promise<RestResponse<T> | undefined> {
return this.cache.get(key) as RestResponse<T> | undefined;
}
async set<T>(key: string, response: RestResponse<T>): Promise<void> {
this.cache.set(key, response as RestResponse<unknown>);
}
async delete(key: string): Promise<void> {
this.cache.delete(key);
}
async deletePrefix(prefix: string): Promise<void> {
for (const key of this.cache.keys()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
}
}
}
}
const posts = $rest<Post, number>('/api/posts', Post, {
backend: new CachedRestBackend({
network: new HttpRestBackend($http),
cache: new MapRestCacheStore(),
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.
Provider Registration
Register definitions during configuration with $restProvider.rest() when a
resource should be available from shared setup code.
angular.module('app', []).config(($restProvider) => {
$restProvider.rest('posts', '/api/posts', Post);
});
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.
Related
14 - $rootElement
15 - $rootScope
16 - $sce
$sceProvider and $sce Documentation
$sceProvider
The $sceProvider provider allows developers to configure the $sce
service.
- Enable/disable Strict Contextual Escaping (SCE) in a module
- Override the default implementation with a custom delegate
Read more about Strict Contextual Escaping (SCE).
$sce
$sce is a service that provides Strict Contextual Escaping (SCE) services
to AngularTS.
Strict Contextual Escaping
Overview
Strict Contextual Escaping (SCE) is a mode in which AngularTS constrains bindings to only render trusted values. Its goal is to:
- Help you write secure-by-default code.
- Simplify auditing for vulnerabilities such as XSS and clickjacking.
By default, AngularTS treats all values as untrusted in HTML or sensitive URL bindings. When binding untrusted values, AngularTS will:
- Sanitize or validate them based on context.
- Or throw an error if it cannot guarantee safety.
Example — ng-bind-html renders its value directly as HTML (its “context”). If
the input is untrusted, AngularTS will sanitize or reject it.
To bypass sanitization, you must mark a value as trusted before binding it.
Note: Since version 1.2, AngularTS ships with SCE enabled by default.
Example: Binding in a Privileged Context
<input ng-model="userHtml" aria-label="User input" />
<div ng-bind-html="userHtml"></div>
If SCE is disabled, this allows arbitrary HTML injection — a serious XSS risk.
To safely render user content, you should sanitize the HTML (on the server or client) before binding it.
SCE ensures that only trusted, validated, or sanitized values are rendered.
You can mark trusted values explicitly using:
$sce.trustAs(context, value);
or shorthand methods such as:
$sce.trustAsHtml(value);
$sce.trustAsUrl(value);
$sce.trustAsResourceUrl(value);
How It Works
Directives and Angular internals bind trusted values using:
$sce.getTrusted(context, value);
Example: the ngBindHtml directive uses $sce.parseAsHtml internally:
let ngBindHtmlDirective = [
'$sce',
function ($sce) {
return function (scope, element, attr) {
scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function (value) {
element.html(value || '');
});
};
},
];
Impact on Loading Templates
SCE affects both:
- The
ng-includedirective - The
templateUrlproperty in directives
By default, AngularTS loads templates only from the same domain and protocol
as the main document.
It uses:
$sce.getTrustedResourceUrl(url);
To allow templates from other domains, use:
$sceDelegateProvider.trustedResourceUrlList- Or
$sce.trustAsResourceUrl(url)
Note: Browser CORS and Same-Origin policies still apply.
Is This Too Much Overhead?
SCE applies only to interpolation expressions.
Constant literals are automatically trusted, e.g.:
<div ng-bind-html="'<b>implicitly trusted</b>'"></div>
If the ngSanitize module is included, $sceDelegate will use $sanitize to
clean untrusted HTML automatically.
AngularTS’ default $sceDelegate allows loading from your app’s domain,
blocking others unless explicitly whitelisted.
This small overhead provides major security benefits and simplifies auditing.
Supported Trusted Contexts
| Context | Description |
|---|---|
$sce.HTML | Safe HTML (used by ng-bind-html). |
$sce.CSS | Safe CSS. Currently unused. |
$sce.MEDIA_URL | Safe media URLs (auto-sanitized). |
$sce.URL | Safe navigable URLs. |
$sce.RESOURCE_URL | Safe resource URLs (used in ng-include, iframe, etc.). |
$sce.JS | Safe JavaScript for execution. |
⚠️ Before AngularTS 1.7.0,
a[href]andimg[src]sanitized directly.
As of 1.7.0, these now use$sce.URLand$sce.MEDIA_URLrespectively.
Resource URL List Patterns
Trusted and banned resource URL lists accept:
'self'→ matches same domain & protocol- Strings with wildcards:
*→ matches within a single path segment**→ matches across path segments (use carefully)
- Regular expressions (
RegExp)
Caution: Regex patterns are powerful but harder to maintain — use only when necessary.
Disabling SCE (Not Recommended)
You can disable SCE globally — though this is strongly discouraged.
angular.module('myAppWithSceDisabled', []).config(function ($sceProvider) {
// Completely disable SCE. For demonstration purposes only!
// Do not use in new projects or libraries.
$sceProvider.enabled(false);
});
17 - $templateCache
$templateCache is a
Map
object created by $templateCacheProvider.
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.module('myApp', []).run(($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',
class {
constructor($templateCache) {
const tmp = $templateCache.get('templateId.html');
}
},
);
18 - $templateRequest
19 - $url
20 - $workflow
$workflow
$workflow adds command boundaries, diagnostics, history, snapshots, restore,
retry, and repeat on top of $machine.
Use $machine when you only need legal modes and transitions. Use $workflow
when external work needs to be run, inspected, repaired, repeated, or handed to
another process as JSON.
Create a Workflow
Inject $workflow and assign the workflow to a controller or scope property:
app.controller('DocsCtrl', function ($workflow) {
this.build = $workflow({
id: 'docs-build',
initial: 'idle',
data: {
status: 'idle',
output: '',
},
transitions: {
idle: {
start(data) {
data.status = 'running';
return 'running';
},
},
running: {
complete(data, output) {
data.status = 'complete';
data.output = output;
return 'complete';
},
fail(data, reason) {
data.status = reason;
return 'failed';
},
},
},
commands: {
build({ workflow, data, input }) {
workflow.send('start');
data.output = String(input);
workflow.send('complete', data.output);
return {
ok: true,
output: {
file: data.output,
},
};
},
},
});
});
workflow.current is the current mode. workflow.data is reactive data for
templates. workflow.run(name, input) is the explicit boundary for work that
can succeed, fail, or be retried.
Commands may be synchronous or async. run(), retry(), and repeat() always
return a promise that resolves to a normalized WorkflowCommandResult.
Register a Named Workflow
Use module.workflow(name, config) when a workflow should be injectable by
name:
app.workflow('docsWorkflow', {
id: 'docs',
initial: 'idle',
data: {
runs: 0,
},
transitions: {
idle: {
start(data) {
data.runs += 1;
return 'running';
},
},
},
});
app.controller('DocsCtrl', function (docsWorkflow) {
this.workflow = docsWorkflow;
});
You can make workflow registration resumable and environment-driven by passing a config factory:
function docsWorkflowConfig(buildConfig) {
return {
id: buildConfig.workflowId,
initial: buildConfig.initialMode,
data: {
runs: 0,
status: buildConfig.initialMode,
},
transitions: {
idle: {
start(data) {
data.runs += 1;
data.status = 'running';
return 'running';
},
},
},
commands: {},
};
}
docsWorkflowConfig.$inject = ['buildConfig'];
app.workflow('docsWorkflow', docsWorkflowConfig);
Named workflows are DI singletons for an injector. Observing scopes can be destroyed without destroying the workflow instance.
Command Diagnostics
Commands return a WorkflowCommandResult, or they can throw. Thrown values are
converted to structured diagnostics and partial data mutations are preserved:
const result = await workflow.run('publish', 'index.html');
if (!result.ok) {
for (const diagnostic of result.diagnostics) {
console.warn(diagnostic.code, diagnostic.message);
}
}
Diagnostics are safe to serialize:
const diagnosticsJson = JSON.stringify(workflow.diagnostics);
Snapshot And Restore
Snapshots are the JSON handoff format:
const snapshot = workflow.snapshot();
localStorage.setItem('docsWorkflow', JSON.stringify(snapshot));
const restored = JSON.parse(localStorage.getItem('docsWorkflow'));
workflow.restore(restored);
A snapshot contains:
{
version: 1,
id: 'docs-build',
current: 'failed',
data: {},
diagnostics: [],
history: []
}
restore(snapshot) requires version 1 and a matching workflow id. Restored
diagnostics and history entries are normalized into the same JSON-safe shape
that live commands produce. Restored history IDs are normalized to unique
positive integers. Command inputs and outputs in workflow.history and
snapshots are stored as JSON-safe projections. Live workflows still retry or
repeat with the original input value; after restore(snapshot), retry and
repeat use the serialized input from the snapshot.
Use migrateSnapshot(snapshot) to restore older persisted shapes:
const workflow = $workflow({
id: 'docs-build',
initial: 'idle',
data: {
title: '',
},
transitions: {},
migrateSnapshot(snapshot) {
return {
version: 1,
id: 'docs-build',
current: snapshot.state,
data: {
title: snapshot.title,
},
diagnostics: [],
history: [],
};
},
});
Retry, Repeat, And Repair
retry(commandName?) reruns the latest failed command with its original input:
const retryResult = await workflow.retry('publish');
repeat(commandName?) reruns the latest completed command with its original
input:
const repeatResult = await workflow.repeat('publish');
Repair is intentionally configured as a normal command instead of a magic built-in policy:
app.controller('DocsCtrl', function ($workflow) {
this.workflow = $workflow({
id: 'repairable-docs',
initial: 'idle',
data: {
title: '',
},
transitions: {
idle: {
validate(data) {
return data.title ? 'complete' : 'failed';
},
},
failed: {
complete() {
return 'complete';
},
},
},
commands: {
validate({ workflow }) {
workflow.send('validate');
return workflow.matches('complete')
? { ok: true }
: {
ok: false,
diagnostics: [
{
code: 'docs.missingTitle',
message: 'Missing title.',
recoverable: true,
},
],
};
},
repair({ workflow, data, input }) {
data.title = String(input);
workflow.send('complete');
return {
ok: true,
};
},
},
});
});
await workflow.run('validate');
await workflow.run('repair', 'Guide');
Diagnostics and history are append-only in v1. Repair, retry, and repeat add evidence; they do not erase the commands that made recovery or replay necessary.
Production Policies
Concurrent command calls are allowed by default. Set concurrency to reject or
queue overlapping runs for the same command:
const workflow = $workflow({
id: 'publish',
concurrency: 'queue',
initial: 'idle',
data: {},
transitions: {},
commands: {
publish() {
return { ok: true };
},
},
});
Per-run options can override the workflow default:
await workflow.run('publish', payload, { concurrency: 'reject' });
Use commandTimeout or per-run timeout to fail commands that exceed a time
budget. Timeout and cancellation resolve the command result with diagnostics;
commands receive an AbortSignal so async work can stop early:
const workflow = $workflow({
id: 'publish',
commandTimeout: 5000,
initial: 'idle',
data: {},
transitions: {},
commands: {
async publish({ cleanup, signal }) {
const controller = new AbortController();
cleanup(() => controller.abort());
signal.addEventListener('abort', () => controller.abort(), {
once: true,
});
await fetch('/publish', { signal: controller.signal });
return { ok: true };
},
},
});
workflow.cancel('publish');
cleanup(callback) runs after the command resolves, fails, times out, or is
cancelled. Use it for timers, event listeners, observers, and request
controllers created by the command.
restore(snapshot) is a hard recovery boundary. It cancels running commands,
prevents queued commands from starting, and ignores late writes from cancelled
command continuations. Command code should still observe signal.aborted so it
can release external resources promptly.
Diagnostics and history are bounded to 1000 entries by default. Use
diagnosticLimit and historyLimit to choose different bounds:
const workflow = $workflow({
id: 'bounded',
diagnosticLimit: 100,
historyLimit: 100,
initial: 'idle',
data: {},
transitions: {},
});
Runtime API
workflow.current;
workflow.data;
workflow.diagnostics;
workflow.history;
workflow.matches('idle');
workflow.can('start');
workflow.send('start');
workflow.run('publish', payload, { timeout: 5000 });
workflow.retry('publish', { concurrency: 'reject' });
workflow.repeat('publish');
workflow.cancel('publish');
workflow.snapshot();
workflow.restore(snapshot);
TypeScript callers can specify the expected command output type at the command boundary:
const result = await workflow.run<{ file: string }>('publish', payload);
if (result.ok) {
result.output?.file;
}
Workflow definitions are strict by default in TypeScript. If no event or command
map is provided, send(), run(), retry(), and repeat() have no valid
names. Use defineWorkflow<Data, Events, Commands>() and defineCommand() for
checked event names, command names, inputs, and outputs:
import { defineCommand, defineWorkflow } from '@angular-wave/angular.ts';
type DocsData = {
output: string;
};
type DocsEvents = {
complete: { output: string };
};
type DocsCommands = {
publish: ng.WorkflowCommand<DocsData, string, { file: string }, DocsEvents>;
};
const config = defineWorkflow<DocsData, DocsEvents, DocsCommands>({
id: 'docs',
initial: 'idle',
data: {
output: '',
} satisfies DocsData,
transitions: {
idle: {
complete(data, payload) {
data.output = payload.output;
return 'complete';
},
},
},
commands: {
publish: defineCommand<DocsData, string, { file: string }, DocsEvents>(
({ workflow, input }) => {
workflow.send('complete', { output: input });
return {
ok: true,
output: {
file: input,
},
};
},
),
},
});
const workflow = $workflow(config);
const result = await workflow.run('publish', 'index.html');
When a command map is provided, commands is required and every declared
command key must be present and callable. This keeps workflow.run('name')
aligned with the runtime command table.
When a command calls another command, pass the full command map to
defineCommand() so context.workflow.run() is checked too:
type DocsCommands = {
build: ng.WorkflowCommand<
DocsData,
string,
{ file: string },
DocsEvents,
DocsCommands
>;
publish: ng.WorkflowCommand<
DocsData,
{ file: string },
{ url: string },
DocsEvents,
DocsCommands
>;
};
const build = defineCommand<
DocsData,
string,
{ file: string },
DocsEvents,
DocsCommands
>(({ workflow, input }) => {
workflow.run('publish', { file: input });
return {
ok: true,
output: {
file: input,
},
};
});
Use ng.WorkflowCommandMap only when you intentionally need dynamic command
names. Dynamic command inputs are typed as unknown; narrow the value inside
the command before using it.