Salta el contingut

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

  1. Crea una carpeta models dins de src/app:
mkdir src/app/models
  1. 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.

  1. Obre el fitxer src/app/app.config.ts i 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

  1. Crea una carpeta services dins de src/app:
mkdir src/app/services
  1. Genera el servei amb Angular CLI:
ng generate service services/usuaris
  1. 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

  1. Genera el servei:
ng generate service services/publicacions
  1. 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

  1. 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.

  1. 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);
      }
    });
  }
}
  1. 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 ().

  1. 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

  1. Genera el component:
ng generate component components/llista-publicacions
  1. 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);
      }
    });
  }
}
  1. 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>
  1. 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ó

  1. Obre la terminal d'IntelliJ
  2. Executa:
ng serve
  1. Obre el navegador a http://localhost:4200
  2. Prova la navegació entre les diferents seccions
  3. 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

  1. Genera un nou component detall-usuari
  2. Afegeix una ruta amb paràmetre: /usuaris/:id
  3. Mostra tota la informació de l'usuari
  4. Mostra les seves publicacions

Exercici 3: Crear un formulari per afegir publicacions

  1. Crea un nou component nova-publicacio
  2. Utilitza FormsModule o ReactiveFormsModule
  3. Envia la publicació a l'API amb el mètode crearPublicacio del 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