Creating an Angular project
Angular is a complete framework, maintained by Google and written in TypeScript: router, forms, HTTP client, testing and command-line tooling come built in. You start with the Angular CLI:
npm install -g @angular/cli # Node.js 22.22, 24.15 or 26 (LTS recommended)
ng new boutique # questions: stylesheet, server-side rendering, AI tools…
cd boutique
ng serve # http://localhost:4200, automatic reload
ng generate component liste-taches # shortcut: ng g c liste-taches
The project created today uses Angular 22.2 with the TypeScript version it supports (6.0); it works without zone.js (change detection driven by signals and events) and its unit tests (ng test) run with Vitest. Files no longer carry a suffix: the root component is the App class in src/app/app.ts.
The entry point src/main.ts starts the application with bootstrapApplication(App, appConfig); app.config.ts declares the global providers (router, HTTP client).
Historically, components were grouped in NgModules. Recent versions make components standalone by default: each component declares what it uses in imports. You will still come across NgModules in existing projects; the principle of components remains the same.
Anatomy of a component
import { Component, signal, computed } from '@angular/core';
interface Tache { id: number; libelle: string; faite: boolean }
@Component({
selector: 'app-liste-taches',
template: `
<h2>{{ titre }}</h2>
<ul>
@for (tache of taches(); track tache.id) {
<li [class.faite]="tache.faite">
<input type="checkbox" [checked]="tache.faite" (change)="basculer(tache.id)" />
{{ tache.libelle }}
</li>
} @empty {
<li>No tasks.</li>
}
</ul>
@if (restantes() > 0) {
<p>{{ restantes() }} task(s) remaining</p>
} @else {
<p>All done.</p>
}
`,
styles: `.faite { text-decoration: line-through; }`,
})
export class ListeTaches {
titre = 'Mes tâches';
taches = signal<Tache[]>([
{ id: 1, libelle: 'Acheter du pain', faite: false },
{ id: 2, libelle: 'Réviser Angular', faite: true },
]);
restantes = computed(() => this.taches().filter((t) => !t.faite).length);
basculer(id: number) {
this.taches.update((liste) =>
liste.map((t) => (t.id === id ? { ...t, faite: !t.faite } : t)));
}
}
The selector app-liste-taches lets you use the component in another template: <app-liste-taches />. The template can also be placed in a separate file (templateUrl), which is what the CLI does.
Template bindings
| Syntax | Direction | Example |
|---|---|---|
{{ expr }} |
interpolation (text) | {{ produit.nom }} |
[prop]="expr" |
property, from the component to the DOM | [disabled]="panierVide()" |
(evt)="instr" |
event, from the DOM to the component | (click)="ajouter()" |
[(prop)]="val" |
two-way binding | [(ngModel)]="recherche" (with FormsModule) |
Useful variants: [class.active]="estActif", [style.width.px]="largeur", [attr.aria-label]="libelle".
Conditions and lists
The control flow blocks @if, @else, @for, @empty and @switch are the modern syntax (the older *ngIf and *ngFor directives remain common in existing code). @for requires a track expression: Angular uses it to know which DOM element matches which piece of data when the list changes. Use a stable identifier (track tache.id).
Signals
A signal is a reactive value: you read it by calling it (taches()), and modify it with set(newValue) or update(fn). Angular knows which templates read which signals and updates only what depends on them. computed() derives a value from other signals, recomputed only when they change. As with any state, replace arrays and objects with new values rather than modifying them in place.
Communicating: input() and output()
@Component({
selector: 'app-carte-produit',
template: `
<h3>{{ nom() }}</h3>
<button (click)="ajouter.emit(id())">Add</button>
`,
})
export class CarteProduit {
id = input.required<number>();
nom = input.required<string>();
ajouter = output<number>();
}
The parent writes: <app-carte-produit [id]="p.id" [nom]="p.nom" (ajouter)="onAjouter($event)" />. The parent component must import CarteProduit in its imports array. The @Input() and @Output() decorators play the same role in older code.
Pipes
A pipe transforms a value for display: {{ prix | currency:'EUR' }}, {{ date | date:'dd/MM/yyyy' }}. A custom pipe is a class with a transform method; it is a pure function, ideal for a test.
Takeaways
Class + decorator + template; [ ] towards the DOM, ( ) from the DOM; @for with track; signals read by calling them; input()/output() to communicate.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Summary of a task list
Logic of the component’s
resumecomputed. Declare theTacheinterface (idnumber,libellestring,faiteboolean) and writeresumeTaches(taches: readonly Tache[]): Resumethat returns{ total, faites, pourcentage }, the percentage of completed tasks being rounded to the nearest integer (0 for an empty list).// TODO : interface Tache et type Resume function resumeTaches(taches) { // TODO }tronquer pipe (executed logic)
Write in JavaScript the
transformmethod of atronquerpipe:tronquer(texte, longueur = 20, suffixe = "…").nullorundefined→ empty string;- if the text (leading and trailing whitespace removed) fits within
longueur, it is returned as is (without the leading and trailing whitespace); - otherwise, keep the first
longueurcharacters, remove trailing whitespace, then add the suffix.
function tronquer(texte, longueur = 20, suffixe = "…") { // TODO }First Angular component
Create a project with
ng newthen aListeTachescomponent (ng g c liste-taches) displayed in the root component.- State: a signal initialized with these tasks:
id,libelle,faite 1,Acheter du pain,false 2,Réviser Angular,true 3,Appeler le garage,false- Display them with
@for (…; track tache.id)and a checkbox bound with[checked]and(change). - Add a
resumecomputed that uses yourresumeTachesfunction, displayed as “1 / 3 tâches faites (33 %)”. - Add a text field and an “Ajouter” button that adds a task with
update(new identifier = largest identifier + 1). The button is disabled ([disabled]) when the field is empty.
- The tasks are in a signal and read with taches() in the template
- @for uses track tache.id
- The summary is a computed, recalculated automatically
- Adding replaces the array with update instead of using push
Reading a signal
In the component,
total = signal(0). How do you display its value in the template?- {{ total }}
- {{ total() }}
- {{ total.value }}
- {{ $total }}
End-of-lesson quiz
What does
[disabled]="panierVide()"do?- It listens to the disabled event
- It binds the element's disabled property to the value of the expression
- It creates a disabled variable
Why does @for require a track expression?
- To sort the list
- To associate each DOM element with a piece of data and reuse it correctly when the list changes
- To count the elements
How does a child component notify its parent of an action?
- By modifying its input
- By emitting an event with an output()
- By directly calling a method of the parent
The rest of the course
- Services and Dependency Injection18 min
- RxJS, Observables and the HTTP Client20 min
- Reactive Forms and Validation19 min
- Routing, Lazy Loading and Guards18 min