Skip to main content

Album Model

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

PropertyTypeRequiredDescription
idnumber✅ YesUnique identifier for the album
namestring✅ YesAlbum name (e.g., "Hot Rats", "Dark Side of the Moon")
artiststring✅ YesArtist or band name (e.g., "Frank Zappa", "Pink Floyd")
descriptionstring✅ YesAlbum description or review
pricenumber✅ YesAlbum price (e.g., 29.99, 19.99)
tagsstring[]✅ YesGenre tags (e.g., ["Rock", "Progressive"])
highlightedboolean❌ OptionalFeatured/highlighted status for homepage

Example Data

Example 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
};

Example 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"]
};

Example 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
};

Usage 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;
}

Usage 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}`);
}
}

Usage in Forms

Reactive Forms (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)

  1. Price must end with 9 (custom validator)

    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;
    };
    }
  2. Album must be unique (async validator)

    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 State Management

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)
);
}


Project Files

Find this model in student projects:

  • Path: src/app/model/album.model.ts
  • Used in: All 19 step projects