Tutorial
Build and ship one complete AngularTS application from an empty directory.
This tutorial assumes basic HTML and JavaScript but no AngularTS knowledge. You
will build a browser-owned task board with reactive state, form validation,
routing, server data, tests, and a production checklist.
This application deliberately uses AngularTS’s optional typed programmatic
views because its model and rendering live in TypeScript. If your server renders
the HTML, follow the first application guide instead and keep the HTML you already
have.
What you will build
The finished application can list tasks, add a task, open a task route, load
initial data, and expose loading and error states.
Follow the tutorial
- Create the application
- Add the task board’s typed view
- Add reactive state
- Build the form
- Add routing and server data
- Test the behavior
- Prepare production
- Run the complete application
Complete the steps in order. Every page states the expected result before moving
on.
1 - Create the application
Install AngularTS, define the application module, and bootstrap the page.
Before you start
Use a current Node.js release and an application bundler that supports
TypeScript and browser ESM.
Install
npm install @angular-wave/angular.ts
Create the application entrypoint:
import { angular } from '@angular-wave/angular.ts';
export const app = angular.createModule('taskBoard', []);
angular.bootstrap(document, ['taskBoard']);
Create the host page:
<!doctype html>
<html lang="en">
<body>
<task-board></task-board>
<script type="module" src="/src/app.ts"></script>
</body>
</html>
Checkpoint
Start the development server. The page should load without a bootstrap or
missing-module error. The custom element is empty until the next step.
Next step
Add the optional typed component view used by this tutorial.
2 - Add a typed view
Register a component whose model and DOM bindings are checked by TypeScript.
A typed view constructs real DOM and keeps controller reads reactive. Its main
advantage over string templates is type safety between the component model and
its bindings.
import { h1, main, p } from '@angular-wave/angular.ts';
class TaskBoard {
title = 'Task board';
tasks = [{ id: 1, title: 'Read the guide', done: false }];
}
app.component<TaskBoard>('taskBoard', {
controller: TaskBoard,
view: ({ controller }) =>
main(
h1(() => controller.title),
p(() => `${controller.tasks.length} task(s)`),
),
});
The view is registered on the normal application module. It does not need a
separate mount operation. Functions used as children or property values are
reactive readers; plain values render once.
Checkpoint
The page should display “Task board” and one task count.
Next step
Render and update the collection.
3 - Add reactive state
Render a keyed collection and update it without replacing unrelated DOM.
Add a method to the controller:
class TaskBoard {
nextId = 2;
tasks = [{ id: 1, title: 'Read the guide', done: false }];
add(title: string) {
this.tasks = [...this.tasks, { id: this.nextId++, title, done: false }];
}
}
Render the collection with stable keys:
import { each, li, ul } from '@angular-wave/angular.ts';
view: ({ controller }) =>
ul(
each(
() => controller.tasks,
(task) => task.id,
(task) => li(() => task().title),
),
),
The item renderer receives a reader. Call task() inside reactive bindings so a
same-key item replacement updates its row.
Checkpoint
Calling controller.add("Ship it") appends one row while retaining the existing
row.
Next step
Build the form.
4 - Build the form
Bind typed DOM events, validation state, and a reactive disabled property.
Add draft and a submit method:
draft = "";
submit() {
const title = this.draft.trim();
if (!title) return;
this.add(title);
this.draft = "";
}
Add the form before the list:
import { button, form, input, label } from '@angular-wave/angular.ts';
form(
{
onsubmit: (event) => {
event.preventDefault();
controller.submit();
},
},
label({ htmlFor: 'new-task' }, 'Task'),
input({
id: 'new-task',
value: () => controller.draft,
oninput: (event) => {
controller.draft = (event.currentTarget as HTMLInputElement).value;
},
}),
button({ disabled: () => !controller.draft.trim() }, 'Add'),
);
The callback expresses reactive intent directly. No separate computed wrapper is
required.
Checkpoint
The button is disabled for blank input. Submitting valid text adds a row and
clears the input.
Next step
Add routing and initial server data.
5 - Add routing and server data
Give the task board a URL and expose explicit loading and error states.
Register the component as a route:
app.router({
name: 'tasks',
url: '/tasks',
component: 'taskBoard',
});
Place the route outlet in the host page:
Load initial data from a controller method:
loading = true;
error = "";
tasks: Task[] = [];
async onInit() {
try {
const response = await fetch("/api/tasks");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
this.tasks = await response.json();
} catch (error) {
this.error = error instanceof Error ? error.message : "Load failed";
} finally {
this.loading = false;
}
}
Render loading, error, empty, and populated states deliberately. An empty array
does not prove loading succeeded.
Checkpoint
Opening /tasks renders the board. A failed request produces a readable error
state.
Next step
Test the application behavior.
6 - Test the application
Separate model tests from browser-level rendering checks.
Start with a fast controller test:
it('adds a trimmed task', () => {
const board = new TaskBoard();
board.draft = ' Write tests ';
board.submit();
expect(board.tasks.at(-1)?.title).toBe('Write tests');
expect(board.draft).toBe('');
});
Then verify the user-visible contract in a browser:
test('adds a task', async ({ page }) => {
await page.goto('/tasks');
await page.getByLabel('Task').fill('Write tests');
await page.getByRole('button', { name: 'Add' }).click();
await expect(page.getByText('Write tests')).toBeVisible();
});
Test behavior, not internal scope fields or generated DOM comments. Keep network
outcomes deterministic.
Checkpoint
Both the model test and browser test pass, including the loading failure case.
Next step
Prepare the application for production.
7 - Prepare production
Apply the minimum production, security, and operability checks to the task board.
Before deployment:
- Build with production optimization and an intentional source-map policy.
- Serve hashed assets with long-lived caching and HTML with revalidation.
- Keep API URLs and secrets out of the browser bundle.
- Enforce HTTPS and an explicit Content Security Policy.
- Exercise loading, empty, error, offline, and slow-network states.
- Test keyboard navigation and visible focus.
- Record the AngularTS version in the lockfile.
- Run type checking, unit tests, browser tests, and integration checks in CI.
Expected result
The application can be deployed repeatedly, reports failures without exposing
secrets, and remains usable when requests fail.
Continue with Production and deployment and
Best practices.
8 - Run the complete application
Open the maintained task-board example and compare it with your implementation.
The repository contains a browser-runnable version of the task board:
docs/examples/task-board/task-board.ts is compiled as a package consumer.docs/static/examples/task-board/task-board-demo.html provides the host page.docs/static/examples/task-board/task-board.js registers the component view.
Open the task-board example.
The example keeps data local so it runs without a backend. The routing and
server-data step shows where application-owned transport behavior belongs.
Compare behavior
Verify that both versions:
- Render the initial keyed collection.
- Disable submission for blank text.
- Trim accepted task titles.
- Clear the input after submission.
- Preserve existing rows when a task is appended.
CI type-checks the TypeScript application and runs the browser interaction
contract. Documentation checks also validate its AngularTS API usage.