Pràctica 2: Orientació a Objectes i Accés a API REST amb Angular¶
Objectius de la pràctica¶
En aquesta pràctica aprendràs a: - Aplicar conceptes d'orientació a objectes amb TypeScript - Crear models i interfícies per estructurar les dades - Crear serveis per encapsular la lògica de negoci - Fer peticions HTTP a una API REST - Mostrar dades dinàmiques obtingudes d'una API - Gestionar errors i estats de càrrega
Prerequisits¶
- Haver completat la Pràctica 1
- Tenir instal·lat Node.js, npm i Angular CLI
- Tenir IntelliJ IDEA configurat per Angular
Part 1: Conceptes d'Orientació a Objectes en TypeScript¶
1.1 Introducció a TypeScript i POO¶
TypeScript és el llenguatge que utilitza Angular. És un superconjunt de JavaScript que afegeix: - Tipatge estàtic: Definició de tipus per variables i funcions - Classes i interfícies: Programació orientada a objectes - Decoradors: Metadades per classes i mètodes
1.2 Classes en TypeScript¶
Una classe és un motlle per crear objectes amb propietats i mètodes:
class Persona {
// Propietats
nom: string;
edat: number;
// Constructor
constructor(nom: string, edat: number) {
this.nom = nom;
this.edat = edat;
}
// Mètode
saludar(): string {
return `Hola, em dic ${this.nom} i tinc ${this.edat} anys.`;
}
}
// Crear una instància
const persona = new Persona('Anna', 25);
console.log(persona.saludar());
1.3 Interfícies en TypeScript¶
Les interfícies defineixen l'estructura que ha de tenir un objecte:
interface Usuari {
id: number;
nom: string;
email: string;
actiu?: boolean; // Propietat opcional
}
// Objecte que implementa la interfície
const usuari: Usuari = {
id: 1,
nom: 'Joan',
email: 'joan@exemple.com'
};
1.4 Encapsulament¶
TypeScript suporta modificadors d'accés:
class CompteBancari {
private saldo: number;
public titular: string;
constructor(titular: string, saldoInicial: number) {
this.titular = titular;
this.saldo = saldoInicial;
}
// Getter
getSaldo(): number {
return this.saldo;
}
// Mètodes públics
ingressar(quantitat: number): void {
if (quantitat > 0) {
this.saldo += quantitat;
}
}
retirar(quantitat: number): boolean {
if (quantitat > 0 && quantitat <= this.saldo) {
this.saldo -= quantitat;
return true;
}
return false;
}
}
Part 2: Crear el projecte¶
2.1 Crear un nou projecte Angular¶
Obre la terminal i executa:
ng new practica-api-rest
Nota Angular 20: El projecte es crea automàticament amb routing i CSS per defecte. Si vols altres opcions:
# Exemple amb SCSS
ng new practica-api-rest --style=scss
2.2 Navegar al projecte i obrir-lo¶
cd practica-api-rest
Obre el projecte a IntelliJ IDEA.
Part 3: Crear el model de dades¶
Utilitzarem l'API pública JSONPlaceholder que proporciona dades fictícies per a proves.
3.1 Crear la interfície per a usuaris¶
- Crea una carpeta
modelsdins desrc/app:
mkdir src/app/models
- Crea el fitxer
src/app/models/usuari.model.ts:
export interface Usuari {
id: number;
name: string;
username: string;
email: string;
phone?: string;
website?: string;
address?: {
street: string;
suite: string;
city: string;
zipcode: string;
};
company?: {
name: string;
catchPhrase: string;
};
}
3.2 Crear la interfície per a publicacions¶
Crea el fitxer src/app/models/publicacio.model.ts:
export interface Publicacio {
id: number;
userId: number;
title: string;
body: string;
}
3.3 Crear un fitxer d'índex per exportar els models¶
Crea el fitxer src/app/models/index.ts:
export * from './usuari.model';
export * from './publicacio.model';
Part 4: Crear el servei per accedir a l'API¶
4.1 Configurar HttpClient¶
Angular utilitza HttpClient per fer peticions HTTP.
- Obre el fitxer
src/app/app.config.tsi afegeix la configuració del HttpClient:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient()
]
};
4.2 Crear el servei d'usuaris¶
- Crea una carpeta
servicesdins desrc/app:
mkdir src/app/services
- Genera el servei amb Angular CLI:
ng generate service services/usuaris
- Edita el fitxer
src/app/services/usuaris.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Usuari } from '../models';
@Injectable({
providedIn: 'root'
})
export class UsuarisService {
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
constructor(private http: HttpClient) { }
// Obtenir tots els usuaris
getUsuaris(): Observable<Usuari[]> {
return this.http.get<Usuari[]>(this.apiUrl)
.pipe(
catchError(this.handleError)
);
}
// Obtenir un usuari per ID
getUsuari(id: number): Observable<Usuari> {
return this.http.get<Usuari>(`${this.apiUrl}/${id}`)
.pipe(
catchError(this.handleError)
);
}
// Gestió d'errors
private handleError(error: HttpErrorResponse) {
let errorMessage = 'Error desconegut';
if (error.error instanceof ErrorEvent) {
// Error del client
errorMessage = `Error: ${error.error.message}`;
} else {
// Error del servidor
errorMessage = `Codi d'error: ${error.status}, Missatge: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
4.3 Crear el servei de publicacions¶
- Genera el servei:
ng generate service services/publicacions
- Edita el fitxer
src/app/services/publicacions.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Publicacio } from '../models';
@Injectable({
providedIn: 'root'
})
export class PublicacionsService {
private apiUrl = 'https://jsonplaceholder.typicode.com/posts';
constructor(private http: HttpClient) { }
// Obtenir totes les publicacions
getPublicacions(): Observable<Publicacio[]> {
return this.http.get<Publicacio[]>(this.apiUrl)
.pipe(
catchError(this.handleError)
);
}
// Obtenir publicacions d'un usuari
getPublicacionsPerUsuari(userId: number): Observable<Publicacio[]> {
return this.http.get<Publicacio[]>(`${this.apiUrl}?userId=${userId}`)
.pipe(
catchError(this.handleError)
);
}
// Obtenir una publicació per ID
getPublicacio(id: number): Observable<Publicacio> {
return this.http.get<Publicacio>(`${this.apiUrl}/${id}`)
.pipe(
catchError(this.handleError)
);
}
// Crear una nova publicació (POST)
crearPublicacio(publicacio: Omit<Publicacio, 'id'>): Observable<Publicacio> {
return this.http.post<Publicacio>(this.apiUrl, publicacio)
.pipe(
catchError(this.handleError)
);
}
// Gestió d'errors
private handleError(error: HttpErrorResponse) {
let errorMessage = 'Error desconegut';
if (error.error instanceof ErrorEvent) {
errorMessage = `Error: ${error.error.message}`;
} else {
errorMessage = `Codi d'error: ${error.status}, Missatge: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
Part 5: Crear els components per mostrar les dades¶
5.1 Crear el component de llista d'usuaris¶
- Genera el component:
ng generate component components/llista-usuaris
Nota Angular 20: El component es genera amb noms de fitxers simplificats:
llista-usuaris.ts,llista-usuaris.html,llista-usuaris.css. Els components són standalone per defecte.
- Edita
src/app/components/llista-usuaris/llista-usuaris.ts:
import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UsuarisService } from '../../services/usuaris.service';
import { Usuari } from '../../models';
@Component({
selector: 'app-llista-usuaris',
imports: [CommonModule],
templateUrl: './llista-usuaris.html',
styleUrl: './llista-usuaris.css'
})
export class LlistaUsuaris implements OnInit {
usuaris = signal<Usuari[]>([]);
carregant = signal(true);
error = signal<string | null>(null);
constructor(private usuarisService: UsuarisService) { }
ngOnInit(): void {
this.carregarUsuaris();
}
carregarUsuaris(): void {
this.carregant.set(true);
this.error.set(null);
this.usuarisService.getUsuaris().subscribe({
next: (dades) => {
this.usuaris.set(dades);
this.carregant.set(false);
},
error: (err) => {
this.error.set('Error al carregar els usuaris');
this.carregant.set(false);
console.error(err);
}
});
}
}
- Edita
src/app/components/llista-usuaris/llista-usuaris.html:
<div class="container">
<h2>Llista d'Usuaris</h2>
<!-- Indicador de càrrega -->
@if (carregant()) {
<div class="loading">
<p>Carregant usuaris...</p>
</div>
}
<!-- Missatge d'error -->
@if (error()) {
<div class="error">
<p>{{ error() }}</p>
<button (click)="carregarUsuaris()">Tornar a provar</button>
</div>
}
<!-- Llista d'usuaris -->
@if (!carregant() && !error()) {
<div class="usuaris-grid">
@for (usuari of usuaris(); track usuari.id) {
<div class="usuari-card">
<h3>{{ usuari.name }}</h3>
<p class="username">@{{ usuari.username }}</p>
<p><strong>Email:</strong> {{ usuari.email }}</p>
@if (usuari.phone) {
<p><strong>Telèfon:</strong> {{ usuari.phone }}</p>
}
@if (usuari.website) {
<p><strong>Web:</strong> {{ usuari.website }}</p>
}
@if (usuari.company) {
<p><strong>Empresa:</strong> {{ usuari.company.name }}</p>
}
</div>
}
</div>
}
</div>
Nota Angular 20: Utilitzem la nova sintaxi de control de flux (
@if,@for) en lloc de les directives estructurals (*ngIf,*ngFor). A més, els signals es criden amb().
- Edita
src/app/components/llista-usuaris/llista-usuaris.css:
.container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
h2 {
color: #3f51b5;
text-align: center;
margin-bottom: 30px;
}
.loading, .error {
text-align: center;
padding: 40px;
}
.error {
color: #d32f2f;
}
.error button {
margin-top: 10px;
padding: 10px 20px;
background-color: #3f51b5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error button:hover {
background-color: #303f9f;
}
.usuaris-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.usuari-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.usuari-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.usuari-card h3 {
color: #333;
margin: 0 0 5px 0;
}
.username {
color: #666;
font-style: italic;
margin-bottom: 15px;
}
.usuari-card p {
margin: 8px 0;
color: #555;
}
5.2 Crear el component de llista de publicacions¶
- Genera el component:
ng generate component components/llista-publicacions
- Edita
src/app/components/llista-publicacions/llista-publicacions.ts:
import { Component, OnInit, signal } from '@angular/core';
import { PublicacionsService } from '../../services/publicacions.service';
import { Publicacio } from '../../models';
@Component({
selector: 'app-llista-publicacions',
imports: [],
templateUrl: './llista-publicacions.html',
styleUrl: './llista-publicacions.css'
})
export class LlistaPublicacions implements OnInit {
publicacions = signal<Publicacio[]>([]);
carregant = signal(true);
error = signal<string | null>(null);
constructor(private publicacionsService: PublicacionsService) { }
ngOnInit(): void {
this.carregarPublicacions();
}
carregarPublicacions(): void {
this.carregant.set(true);
this.error.set(null);
this.publicacionsService.getPublicacions().subscribe({
next: (dades) => {
// Mostrem només les primeres 10 publicacions
this.publicacions.set(dades.slice(0, 10));
this.carregant.set(false);
},
error: (err) => {
this.error.set('Error al carregar les publicacions');
this.carregant.set(false);
console.error(err);
}
});
}
}
- Edita
src/app/components/llista-publicacions/llista-publicacions.html:
<div class="container">
<h2>Publicacions</h2>
<!-- Indicador de càrrega -->
@if (carregant()) {
<div class="loading">
<p>Carregant publicacions...</p>
</div>
}
<!-- Missatge d'error -->
@if (error()) {
<div class="error">
<p>{{ error() }}</p>
<button (click)="carregarPublicacions()">Tornar a provar</button>
</div>
}
<!-- Llista de publicacions -->
@if (!carregant() && !error()) {
<div class="publicacions-list">
@for (publicacio of publicacions(); track publicacio.id) {
<article class="publicacio-card">
<h3>{{ publicacio.title }}</h3>
<p>{{ publicacio.body }}</p>
<span class="autor">Usuari #{{ publicacio.userId }}</span>
</article>
}
</div>
}
</div>
- Edita
src/app/components/llista-publicacions/llista-publicacions.css:
.container {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h2 {
color: #673ab7;
text-align: center;
margin-bottom: 30px;
}
.loading, .error {
text-align: center;
padding: 40px;
}
.error {
color: #d32f2f;
}
.error button {
margin-top: 10px;
padding: 10px 20px;
background-color: #673ab7;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error button:hover {
background-color: #512da8;
}
.publicacions-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.publicacio-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.publicacio-card h3 {
color: #333;
margin: 0 0 15px 0;
text-transform: capitalize;
}
.publicacio-card p {
color: #666;
line-height: 1.6;
margin-bottom: 15px;
}
.autor {
display: inline-block;
background: #f0f0f0;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.9em;
color: #666;
}
Part 6: Integrar els components a l'aplicació¶
6.1 Configurar les rutes¶
Edita el fitxer src/app/app.routes.ts:
import { Routes } from '@angular/router';
import { LlistaUsuaris } from './components/llista-usuaris/llista-usuaris';
import { LlistaPublicacions } from './components/llista-publicacions/llista-publicacions';
export const routes: Routes = [
{ path: '', redirectTo: '/usuaris', pathMatch: 'full' },
{ path: 'usuaris', component: LlistaUsuaris },
{ path: 'publicacions', component: LlistaPublicacions }
];
6.2 Modificar el component principal¶
Edita src/app/app.ts:
import { Component, signal } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected readonly title = signal('Pràctica API REST');
}
Edita src/app/app.html:
<header>
<h1>{{ title() }}</h1>
<nav>
<a routerLink="/usuaris" routerLinkActive="active">Usuaris</a>
<a routerLink="/publicacions" routerLinkActive="active">Publicacions</a>
</nav>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<p>Pràctica 2 - Angular i API REST</p>
</footer>
Edita src/app/app.css:
header {
background: linear-gradient(135deg, #3f51b5, #673ab7);
color: white;
padding: 20px;
text-align: center;
}
header h1 {
margin: 0 0 15px 0;
}
nav {
display: flex;
justify-content: center;
gap: 20px;
}
nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 4px;
transition: background 0.3s;
}
nav a:hover {
background: rgba(255,255,255,0.2);
}
nav a.active {
background: rgba(255,255,255,0.3);
font-weight: bold;
}
main {
min-height: calc(100vh - 200px);
background: #f5f5f5;
}
footer {
background: #333;
color: white;
text-align: center;
padding: 15px;
}
6.3 Estils globals¶
Edita src/styles.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
}
Part 7: Executar i provar l'aplicació¶
- Obre la terminal d'IntelliJ
- Executa:
ng serve
- Obre el navegador a http://localhost:4200
- Prova la navegació entre les diferents seccions
- Verifica que es carreguen les dades de l'API
Part 8: Exercicis pràctics¶
Exercici 1: Afegir paginació¶
Modifica el component de publicacions per mostrar les publicacions amb paginació:
// Propietats
paginaActual = 1;
publicacionsPerPagina = 5;
// Mètode per obtenir publicacions de la pàgina actual
get publicacionsPagina(): Publicacio[] {
const inici = (this.paginaActual - 1) * this.publicacionsPerPagina;
const fi = inici + this.publicacionsPerPagina;
return this.publicacions.slice(inici, fi);
}
// Mètodes de navegació
paginaAnterior(): void {
if (this.paginaActual > 1) {
this.paginaActual--;
}
}
paginaSeguent(): void {
if (this.paginaActual * this.publicacionsPerPagina < this.publicacions.length) {
this.paginaActual++;
}
}
Exercici 2: Crear un component de detall d'usuari¶
- Genera un nou component
detall-usuari - Afegeix una ruta amb paràmetre:
/usuaris/:id - Mostra tota la informació de l'usuari
- Mostra les seves publicacions
Exercici 3: Crear un formulari per afegir publicacions¶
- Crea un nou component
nova-publicacio - Utilitza
FormsModuleoReactiveFormsModule - Envia la publicació a l'API amb el mètode
crearPublicaciodel servei
Exercici 4: Afegir cerca¶
Implementa un camp de cerca per filtrar usuaris per nom:
cercaText = '';
get usuarisFiltrats(): Usuari[] {
if (!this.cercaText) {
return this.usuaris;
}
return this.usuaris.filter(u =>
u.name.toLowerCase().includes(this.cercaText.toLowerCase())
);
}
Part 9: Conceptes clau apresos¶
| Concepte | Descripció |
|---|---|
| Interfície | Defineix l'estructura d'un objecte (propietats i tipus) |
| Servei | Classe que encapsula lògica de negoci i es pot injectar |
| Injecció de dependències | Pattern que permet passar dependències a una classe |
| Signal | Funció reactiva per gestionar l'estat (signal(), .set()) |
| Observable | Flux de dades asíncron (utilitzat per HttpClient) |
| subscribe | Mètode per rebre dades d'un Observable |
| HttpClient | Servei d'Angular per fer peticions HTTP |
| @if / @for | Nova sintaxi de control de flux (Angular 20) |
| RouterLink | Directiva per crear enllaços de navegació |
| RouterOutlet | Component que mostra el component de la ruta activa |
Diagrama de l'arquitectura¶
┌───────────────────────────────────────────────────────┐
│ APP COMPONENT │
│ ┌────────────────────────────────────────────────┐ │
│ │ ROUTER │ │
│ │ ┌───────────────┐ ┌────────────────────┐ │ │
│ │ │ LlistaUsuaris │ │ LlistaPublicacions │ │ │
│ │ └───────┬───────┘ └─────────┬──────────┘ │ │
│ └──────────┼──────────────────────┼──────────────┘ │
└─────────────┼──────────────────────┼──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌────────────────────┐
│ UsuarisService │ │ PublicacionsService│
└────────┬─────────┘ └──────────┬─────────┘
│ │
└───────────┬────────────┘
▼
┌───────────────────┐
│ HttpClient │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ API REST │
│ (JSONPlaceholder) │
└───────────────────┘
Resum¶
En aquesta pràctica has après a:
- ✅ Utilitzar conceptes d'orientació a objectes amb TypeScript
- ✅ Crear interfícies per definir l'estructura de les dades
- ✅ Crear serveis per encapsular la comunicació amb l'API
- ✅ Configurar HttpClient per fer peticions HTTP
- ✅ Gestionar dades asíncrones amb Observables
- ✅ Utilitzar Signals per gestionar l'estat dels components
- ✅ Crear components amb la nova sintaxi de control de flux (@if, @for)
- ✅ Configurar el sistema de rutes d'Angular
- ✅ Gestionar estats de càrrega i errors
Recursos addicionals¶
- Angular HttpClient Guide
- Angular Services & Dependency Injection
- Angular Signals
- TypeScript Classes
- TypeScript Interfaces
- RxJS Documentation
- JSONPlaceholder API