Task guides
Implement complete AngularTS workflows with explicit state, failure, lifecycle, accessibility, and verification behavior.
These guides extend one task-board application. They explain implementation
boundaries rather than presenting isolated snippets.
Start with server-rendered HTML
Do not start with an SPA. Keep the model, rules, URLs, and rendering together on
the server. Send useful HTML first, then add AngularTS to the smallest region
that needs browser behavior.
Even when using the router, prefer server-rendered route fragments. Move the
model into the browser only for the most complex client-owned features. When the
browser takes over model consistency and compilation, use typed programmatic
views as the primary view style. REST services, explicit caching, and a
browser-facing security policy also become primary at that same threshold. Until
then, prefer normal HTTP pages, forms, and server-rendered fragments.
Apply this decision per module. A server-rendered login and public site can live
beside a dashboard SPA in the same application. The dashboard’s complexity does
not require every other module to move into the browser.
Read Keep the application on the server before choosing the
application structure.
Build the application
- Keep the application on the server
- Structure the application
- Build and validate a form
- Load server data
- Add routing
- Handle errors
- Test behavior
Make it production-ready
Start with the complete tutorial if you have not built an
AngularTS component.
1 - Keep the application on the server
Render HTML and keep domain logic on the server until the browser must own a genuinely complex application model.
Start with HTML from the server
Do not start by building an SPA. Render the useful page on the server and use
normal links and forms. Keep domain models, validation, authorization,
transactions, URLs, and HTML generation together for as long as possible.
This is the simplest way to keep the model consistent. The server reads current
data, applies the rules, and renders the result in one place. The browser
receives HTML it can display immediately. There is no second model to
synchronize, no client startup request for content the server already had, and
no duplicated loading, routing, validation, and error behavior.
HTML and server rendering are the default application architecture. AngularTS is
there to add behavior where the browser can improve an already useful page.
Add behavior without taking over the page
Put ng-app around the smallest region that needs local interaction. Use scope,
events, effects, HTTP directives, and DOM references for immediate browser
behavior while the server continues to own the page and its durable model.
Keep these on the server:
- Initial and durable application state
- Validation and authorization
- Data loading and mutations
- Primary URLs and navigation
- HTML for complete pages and reusable fragments
- Error, empty, permission, and maintenance responses
Do not fetch JSON after startup merely to rebuild HTML that the server could
have sent in the first response.
Keep router content server-rendered
Using the AngularTS router does not require moving rendering or the model into
the browser. Let the router coordinate transitions and component lifetimes, but
keep route content as server-rendered HTML fragments while that remains
practical.
This preserves one server model while still allowing faster transitions,
long-lived browser resources, and local interaction state. A router is not a
reason to duplicate server templates as client templates.
Move the model only as a last resort
Move model ownership into the browser only when the feature genuinely needs it.
Examples include substantial offline work, high-frequency local editing, complex
cross-screen state, or an application-like workspace whose model must remain
alive independently of server navigation.
Before making that move, confirm that server-rendered fragments, focused
interactive regions, or a small client model cannot solve the problem. Most
pages should never cross this line.
Move compilation with the model
Once the browser owns the model, it also owns model consistency and view
compilation. Do not keep a complex client model on one side and loosely typed
HTML template bindings on the other.
For a client-owned screen, optional typed programmatic views can keep model access, DOM properties, events, and
generated structure in the same checked TypeScript code. HTML templates remain
available for client-owned screens and are the default for server-owned pages,
server-rendered fragments, and small enhancements.
Move the data boundary with the model
Before the browser owns the model, prefer simple HTTP: normal navigation, form
submissions, and server-rendered HTML fragments. The server already has the
authoritative data and can return the final representation directly.
Once the browser owns a complex application model, REST services become the
primary server boundary. At that point the frontend also needs an explicit cache
strategy and security policy:
- Define stable request, response, validation, pagination, and error contracts.
- Decide which model owns each cached record and how writes invalidate it.
- Handle stale data, concurrent writes, retries, and offline state deliberately.
- Define credential handling, authorization failures, CSRF or token policy,
cross-origin access, content security, and how client code avoids secrets.
Do not introduce this REST, cache, and security machinery for a page that can
submit a form or request a server-rendered fragment. It becomes primary only
when the browser has genuinely taken ownership of the model.
Let modules use different architectures
The ownership decision is not application-wide. AngularTS applications are
multimodular, and each module or ng-app root can stop at a different point in
the progression.
For example:
| Area | Appropriate architecture |
|---|
| Sign-in and account recovery | Server-rendered HTML forms and normal HTTP |
| Public content and checkout | Server pages with focused AngularTS enhancements |
| Dashboard workspace | Client router, browser-owned model, REST, cache policy, and a chosen view style |
| Reports and downloads | Server-generated files and normal browser navigation |
The login form does not need to become part of the dashboard SPA. The dashboard
can start as its own ng-app after authentication and own only the screens
whose complexity justifies it. Both modules can share the same server session
and authorization rules without sharing a frontend architecture.
This keeps complexity contained. A client-owned dashboard does not force public,
authentication, settings, or support pages to adopt its router, model cache, or
rendering strategy.
Use this order
| Start here | Escalate only when needed |
|---|
| Server-rendered page | Small AngularTS-enhanced region |
| Normal links and forms | Server-rendered router fragment |
| Server-owned model | Small browser interaction model |
| Simple HTTP pages and fragments | REST, cache, and browser security policy |
| Server HTML templates | Optional typed view when stronger checks are useful |
| One server-rendered module | One client-owned module where justified |
At every step, stop when the current approach solves the user problem. The last
row is not a target architecture. It is the option for the minority of features
that truly need the browser to own the application model.
Verify the decision
For each client-owned model, write down why a server-rendered page, server
fragment, or focused enhancement is insufficient. Test direct navigation,
JavaScript failure, authorization, validation, refresh, Back and Forward, stale
data, deployment rollback, and recovery after a failed request.
2 - Structure an application
Separate AngularTS registration, UI state, domain operations, and external resources without adding speculative layers.
Start with server rendering
Follow the server-first guideline.
Keep domain models, validation, authorization, navigation, and HTML rendering
together on the server. Render the useful document and initial state before
AngularTS starts.
Place an AngularTS root around the smallest region that needs browser behavior.
Do not create a browser copy of the domain model merely to make the page
interactive. Local selection, disclosure, pending state, and DOM behavior can
remain small browser concerns while durable state stays on the server.
Do not choose an SPA as a default project shape. If client routing is justified,
continue loading server-rendered route fragments while the server can own the
model. Move the model into the browser only when complex offline, editing,
cross-screen, or long-lived workspace behavior requires independent client
ownership.
When that move happens, the browser becomes responsible for model consistency
and view compilation. If compile-time checks between that model and its DOM are
valuable, the module can use optional typed programmatic views. HTML templates
remain supported.
The same threshold changes server communication. Server-owned pages should use
normal HTTP navigation, forms, and rendered fragments. A browser-owned model
needs REST contracts, explicit cache ownership and invalidation, and a complete
browser security policy. Introduce those systems together with frontend model
ownership, not before it.
Mix architectures by module
Do not force one rendering architecture across the product. Use separate modules
or ng-app roots for areas with different ownership needs.
A sign-in module can remain a server-rendered HTML form. After authentication, a
dashboard module can bootstrap a client router, keep a complex model in the
browser, and use REST and caching. It can independently choose HTML or typed
programmatic views. Public pages, reports, and account recovery can continue
using server rendering and normal navigation.
Keep shared concerns on the server: session identity, authorization, domain
rules, and durable state. Share frontend services only when two client-owned
modules genuinely need the same browser resource or model.
Start from ownership, not file types
A component owns DOM-facing state and its view lifetime. A model owns shared
reactive domain state. A service owns an operation or external resource. A
module records how those pieces are assembled.
Use one feature directory until a capability is independently reusable:
tasks/
task-module.ts
task-list.ts
task-repository.ts
task-list.test.ts
Avoid global controllers/, services/, and views/ directories once features
evolve independently; they scatter one change across the repository.
Register one feature module
import { angular } from '@angular-wave/angular.ts';
export class TaskRepository {
async list() {
const response = await fetch('/api/tasks');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}
export const tasks = angular
.createModule('tasks', [])
.service('taskRepository', TaskRepository);
Keep registration free of request execution and DOM access. Registration
describes recipes; runtime methods perform work.
Keep view state local
A task-list controller may own loading, error, selection, and filter text.
Move task data to a named model only when multiple component roots need reactive
access to the same records. Move loading into a repository only when the
repository intentionally deduplicates or caches requests.
Do not introduce “manager” or “facade” layers without a lifetime or policy they
own. A forwarding class adds indirection but no boundary.
Verify the structure
A feature passes the ownership test when its component can be destroyed without
leaking resources, its domain logic can be tested without a DOM, and its
repository can be replaced in a test without changing view code.
3 - Build and validate a form
Model AngularTS form submission, validation visibility, duplicate prevention, and accessible recovery as one workflow.
Separate validity from validation visibility
A field can be invalid before the user has interacted with it. Show field
feedback after blur or a failed submit, but always prevent invalid submission.
<form name="taskForm" ng-submit="submit(taskForm)" novalidate>
<label for="task-title">Task title</label>
<input
id="task-title"
name="title"
ng-model="draft.title"
required
aria-describedby="task-title-error"
/>
<p
id="task-title-error"
role="alert"
ng-show="submitted && taskForm.title.$invalid"
>
Enter a task title.
</p>
<button ng-disabled="saving">Add task</button>
</form>
The controller owns submitted, saving, and the editable draft. The
repository owns the write.
$scope.submit = async (form) => {
$scope.submitted = true;
if (form.$invalid || $scope.saving) return;
$scope.saving = true;
try {
await taskRepository.add($scope.draft);
$scope.draft = { title: '' };
$scope.submitted = false;
} finally {
$scope.saving = false;
}
};
Do not clear the draft before the server confirms success. Disable or reject
repeated submission while a non-idempotent write is pending. Return focus to the
first invalid field after a failed submit and announce server failures
separately from field validation.
Use browser constraints for semantics, AngularTS form state for view
coordination, and server validation as the authority. A disabled button does not
enforce a business rule.
4 - Load server data
Own AngularTS HTTP requests, cancellation, stale-result protection, and explicit screen states.
Represent request state explicitly
Do not use tasks.length === 0 to mean both “not loaded” and “loaded empty.”
type LoadState = 'idle' | 'loading' | 'ready' | 'error';
class TaskList {
state: LoadState = 'idle';
tasks: Task[] = [];
error = '';
}
Put transport behavior in a repository
app.factory('taskRepository', [
'$http',
($http) => ({
list: (cancelled) =>
$http
.get('/api/tasks', { timeout: cancelled })
.then((response) => response.data),
}),
]);
The timeout promise should resolve when the component is destroyed or a newer
query supersedes the request. Cancellation saves work; a request sequence or
identity check still protects against transports that complete while
cancellation is racing.
Set loading before starting. On success, replace the collection property so
typed readers observe the commit. In finally, clear loading only if the
completing request is still current. Render loading, stale data, empty success,
recoverable error, and permission failure deliberately.
Use the $http guide for interceptors, defaults,
caching, and response contracts.
5 - Add routing
Register AngularTS route trees, choose resolve boundaries, and make navigation cleanup and failure behavior explicit.
Do not default to client-side routing
Use server routes and ordinary links first. They provide addressability,
history, streaming HTML, metadata, authorization boundaries, and resilient
navigation without a client router.
Introduce the AngularTS router when multiple screens are intentionally owned by
one bootstrapped application and preserving client state or resources between
those screens materially improves the product. A client router is not required
for AngularTS components embedded in server-rendered pages.
Keep route snippets server-rendered for as long as possible. Let the router own
the transition, URL state, and component lifetime while the server still renders
the route HTML from its authoritative model. Do not move the model and rendering
into the browser merely because navigation no longer reloads the document.
If a route eventually needs a complex browser-owned model, move compilation with
that model. A typed programmatic view is available when the screen benefits from
compile-time checks between the model and DOM; it is not required by the router.
That transition also makes REST services, frontend cache consistency, and
browser-facing security policy primary concerns. A router using server-rendered
fragments can continue using simple HTTP; client navigation alone does not
justify building a separate API and cache layer.
Scope the router to the module that needs it. A dashboard can be a full SPA
under its own ng-app while sign-in, recovery, public pages, and downloads
remain server-rendered routes. Adding a dashboard router does not make the
entire application a client-owned router tree.
Register components as screens
The router is part of the built-in runtime; do not add a speculative ng.router
module dependency.
const app = angular.createModule('taskBoard', []);
const routes = [
{
name: 'tasks',
url: '/tasks',
component: 'taskList',
},
{
name: 'tasks.detail',
url: '/{taskId:int}',
component: 'taskDetail',
},
] as const satisfies readonly ng.RouterModuleDeclaration[];
app.router(routes);
<nav aria-label="Tasks">
<a ng-state="'tasks'">All tasks</a>
</nav>
<main ng-view></main>
Use stable state names as application identifiers. URLs may evolve; state names
are referenced by links, transitions, tests, and diagnostics.
Resolve identity or authorization data that must exist before the screen can be
valid. Load optional panels and large collections inside the component so the
route can render a shell. Parent states own shared layout and resolves; children
should not refetch parent-owned data.
Test direct URL entry, back/forward, invalid parameters, same-state parameter
changes, rejected resolves, and leaving during pending work. Continue with the
routing overview.
6 - Handle errors
Classify AngularTS domain, transport, transition, and programming failures and give each one the correct owner.
Classify before rendering
- Validation and domain rejection are expected outcomes near the action.
- HTTP timeout, abort, offline, and non-success status belong to the request
owner.
- Route resolve failure belongs to transition policy.
- Unexpected exceptions belong to
$exceptionHandler and diagnostics.
Do not convert every rejection to [], null, or false. That destroys
information and commonly renders an empty success state after a real failure.
Wrap low-level errors with the operation name while retaining the original
cause. The UI should receive a stable category and recovery action, not a stack
trace or server payload. Logs may include release, route, operation, and
correlation identifiers but must exclude credentials and private data.
A repository may retry a safe idempotent read. A component should own the retry
button. Global interceptors should refresh credentials or normalize protocol
behavior, not retry every request indiscriminately.
Unexpected exceptions should reach the
$exceptionHandler reference. Test that
expected failures remain recoverable and unexpected exceptions are reported.
7 - Test application behavior
Allocate AngularTS tests across plain logic, typed contracts, browser integration, lifecycle, and failure boundaries.
Use the narrowest layer that can fail
Test transformations and domain transitions as plain TypeScript. Compile typed
component views to catch invalid model properties, event types, and DOM property
names. Use a browser for compilation, directives, focus, routing, and reactive
rendering.
test('task form prevents duplicate submission', async ({ page }) => {
await page.goto('/tasks');
await page.getByLabel('Task title').fill('Write tests');
await page.getByRole('button', { name: 'Add task' }).dblclick();
await expect(page.getByRole('listitem')).toHaveCount(1);
});
Select by role, label, visible text, or stable application identifiers. Scope
IDs, generated comments, internal CSS classes, and scheduler timing are
implementation details.
For each asynchronous screen, cover loading, stale data, empty success,
recoverable failure, retry, cancellation on navigation, and late completion.
Control server responses rather than adding sleeps. For resource owners, enter
and leave repeatedly and assert listener and connection counts return to
baseline.
Run critical browser contracts against the production bundle.
8 - Troubleshoot an application
Locate AngularTS bootstrap, injection, compilation, reactive, routing, and request failures by testing boundaries in order.
Find the first failed boundary
- Confirm the JavaScript module loaded.
- Confirm registration completed before automatic or manual bootstrap.
- Confirm
ng-app names the created module. - Read the first AngularTS error, not the cascade.
- Confirm injected token order matches function parameters.
- Confirm the owning scope/controller contains the value read by the view.
- Inspect transport status before changing rendering code.
Raw interpolation means bootstrap or compilation did not complete. “Module not
available” usually means boot order or spelling. “Unknown provider” means the
token is absent from the loaded module graph.
For typed views, verify changing positions receive a function reader rather than
a snapshot. Call keyed item readers inside nested bindings. Replace collection
properties in controller methods. ESM applications import tag factories such as
div and button directly from @angular-wave/angular.ts; no-build UMD
applications use angular.tags. Controller typing is supplied to
component<T>(); use DOM property names such as htmlFor.
Remove sibling features while preserving the failing owner, data, and timing. Do
not hide races with timeouts. Use the error catalog
after identifying the failed boundary.
9 - Production and deployment
Produce, verify, deploy, observe, and roll back an AngularTS artifact without relying on development behavior.
Produce one immutable artifact
Install from the lockfile, typecheck source and tests, validate generated
bindings, run browser tests, and build once. Promote the same artifact through
environments. Rebuilding after approval changes the release.
Fingerprint immutable assets. Revalidate HTML and route manifests so they
reference current fingerprints. Version service-worker caches and remove
obsolete entries only after the new worker can serve a complete application.
Verify production behavior
Smoke-test generated output with development fallbacks disabled. Cover direct
route entry, refresh on nested URLs, CSP, API credentials, loading and failure
states, offline behavior, and service-worker upgrade from the previous release.
Design rollback before deployment
Retain the previous artifact. Determine whether storage migrations, server
contracts, or service-worker caches make rollback incompatible. Prefer
additive data changes.
Attach a release identifier to errors and operations. After deployment, verify
asset integrity, critical routes, request latency, error rate, and one synthetic
workflow. Stop promotion or roll back from explicit thresholds.
Follow the upgrade procedure for framework
changes.
10 - Browser support
Define an AngularTS browser contract from required Web APIs, compiled output, fallbacks, and production tests.
List browser families and versions, mobile requirements, assistive-technology
coverage, and optional capabilities. Framework support does not automatically
provide WebTransport, service workers, storage, workers, or WebAssembly
everywhere.
Isolate capability selection:
function createRealtime() {
if ('WebTransport' in window) return createWebTransport();
if ('WebSocket' in window) return createWebSocket();
return createPolling();
}
Detect the API used, not a browser brand. Keep fallback selection in one service
so components consume domain events rather than transport differences.
Test production output: cold load, direct route entry, back/forward, storage
denial, offline transition, reduced motion, keyboard navigation, and
service-worker upgrade. Disable capabilities to test fallbacks even when CI
supports them.
When no fallback exists, render an actionable unsupported state before
constructing the service. A runtime exception is not a support policy.
11 - Accessibility
Preserve semantics, focus, validation, announcements, and motion preferences across AngularTS structural and route updates.
ng-if removes content and destroys focused descendants. Before closing a
dialog, menu, or editor, choose the element that regains focus. ng-show
retains the subtree; verify hidden descendants cannot remain in keyboard order.
Use native buttons, links, labels, headings, and lists before ARIA. Attach
AngularTS events to those controls instead of rebuilding their keyboard behavior
on generic elements.
Associate errors with controls using aria-describedby. Show invalid state
after interaction or submission, retain the invalid value, focus the first
invalid control after failed submission, and provide a summary for long forms.
Server failures are separate from field constraints.
Use a polite live region for completion or background updates and an alert for
failures requiring immediate attention. Do not place an entire keyed list in a
live region.
Test direct route entry, route transitions, keyed reordering, loading
replacement, reduced motion, zoom, keyboard-only operation, and a screen-reader
workflow. Automated rules cannot determine whether focus movement makes sense.
12 - Internationalization
Design AngularTS message ownership, locale formatting, asynchronous catalogs, and bidirectional layouts.
Use stable message identifiers and named placeholders. Do not concatenate
translated fragments or use English text as an identifier. Choose plural forms
from locale rules rather than count === 1.
AngularTS formatting filters delegate to Intl. Pass the application locale and
explicit currency or time-zone policy; browser defaults may differ from user
settings.
<span>{{ total | currency:user.locale }}</span>
<time>{{ dueAt | date:user.locale }}</time>
A locale service should deduplicate catalog requests, cache by locale and
catalog version, and expose loading and fallback state. Do not render raw
message identifiers while loading.
Set document or feature lang and dir. Prefer logical CSS properties. Test
long labels, narrow screens, mixed-direction names, non-Latin input, plural edge
cases, currency minor units, and dates around daylight-saving transitions.
Locale switching should invalidate message and formatter readers without
rebuilding unrelated domain state.
13 - Application security
Apply AngularTS trust boundaries to templates, typed properties, URLs, requests, storage, workers, and service workers.
External sources include URL parameters, forms, storage, server responses,
realtime messages, worker messages, and cross-window events. Sensitive sinks
include HTML, executable URLs, navigation, credentials, logs, and persisted
state.
Prefer interpolation, text children, and typed DOM properties. Use
ng-bind-html only for markup that passed application sanitization. A trust
override must be local and reviewable; never trust a value merely because it
came from your server.
Hidden routes, disabled buttons, and client guards improve UX but do not
authorize operations. The server must validate identity, permission, input
shape, size, and state transition.
Configure credentials and XSRF behavior for known origins. Reject unexpected
redirect and content types at the repository boundary. Store the minimum browser
data, validate versioned records on read, and never persist bearer credentials
for convenience.
Deploy HTTPS and a specific Content Security Policy. Review worker and
service-worker scope, allowed connection origins, update behavior, and rollback.
Security failures should remain visible rather than trigger broad policy
relaxation.
14 - Observability
Correlate AngularTS navigation, requests, workflows, rendering outcomes, and unexpected failures without recording sensitive state.
Assign a stable operation name and correlation ID at the user-action or route
boundary. Record start, terminal outcome, duration, release, and route. Do not
log every reactive read or scope write; volume hides causality and changes
timing.
performance.mark(`task-save:${id}:start`);
try {
await repository.save(task);
record({ operation: 'task.save', outcome: 'success', id });
} catch (error) {
record({ operation: 'task.save', outcome: classify(error), id });
throw error;
}
Expected domain and transport failures should produce classified outcomes.
Unexpected exceptions should reach $exceptionHandler with their cause. Redact
credentials, request bodies, user-entered text, and model snapshots.
Carry one correlation ID through route transition, request, retry, and workflow
command. Record cancellation separately from failure. For realtime connections,
record state transitions and reconnect exhaustion, not every message.
After deployment, verify release tagging, source-map policy, critical routes,
error rate, request latency, and one synthetic path. Every alert needs an owner
and actionable threshold.