Modèle Album
This is the core data model used throughout the Angular course projects
TypeScript Definition
export type Album = {
id: number;
name: string;
artist: string;
description: string;
price: number;
tags: string[];
highlighted?: boolean;
}
Properties
| Property | Type | Required | Description |
|---|---|---|---|
id | nombre | ✅ Yes | Unique identifier for the album |
name | string | ✅ Yes | Album name (e.g., "Hot Rats", "Dark Side of the Moon") |
artist | string | ✅ Yes | Artist or band name (e.g., "Frank Zappa", "Pink Floyd") |
description | string | ✅ Yes | Album description or review |
price | nombre | ✅ Yes | Album price (e.g., 29.99, 19.99) |
tags | string[] | ✅ Yes | Genre tags (e.g., ["Rock", "Progressive"]) |
highlighted | booléen | ❌ Optional | Featured/highlighted status for homepage |
Exemple Data
Exemple 1: Classic Rock Album
const album1: Album = {
id: 1,
name: "Dark Side of the Moon",
artist: "Pink Floyd",
description: "Progressive rock masterpiece exploring themes of conflict, greed, time, and mental illness",
price: 29.99,
tags: ["Rock", "Progressive", "Classic"],
highlighted: true
};
Exemple 2: Jazz Album
const album2: Album = {
id: 2,
name: "Hot Rats",
artist: "Frank Zappa",
description: "Instrumental jazz fusion album featuring extended improvisations",
price: 24.99,
tags: ["Jazz", "Fusion", "Experimental"]
};
Exemple 3: Budget Album
const album3: Album = {
id: 3,
name: "Abbey Road",
artist: "The Beatles",
description: "The Beatles' eleventh studio album and their final recorded work",
price: 19.99,
tags: ["Rock", "Pop", "Classic"],
highlighted: false
};
Utilisation in Components
Signal-based Input (Angular 17+)
import { Component, input } from '@angular/core';
import { Album } from './models/album.model';
@Component({
selector: 'app-album-card',
standalone: true,
template: `
<div class="card">
<h2>{{ album().name }}</h2>
<p>by {{ album().artist }}</p>
<p class="price">{{ album().price | currency }}</p>
<div class="tags">
@for (tag of album().tags; track tag) {
<span class="tag">{{ tag }}</span>
}
</div>
</div>
`
})
export class AlbumCardComponent {
album = input.required<Album>();
}
Old @Input() Decorator (Angular v16)
import { Component, Input } from '@angular/core';
import { Album } from './models/album.model';
@Component({
selector: 'app-album-card',
template: `
<div class="card">
<h2>{{ album.name }}</h2>
<p>by {{ album.artist }}</p>
<p class="price">{{ album.price | currency }}</p>
<div class="tags">
<span *ngFor="let tag of album.tags" class="tag">
{{ tag }}
</span>
</div>
</div>
`
})
export class AlbumCardComponent {
@Input() album!: Album;
}
Utilisation in Services
HttpClient GET
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Album } from '../models/album.model';
@Injectable({ providedIn: 'root' })
export class AlbumService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:3000/albums';
getAlbums(): Observable<Album[]> {
return this.http.get<Album[]>(this.apiUrl);
}
getAlbum(id: number): Observable<Album> {
return this.http.get<Album>(`${this.apiUrl}/${id}`);
}
createAlbum(album: Omit<Album, 'id'>): Observable<Album> {
return this.http.post<Album>(this.apiUrl, album);
}
updateAlbum(album: Album): Observable<Album> {
return this.http.put<Album>(`${this.apiUrl}/${album.id}`, album);
}
deleteAlbum(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Utilisation in Formulaires
Reactive Formulaires (v16)
import { FormBuilder, Validators } from '@angular/forms';
import { Album } from '../models/album.model';
export class AlbumFormComponent {
private fb = inject(FormBuilder);
albumForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
artist: ['', [Validators.required, Validators.minLength(2)]],
description: ['', Validators.required],
price: [0, [Validators.required, Validators.min(0)]],
tags: this.fb.array([]),
highlighted: [false]
});
saveAlbum() {
if (this.albumForm.valid) {
const album: Omit<Album, 'id'> = {
name: this.albumForm.value.name!,
artist: this.albumForm.value.artist!,
description: this.albumForm.value.description!,
price: this.albumForm.value.price!,
tags: this.albumForm.value.tags!,
highlighted: this.albumForm.value.highlighted
};
this.albumService.createAlbum(album).subscribe(
created => console.log('Album created:', created)
);
}
}
}
Validation Rules
Required Fields
- ✅
name- Cannot be empty, min 2 characters - ✅
artist- Cannot be empty, min 2 characters - ✅
description- Cannot be empty - ✅
price- Must be ≥ 0 - ✅
tags- Array can be empty but must exist
Business Rules (from exercises)
-
Price must end with 9 (personnalisé validaaur)
function priceEndingWith9(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value?.toString();
if (value && value[value.length - 1] !== '9') {
return { priceEndingWith9: { value: control.value } };
}
return null;
};
} -
Album must be unique (async validaaur)
function albumExistsValidator(albumService: AlbumService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const formGroup = control as FormGroup;
const name = formGroup.get('name')?.value;
const artist = formGroup.get('artist')?.value;
if (!name || !artist) return of(null);
return albumService.isAlbumExist(artist, name).pipe(
map(exists => exists ? { albumExists: true } : null)
);
};
}
Signal-based Gestion d'État
With Signals (v21)
import { signal, computed } from '@angular/core';
import { Album } from '../models/album.model';
export class AlbumListComponent {
// Signal state
albums = signal<Album[]>([]);
// Computed values
highlightedAlbums = computed(() =>
this.albums().filter(a => a.highlighted)
);
totalAlbums = computed(() => this.albums().length);
averagePrice = computed(() => {
const albums = this.albums();
if (albums.length === 0) return 0;
return albums.reduce((sum, a) => sum + a.price, 0) / albums.length;
});
// Mutations
addAlbum(album: Album) {
this.albums.update(current => [...current, album]);
}
removeAlbum(id: number) {
this.albums.update(current => current.filter(a => a.id !== id));
}
}
JSON Server Mock Data
Create db.json for development:
{
"albums": [
{
"id": 1,
"name": "Dark Side of the Moon",
"artist": "Pink Floyd",
"description": "Progressive rock masterpiece",
"price": 29.99,
"tags": ["Rock", "Progressive"],
"highlighted": true
},
{
"id": 2,
"name": "Hot Rats",
"artist": "Frank Zappa",
"description": "Instrumental jazz fusion",
"price": 24.99,
"tags": ["Jazz", "Fusion"]
},
{
"id": 3,
"name": "Abbey Road",
"artist": "The Beatles",
"description": "The Beatles' final recorded work",
"price": 19.99,
"tags": ["Rock", "Pop"],
"highlighted": false
}
]
}
Run with:
npx json-server --watch db.json --port 3000
Common Patterns
Filter by Tag
filterByTag(tag: string): Album[] {
return this.albums().filter(album =>
album.tags.includes(tag)
);
}
Sort by Price
sortedByPrice = computed(() =>
[...this.albums()].sort((a, b) => a.price - b.price)
);
Search by Name or Artist
search(query: string): Album[] {
const lower = query.toLowerCase();
return this.albums().filter(album =>
album.name.toLowerCase().includes(lower) ||
album.artist.toLowerCase().includes(lower)
);
}
Related Documentation
- Components with Album Data
- Forms with Album Model
- Signal Forms
- HTTP Service
- Testing Albums
- Signal Store
Project Files
Find this model in student projects:
- Path:
src/app/model/album.model.ts - Utiliserd in: All 19 step projects