Salta el contingut

Angular - Guia Completa de Desenvolupament

Índex

  1. TypeScript
  2. Hola Món en Angular
  3. Elements del Framework
  4. Formularis
  5. Arquitectura
  6. Desplegament
  7. Biblioteques de Tercers
  8. Telèfons Mòbils

Introducció a Angular

Nom i Versions

El framework ha experimentat canvis significatius en la seva nomenclatura i esquema de versions:

  • Fins a la versió 1.7: Es denominava Angular.js (1.0, 1.2, 1.5, 1.7)
  • Després: Ha evolucionat com Angular 2, 4, 5, 6, 7, ..., fins a Angular 18.X.X actualment
  • Compatibilitat: La majoria del codi d'Angular 2 funciona en versions posteriors

Sistema de Versionat (exemple: Angular 7.0.2): - 7: Canvi de versió major (potencialment no retrocompatible) - 0: Actualització menor (retrocompatible, afegeix funcionalitats) - 2: Pedaços de seguretat (retrocompatible)

Cadència de publicació: Nova versió cada 6 mesos

Característiques Principals

  • Expressivitat en les plantilles HTML
  • Disseny modular amb suport per a Lazy Loading
  • Facilitat per reutilitzar components
  • Comunicació efectiva amb el backend (suporta Angular en el servidor)
  • Eines de desenvolupament potents (DevTools, Karma, Jasmine)
  • Integració amb frameworks de disseny (Bootstrap, Angular Material, Ionic)
  • Aplicacions de pàgina única (SPA)
  • Sistema modular extensible
  • Reactivitat simplificada
  • DOM virtual
  • Dissenyat per a aplicacions grans i complexes

Tipus d'Aplicacions Web

Tipus Descripció Avantatges Limitacions
Web Tradicional HTML generat al servidor, JavaScript per interacció Senzill, SEO amigable Comportament menys dinàmic
SPA HTML generat al client amb JSON/XML del servidor Experiència fluida, reactiva Complexitat inicial
PWA SPA que funciona sense connexió (Service Workers) Accessible offline, com una app, sense instal·lació Limitacions de hardware
App Híbrida JavaScript compilat a codi natiu (Ionic, Cordova) Accés a hardware, distribució com app Manteniment multiplataforma

Requisits Previs

Instal·lació de dependències:

# Node.js
sudo apt install nodejs

# npm
sudo apt install npm

# TypeScript
sudo npm install -g typescript

# Angular CLI
sudo npm install -g @angular/cli

# Actualitzar Node.js
sudo npm install -g n
sudo n stable
sudo npm install -g npm

Configuració de l'Editor

Extensions recomanades per a Visual Studio Code: - Angular 2 TypeScript Emmet - Angular Language Service - Angular Snippets (John Papa) - Material Icon Theme

DevTools del Navegador

Accés a DevTools d'Angular: https://angular.io/guide/devtools

Consideracions Inicials

Quan es comença amb qualsevol framework: - Temps d'aprenentatge significatiu - Canvi d'hàbits de programació - Abstracció respecte a crides de baix nivell - Possibles confusions sobre el flux d'execució - Dependència del framework per a actualitzacions de seguretat - Alguns frameworks poden tindre costos associats


TypeScript

Què és TypeScript?

TypeScript és un superset de JavaScript que afegeix tipat estàtic. Els navegadors no entengueren TypeScript directament; cal transpilar-lo a JavaScript.

Avantatges de TypeScript: - Tipat estàtic: Detecta errors en temps d'edició - Autocompletació inteligent basada en tipus - Programació més rigorosa i similar a llenguatges compilats - Suport per a classes i mètodes estàtics (ja disponibles en ES6) - Evita errors comuns: variables no definides, propietats inexistents

Problemes que soluciona: - Errors per variables no definides - Accés a propietats inexistents - Falta de documentació sobre el funcionament de funcions - Sobrescriptura accidental de variables globals

Transpilar TypeScript a JavaScript

TypeScript es transpila automàticament a JavaScript mitjançant eines sense errors. El compilador TypeScript s'encarrega de compatibilitat amb navegadors antics.

Configuració del compiler:

# Crear tsconfig.json
tsc --init

# Compilar automàticament en observar canvis
tsc -w

Nota: Angular gestiona automàticament la transpilació, sense necessitat de configuració manual.

Estàndards de Compilació

Per defecte, TypeScript transpila a ES5 (compatible amb navegadors antics). Els estàndards ES6 afegiren millores sintàctiques (classes, let, const, arrow functions) que faciliten la programació equivalent a TypeScript.

Tipus de Dades en TypeScript

// Assignació explícita de tipus (recomanat)
let nombre: string = 'Joaquin';
let numero: number = 123;
let booleano: boolean = true;
let hoy: Date = new Date();

// Arrays
let personajes: string[] = ['Paul', 'Jessica'];
let numeros: Array<number> = [1, 2, 3];

// Tipus dinàmic (no recomanat)
let cualquierDato; // any (dinàmic)

// Objectes
let persona = {
  nombre: 'Pepe',
  edad: 30
};

Paràmetres de Funcions

function saludar(
  quien: string,                    // Paràmetre obligatori
  momento?: string,                  // Paràmetre opcional
  objeto: string = 'la mano'        // Valor per defecte
) {
  if (momento) {
    console.log(`${quien} saludó con ${objeto} ${momento}`);
  } else {
    console.log(`${quien} saludó con ${objeto}`);
  }
}

saludar('Paul');                     // Únicament obligatori
saludar('Leto', 'por la tarde');
saludar('Gurney', 'el basilet', 'el cristal');

Return de Funcions

function sumar(a: number, b: number): number {
  return a + b;
}

function nombre(): string {
  return 'Pepe';
}

const multiplicar = (x: number, y: number): number => x * y;

Funcions Fletxa i Context this

Problema: Les funcions normals creen el seu propi context this:

const toptero = {
  posicion: 'aire',
  comunica() {
    // ❌ PROBLEMA: `this` no referencia `toptero`
    setTimeout(function() {
      console.log(`Posición: ${this.posicion}`); // undefined
    }, 1000);
  }
};
toptero.comunica();

Solució: Usar funcions fletxa que hereten el this del context exterior:

const toptero = {
  posicion: 'aire',
  comunica() {
    // ✅ CORRECTE: La funció fletxa hereta `this`
    setTimeout(() => {
      console.log(`Posición: ${this.posicion}`); // 'aire'
    }, 1000);
  }
};
toptero.comunica();

Promeses i Tipus Genèrics

ES5 no suporta promeses. Per usar-les, cal configurar target: 'ES6' en tsconfig.json.

const recogerEsencia = (cantidad: number): Promise<number> => {
  let cantidadActual = 1000;

  return new Promise((resolve, reject) => {
    if (cantidad > cantidadActual) {
      reject('No queda');
    } else {
      cantidadActual -= cantidad;
      resolve(cantidadActual);
    }
  });
};

recogerEsencia(500)
  .then(cantidadActual => console.log(`Queda ${cantidadActual}`))
  .catch(err => console.warn(err));

Interfaces

Les interfaces definiren l'estructura d'objectes i milloren la mantenibilitat del codi:

interface Caracter {
  nombre: string;
  edad: number;
  familia?: string;  // Propietat opcional
}

function enviarInterface(persona: Caracter) {
  console.log(`Enviando a ${persona.nombre} a Arrakis`);
}

const personaInterface: Caracter = { nombre: 'Hawat', edad: 80 };
enviarInterface(personaInterface);

Classes

class Recolector {
  private piloto: string = 'fremen';

  constructor(
    public identificador: string,
    public propietario: string,
    public buenEstado: boolean = true,
    private lugar?: string
  ) {}

  public getPiloto(): string {
    return this.piloto;
  }
}

const rec = new Recolector('1234', 'cofradia', true, 'desierto');
console.log(rec.getPiloto()); // 'fremen'
console.log(rec.piloto);      // ❌ Error: propietat privada

Modificadors d'accés: - public: Accessible des de qualsevol lloc (per defecte) - private: Accessible únicament dins de la classe - protected: Accessible dins de la classe i subclasses

Decoradors

Els decoradors són funcions que modifiquen classes, mètodes o propietats. Angular els utilitza extensament.

function imprimirConsola(constructorClase: Function) {
  console.log(constructorClase);
}

@imprimirConsola
class Recolector {
  constructor(
    public identificador: string,
    public propietario: string
  ) {}
}

const rec = new Recolector('1234', 'cofradia');

Nota: Cal activar experimentalDecorators en tsconfig.json


Hola Món en Angular

Creació de la Primera Aplicació

# Instal·lar Angular CLI (si no estava instal·lat)
sudo npm install -g @angular/cli

# Crear nou projecte
ng new my-app

# Navegar al directori
cd my-app

# Executar l'aplicació
ng serve -o

L'opció -o obri automàticament el navegador en http://localhost:4200.

Estructura Bàsica d'un Component

Template (app.component.html):

<p>Hola Mundo</p>
<ul>
  <li>{{nombre}}</li>
  <li>{{apellido}}</li>
</ul>

Component (app.component.ts):

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'my-app';
  nombre = 'Pepe';
  apellido = 'Lopez';
}

Angular CLI - Generació d'Elements

# Component
ng g component components/my-new-component

# Directiva
ng g directive directives/my-new-directive

# Pipe (filtre)
ng g pipe pipes/my-new-pipe

# Servici
ng g service services/my-new-service

# Classe
ng g class models/my-new-class

# Interfície
ng g interface models/my-new-interface

# Enum
ng g enum models/my-new-enum

# Mòdul
ng g module modules/my-new-module

# Guard
ng g guard guards/my-new-guard

# Instal·lar biblioteques
ng add @angular/material

# Actualitzar Angular
ng update --all

# Compilar per a producció
ng build --prod

Estructura del Projecte

my-app/
├── node_modules/           # Dependències de npm
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   ├── app.component.css
│   │   └── app.module.ts
│   ├── assets/             # Recursos estàtics
│   ├── environments/        # Variables d'entorn
│   ├── index.html          # HTML principal
│   ├── main.ts             # Punt d'entrada
│   └── styles.css          # Estils globals
├── angular.json            # Configuració d'Angular
├── tsconfig.json           # Configuració de TypeScript
└── package.json            # Dependències del projecte

Elements del Framework

Mòduls (NgModule)

Tota aplicació Angular no-standalone necessita almenys un mòdul principal (app.module.ts).

Beneficis dels mòduls: - Organització eficient del codi - Lazy Loading: Carregar mòduls sols quan es necessiten - Contenidors de components, servicis, pipes, etc.

NgModule vs Components Standalone

Versions anteriors d'Angular utilitzaven @NgModule. La tendència actual és vers components Standalone:

// ❌ Estil anterior (NgModule)
@NgModule({
  declarations: [AppComponent, HeaderComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

// ✅ Estil actual (Standalone)
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent { }

Avantatges dels components Standalone: - Més simples de crear i reutilitzar - Menys boilerplate - Més fàcil de mantindre

Mòdul Principal (app.module.ts)

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Arrays del decorador: - bootstrap: Únicament per al mòdul principal (component inicial) - declarations: Components, directives i pipes (sols pueden estar en un mòdul) - exports: Compartir elements amb altres mòduls - imports: Mòduls externs que importem - providers: Servicis globals (normalment, sols al mòdul app)

Inicialització de l'Aplicació (Bootstrap)

  1. Es carrega index.html (HTML estàtic únic)
  2. S'executa main.ts (primer codi que s'executa)
  3. Es carrega el mòdul principal (AppModule)
  4. Es inicia el component bootstrap (AppComponent)
  5. Angular i les rutes prenen el control

Components

Un component és un controlador d'una vista HTML associada. Els components són entitats independents i reutilitzables.

Estructura d'un Component

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'hola-mon';
}

Propietats del decorador: - selector: Etiqueta HTML per usar el component (<app-root>) - templateUrl: Arxiu HTML del component - template: HTML directament en la classe (no recomanat) - styleUrls: Array de fitxers CSS - styles: Array de CSS en línea

Cicle de Vida dels Components

export class MyComponent implements OnInit, OnDestroy {
  constructor() { }

  ngOnInit(): void {
    // S'executa únicament una vegada al crear-se el component
    // Ideal per obtindre dades del servidor
  }

  ngAfterContentInit(): void {
    // Après que s'ha inicialitzat el contingut
  }

  ngAfterViewInit(): void {
    // Después de que s'ha renderitzat la vista completament
  }

  ngOnChanges(changes: SimpleChanges): void {
    // Cada vegada que canvia una propietat @Input
  }

  ngDoCheck(): void {
    // Detecta canvis que Angular no pot detectar automàticament
    // No recomanat (s'executa constantment)
  }

  ngOnDestroy(): void {
    // Quan el component desapareix de la vista
    // Útil per alliberar recursos
  }
}

Components Niuats (@Input i @Output)

// Component pare
import { Component } from '@angular/core';

@Component({
  selector: 'app-catalogue',
  template: `
    <div class="row">
      <div class="col" *ngFor="let product of products">
        <app-product-item [p]="product" 
                          (ratingChanged)="changeRating($event, product)">
        </app-product-item>
      </div>
    </div>
  `
})
export class CatalogueComponent {
  products: Product[] = [...];

  changeRating(stars: number, product: Product) {
    product.rating = stars;
  }
}

// Component fill
@Component({
  selector: 'app-product-item',
  template: `...`
})
export class ProductItemComponent {
  @Input({ required: true }) p: Product;
  @Input() p?: Product | undefined;
  @Input('productName') name: string;  // Alias

  @Output() ratingChanged = new EventEmitter<number>();

  puntuar(i: number): void {
    this.ratingChanged.emit(i);
  }
}

Opcions de @Input: - required: true: Obligatori (dona error si el pare no l'envia) - Alias: @Input('productName') per usar un nom diferent en el template - ? o | undefined: Opcional

Attribute Selector

Per evitar un "wrapper" HTML (útil per a <tr> i elements especials):

@Component({
  selector: '[app-edifici]',  // Selector entre [ ]
  template: `...`
})
export class EdificiComponent { }

// Ús
<tr app-edifici [punto]="punto"></tr>

Vinculacions (Bindings) i Interpolacions

Interpolacions {{ }}

<!-- Mostra variables -->
<p>{{variable}}</p>

<!-- Executa funcions -->
<p>{{miMetodo()}}</p>

<!-- Expressions -->
<img [src]="{{urlImagen}}">

Limitacions: - No permeten if, for, while - Context únicament del component (no globals) - Mantenir simplicitat

Vincular Atributs [propiedad]

<!-- ✅ Millor -->
<img [src]="product.imageUrl" alt="">

<!-- ❌ No recomanat -->
<img src="{{product.imageUrl}}" alt="">

<!-- Estils -->
<div [style.height.px]="imageHeight"></div>
<div [style.color]="dynamicColor"></div>

ngStyle i ngClass

<!-- ngStyle -->
<div [ngStyle]="{'background-color': isEven ? 'red' : 'green'}"></div>
<div [ngStyle]="{'width.px': width}"></div>
<div [ngStyle]="styleObject"></div>

<!-- ngClass -->
<div [ngClass]="{'even': isEven, 'last': isLast}"></div>
<div [ngClass]="['even', 'active']"></div>
<div [ngClass]="classObject"></div>

Vinculació Bidireccional [(ngModel)]

<!-- Cal importar FormsModule -->
<input type="text" 
       [(ngModel)]="filterSearch" 
       class="form-control"
       name="filterDesc">

<!-- Per a inputs aïllats sense formulari -->
<input type="text"
       [(ngModel)]="filterSearch"
       [ngModelOptions]="{ standalone: true }">

Vincular Esdeveniments (click)

<button class="btn" 
        [ngClass]="{'btn-danger': showImage, 'btn-primary': !showImage}"
        (click)="toggleImage()">
  {{showImage ? 'Ocultar' : 'Mostrar'}} imagen
</button>

Directives Estructurals

ngIf / ngFor / ngSwitch

<!-- ngIf -->
<div *ngIf="mostrar">
  <h3>{{frase.autor}}</h3>
  <p>{{frase.mensaje}}</p>
</div>

<!-- ngIf amb else -->
<div *ngIf="show; else elseBlock">
  La condició és verdadera
</div>
<ng-template #elseBlock>
  La condició és falsa
</ng-template>

<!-- ngFor -->
<ul>
  <li *ngFor="let cita of citas; let i = index">
    {{i}} - {{cita}}
  </li>
</ul>

<!-- Context variables de ngFor -->
<!-- $index, $first, $last, $even, $odd, $count -->

<!-- ngSwitch -->
<span [ngSwitch]="property">
  <span *ngSwitchCase="'val1'">Valor 1</span>
  <span *ngSwitchCase="'val2'">Valor 2</span>
  <span *ngSwitchDefault>Altre valor</span>
</span>

Syntax Moderns: @if, @for, @switch (Angular 17+)

<!-- No cal importar res, més optimitzat -->
@if (names.length > 0) {
  @for (name of names; track $index) {
    <p>
      {{$index}} - {{name}}
    </p>
  }
} @else {
  <p>No names</p>
}

<!-- Variables locals -->
@for (name of names; track $index; 
      let first = $first, 
      let last = $last,
      let odd = $odd, 
      let even = $even, 
      let count = $count) {
  <p>{{$index}}: {{name}}</p>
} @empty {
  <p>No items</p>
}

Combinar Directives amb ng-container

<ng-container *ngFor="let person of persons">
  <ng-container *ngIf="person.age >= 18">
    <p>{{person | json}}</p>
  </ng-container>
</ng-container>

<ng-container> no deixa element HTML en la versió final


Interfaces

ng g interface interfaces/i-product

export interface IProduct {
  id: number;
  description: string;
  price: number;
  available: Date;
  imageUrl: string;
  rating: number;
}

Ús en components:

export class ProductComponent {
  products: IProduct[] = [
    {
      id: 1,
      description: 'SSD hard drive',
      available: new Date('2016-10-03'),
      price: 75,
      imageUrl: 'assets/ssd.jpg',
      rating: 5
    },
    // ... més productes
  ];
}

Rutes

Crear Rutes

import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { PlanetListComponent } from './pages/planets/planet-list.component';
import { PlanetDetailComponent } from './pages/planets/planet-detail.component';
import { LoginComponent } from './pages/login/login.component';
import { AuthGuard } from './guards/auth.guard';
import { PlanetResolver } from './resolvers/planet.resolver';

export const routes: Routes = [
  { path: 'home', component: HomeComponent },

  { 
    path: 'planets', 
    component: PlanetListComponent,
    canActivate: [AuthGuard] 
  },

  { 
    path: 'planet/:id', 
    component: PlanetDetailComponent,
    canActivate: [AuthGuard] 
  },

  { 
    path: 'planet/edit/:id', 
    component: PlanetEditComponent,
    canActivate: [AuthGuard],
    canDeactivate: [LeavePageGuard],
    resolve: { planet: PlanetResolver }
  },

  { path: 'login', component: LoginComponent },

  // Ruta per defecte (sempre última)
  { path: '**', pathMatch: 'full', redirectTo: 'home' }
];

Rutes amb Hash (#)

import { ApplicationConfig } from '@angular/core';
import { provideRouter, withHashLocation } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withHashLocation())
  ]
};

Avantatges de les rutes amb hash: - Funciona en tots els navegadors - Més simple per enviar paràmetres - No necessita manipulació del servidor

<!-- Amb routerLink -->
<a [routerLink]="['home']" 
   [routerLinkActive]="['active']">
  Home
</a>

<!-- Amb ruta amb paràmetres -->
<a [routerLink]="['planet', product.id]">
  {{product.name}}
</a>
import { Router } from '@angular/router';

export class ProductComponent {
  constructor(private router: Router) { }

  detailsProduct(id: number): void {
    this.router.navigate(['/planet', id]);
  }
}

Obtindre Paràmetres de Rutes

import { ActivatedRoute } from '@angular/router';
import { OnInit } from '@angular/core';

export class PlanetDetailComponent implements OnInit {
  planetId: number;

  constructor(private activatedRoute: ActivatedRoute) { }

  ngOnInit(): void {
    // Forma tradicional (Observable)
    this.activatedRoute.params.subscribe(params => {
      this.planetId = params['id'];
    });

    // Forma moderna (Angular 16+)
    // @Input() id: number;  // Amb withComponentInputBinding()
  }
}

Transicions de Rutes (Angular 19+)

import { withViewTransitions } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withViewTransitions())
  ]
};

// CSS
::view-transition-old(main), 
::view-transition-new(main) {
  animation-duration: 200ms;
}

Servicis

Els servicis proporcionen dades reutilitzables a tota l'aplicació.

Crear un Servici

ng g service services/product
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { IProduct } from '../interfaces/i-product';

@Injectable({
  providedIn: 'root'  // Disponible globalment
})
export class ProductService {
  private productURL = 'http://api.example.com/products';

  constructor(private http: HttpClient) { }

  getProducts(): Observable<IProduct[]> {
    return this.http.get<{ products: IProduct[] }>(this.productURL).pipe(
      map(response => response.products)
    );
  }

  getProduct(id: number): Observable<IProduct> {
    return this.http.get<IProduct>(`${this.productURL}/${id}`);
  }

  createProduct(product: IProduct): Observable<IProduct> {
    return this.http.post<IProduct>(this.productURL, product);
  }

  updateProduct(product: IProduct): Observable<IProduct> {
    return this.http.put<IProduct>(
      `${this.productURL}/${product.id}`, 
      product
    );
  }

  deleteProduct(id: number): Observable<void> {
    return this.http.delete<void>(`${this.productURL}/${id}`);
  }
}

Injecció de Dependències

export class ProductComponent implements OnInit {
  products: IProduct[] = [];

  // Angular injecta automàticament el servici
  constructor(private productService: ProductService) { }

  ngOnInit(): void {
    this.loadProducts();
  }

  loadProducts(): void {
    this.productService.getProducts().subscribe(
      prods => this.products = prods,           // Èxit
      error => console.error(error),             // Error (opcional)
      () => console.log('Products loaded')      // Completat (opcional)
    );
  }
}

HttpClientModule

import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient()
  ]
};

POST amb HttpClient

import { HttpClient, HttpHeaders } from '@angular/common/http';

export class AuthService {
  private loginURL = 'http://api.example.com/login';
  private httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  };

  constructor(private http: HttpClient) { }

  login(credentials: any): Observable<any> {
    return this.http.post(
      this.loginURL,
      JSON.stringify(credentials),
      this.httpOptions
    );
  }
}

Observables vs Signals vs Promeses

Comparativa

Característica Observable Signal Promesa
Múltiples valors No (un sol valor)
Cancelable No No
Lazy Sí (sense subscribe no s'executa) No No (s'executa immediatament)
Operadors Sí (map, filter, etc.) No No
Reactivitat Avançada Simple N/A

Observables (RxJS)

import { Observable } from 'rxjs';
import { map, filter } from 'rxjs/operators';

export class DataService {
  getNumbers(): Observable<number[]> {
    return new Observable(observer => {
      observer.next([1, 2, 3]);
      observer.next([4, 5, 6]);
      observer.complete();
    });
  }
}

// Ús
this.dataService.getNumbers().pipe(
  map(nums => nums.map(n => n * 2)),
  filter(nums => nums.length > 0)
).subscribe(
  result => console.log(result),
  error => console.error(error),
  () => console.log('Completat')
);

Signals (Angular 17+)

import { signal, effect, computed } from '@angular/core';

export class CounterComponent {
  count = signal(0);

  // Senyals computades
  doubleCount = computed(() => this.count() * 2);

  constructor() {
    // Efectes que s'executen quan canvia el senyal
    effect(() => {
      console.log(`Count: ${this.count()}`);
    });
  }

  increment(): void {
    this.count.update(c => c + 1);
  }

  reset(): void {
    this.count.set(0);
  }
}

// Template
<p>Count: {{count()}}</p>
<p>Double: {{doubleCount()}}</p>
<button (click)="increment()">Increment</button>

Mostrar Dades Asíncrones

<!-- Operador async (automàtic subscribe/unsubscribe) -->
<p>{{productService.getProduct(1) | async | json}}</p>

<!-- Amb ngIf per evitar errors -->
<div *ngIf="products$ | async as products">
  <div *ngFor="let product of products">
    {{product.name}}
  </div>
</div>

<!-- Operador ? (safe navigation) -->
<p>{{product?.description}}</p>

Resolver

Els resolvers obtenen dades del servidor abans d'accedir a una ruta:

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ProductService } from '../services/product.service';
import { Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class ProductResolver implements Resolve<IProduct> {
  constructor(
    private productService: ProductService,
    private router: Router
  ) { }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<IProduct> {
    return this.productService.getProduct(route.params['id']).pipe(
      catchError(error => {
        console.error('Error loading product', error);
        this.router.navigate(['/products']);
        return of(null as any);
      })
    );
  }
}

// Ruta
{ 
  path: 'product/edit/:id',
  resolve: { product: ProductResolver },
  component: ProductEditComponent 
}

// Accedir a les dades
export class ProductEditComponent implements OnInit {
  product: IProduct;

  constructor(private activatedRoute: ActivatedRoute) { }

  ngOnInit(): void {
    this.product = this.activatedRoute.snapshot.data['product'];
  }
}

Pipes (Filtres)

Els pipes transformen dades en les plantilles sense modificar l'original:

<!-- Pipes built-in -->
<img [title]="product.desc | uppercase">
<td>{{ product.price | currency:'EUR':'symbol' }}</td>
<td>{{ product.available | date:'dd/MM/yyyy' }}</td>
<td>{{ product.rating | number:'1.1-2' }}</td>

<!-- Sense pipe -->
<p>{{ product | json }}</p>

Crear un Pipe Personalitzat

ng g pipe pipes/product-filter
import { Pipe, PipeTransform } from '@angular/core';
import { IProduct } from '../interfaces/i-product';

@Pipe({
  name: 'productFilter',
  standalone: true
})
export class ProductFilterPipe implements PipeTransform {
  transform(
    products: IProduct[],
    filterBy: string
  ): IProduct[] {
    const filter = filterBy 
      ? filterBy.toLocaleLowerCase() 
      : null;

    return filter
      ? products.filter(p => 
          p.description.toLocaleLowerCase().includes(filter)
        )
      : products;
  }
}

// Ús
<tr *ngFor="let product of products | productFilter:filterSearch">
  <td>{{product.description}}</td>
</tr>

Guards

Els guards protegeixen rutes basant-se en condicions:

ng g guard guards/auth

CanActivate Guard

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (this.authService.isLoggedIn()) {
      return true;
    } else {
      this.router.navigate(['/login']);
      return false;
    }
  }
}

// Ús en ruta
{ 
  path: 'product/:id',
  canActivate: [AuthGuard],
  component: ProductDetailComponent 
}

CanDeactivate Guard

@Injectable({ providedIn: 'root' })
export class LeavePageGuard implements CanDeactivate<any> {
  canDeactivate(component: any): boolean {
    if (component.formulario.dirty) {
      return confirm('Tens canvis sense guardar. Segur que vols abandonar?');
    }
    return true;
  }
}

// Ruta
{ 
  path: 'product/edit/:id',
  canDeactivate: [LeavePageGuard],
  component: ProductEditComponent 
}

Functional Guards (Forma moderna)

import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard = (route: any, state: any) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  }

  router.navigate(['/login']);
  return false;
};

Directives Personalitzades

Directiva d'Atribut

ng g directive directives/highlight
import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor: string = 'yellow';

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) { }

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.renderer.setStyle(
      this.el.nativeElement,
      'background-color',
      this.highlightColor
    );
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    this.renderer.setStyle(
      this.el.nativeElement,
      'background-color',
      'null'
    );
  }
}

// Ús
<div [appHighlight]="'lightblue'">Hovered element</div>

Nota: Renderer2 és preferible a ElementRef per compatibilitat multiplataforma

Template Reference Variables

<input #username type="text">
<button (click)="login(username.value)">Login</button>

<div #modal style="display: none">
  <p>Modal content</p>
</div>

Autenticació i Seguretat

Servicis, Tokens i Guards

Flux típic: 1. Formulari de login demana credencials 2. Servici es connecta al servidor 3. Servidor retorna un token (JWT, etc.) 4. Servici guarda el token en localStorage 5. Guard consulta el servici per autoritzar rutes

export class AuthService {
  private isLoggedIn = new BehaviorSubject<boolean>(false);

  constructor(private http: HttpClient) {
    this.checkToken();
  }

  login(email: string, password: string): Observable<any> {
    return this.http.post<any>('http://api.example.com/login', {
      email,
      password
    }).pipe(
      map(response => {
        if (response.token) {
          localStorage.setItem('token', response.token);
          this.isLoggedIn.next(true);
        }
        return response;
      })
    );
  }

  logout(): void {
    localStorage.removeItem('token');
    this.isLoggedIn.next(false);
  }

  isAuthenticated(): Observable<boolean> {
    return this.isLoggedIn.asObservable();
  }

  private checkToken(): void {
    const token = localStorage.getItem('token');
    this.isLoggedIn.next(!!token);
  }
}

Interceptors

Els interceptors modifiquen cada petició HTTP (per exemple, afegir el token):

ng g interceptor interceptors/auth
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('token');

    if (token) {
      const authReq = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(authReq);
    }

    return next.handle(req);
  }
}

// Registrar en app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    { 
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true 
    }
  ]
};

BehaviorSubject per a Variables Globals

import { BehaviorSubject } from 'rxjs';

export class StateService {
  private userSubject = new BehaviorSubject<User | null>(null);
  public user$ = this.userSubject.asObservable();

  setUser(user: User): void {
    this.userSubject.next(user);
  }

  getUser(): User | null {
    return this.userSubject.value;
  }
}

// Ús
this.stateService.user$.subscribe(user => {
  if (user) {
    console.log(`Welcome ${user.name}`);
  }
});

Formularis

Plantilla vs Reactius

Aspecte Plantilla Reactiu
Validació HTML5 + Angular Codi TypeScript
Complexitat Simple Avançada
Control Template Component
Recomanat per Search, Login simple Formularis complexos

Formularis de Plantilla

import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-form',
  imports: [CommonModule, FormsModule],
  template: `
    <form #productForm="ngForm" novalidate (ngSubmit)="create(productForm)">
      <input type="text"
             name="description"
             [(ngModel)]="product.description"
             minlength="5"
             maxlength="600"
             required
             #descModel="ngModel"
             [ngClass]="{'is-valid': descModel.valid && descModel.touched,
                         'is-invalid': descModel.invalid && descModel.touched}">

      <div *ngIf="descModel.invalid && descModel.touched" class="alert alert-danger">
        Description is required (5-600 chars)
      </div>

      <button type="submit" [disabled]="productForm.invalid">Submit</button>
    </form>
  `
})
export class FormComponent {
  product = { description: '' };

  create(form: NgForm): void {
    if (form.valid) {
      console.log(form.value);
    }
  }
}

Modificar l'entrada en temps real

<input type="text"
       [ngModel]="product.description"
       (ngModelChange)="product.description = $event.toUpperCase()">

Formularis Reactius

import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="formulario" (ngSubmit)="create()">
      <input type="text"
             formControlName="name"
             [ngClass]="nameNotValid ? 'is-invalid' : 'is-valid'">

      <div *ngIf="nameNotValid" class="alert alert-danger">
        Name is required (min 5 chars)
      </div>

      <button type="submit" [disabled]="formulario.invalid">Submit</button>
    </form>
  `
})
export class ReactiveFormComponent {
  formulario: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.crearFormulario();
  }

  crearFormulario(): void {
    this.formulario = this.formBuilder.group({
      name: ['', [Validators.required, Validators.minLength(5)]],
      price: [0, Validators.min(0.01)],
      description: ['']
    });
  }

  get nameNotValid(): boolean {
    return this.formulario.get('name')?.invalid &&
           this.formulario.get('name')?.touched;
  }

  create(): void {
    if (this.formulario.valid) {
      console.log(this.formulario.value);
    }
  }
}

Validadors Personalitzats

import { ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';

function minPriceValidator(min: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (control.value && control.value < min) {
      return { minPrice: { min } };
    }
    return null;
  };
}

// Ús
this.formulario = this.formBuilder.group({
  price: [0, [Validators.required, minPriceValidator(0.01)]]
});

Cross-Field Validation

const passwordValidator: ValidatorFn = (control: AbstractControl) => {
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  if (password && confirmPassword && password.value !== confirmPassword.value) {
    return { passwordMismatch: true };
  }

  return null;
};

this.formulario = this.formBuilder.group({
  password: ['', Validators.required],
  confirmPassword: ['', Validators.required]
}, { validators: passwordValidator });

FormArray (Formularis Dinàmics)

export class DynamicFormComponent {
  formulario: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.formulario = this.formBuilder.group({
      name: [''],
      items: this.formBuilder.array([
        this.createItem()
      ])
    });
  }

  createItem(): FormGroup {
    return this.formBuilder.group({
      description: ['', Validators.required]
    });
  }

  get items(): FormArray {
    return this.formulario.get('items') as FormArray;
  }

  addItem(): void {
    this.items.push(this.createItem());
  }

  removeItem(index: number): void {
    this.items.removeAt(index);
  }
}

// Template
<form [formGroup]="formulario">
  <div formArrayName="items">
    <div *ngFor="let item of items.controls; let i = index" [formGroupName]="i">
      <input type="text" formControlName="description">
      <button type="button" (click)="removeItem(i)">Delete</button>
    </div>
  </div>
  <button type="button" (click)="addItem()">Add Item</button>
</form>

Arquitectura

Mòduls i Criteris de Separació

Tipus de mòduls: - Mòduls de domini: Per separar codi sense representar una ruta (ex: menú) - Mòduls de secció: Una secció/ruta de l'aplicació (ex: productes, clients) - Mòduls de servicis: Agrupen servicis globals - Mòduls de components: Components reutilitzables en diversos mòduls

Seleccio de Ferramentes

Necessidat Ferramenta
Parts diferenciades del HTML Components
Navegació dins l'aplicació Rutes
Mostrar variables {{}} (Interpolació)
Canviar estils dinàmicament ngStyle, ngClass, [style]
Formularis [(ngModel)] o Formularis Reactius
Esdeveniments específics ()
Dades utilitzades en varis components Interfaces
Esdeveniments recurrents Directives d'atribut
Protegir rutes Guards
Guardar/servir dades Servicis, Resolvers
Variables globals Environment, Observable, NgRx
Partes reutilitzables en altres projectes Mòduls

Principis d'Arquitectura

Single Responsibility Principle: - Fitxers menuts i enfocats - Un fitxer per cada classe, component, interface - DRY (Don't Repeat Yourself)

Disseny: - Preferir funcionalitat reactiva - Composició sobre herència - Components Dumb/Smart: - Dumb: Reben dades per @Input, retornen per @Output - Smart: Es connecten a servicis

Directori d'Estructura

Veure guia completa: https://angular.io/guide/styleguide#overall-structural-guidelines


Desplegament

Compilació per a Producció

# Compilació estàndard (amb símbol de debug)
ng build

# Compilació per a producció (minificat, optimitzat)
ng build --prod

# Noms aleatoris per a fitxers (prevé cache del navegador)
# Automàtic amb --prod

Variables d'entorn: - Per a desenvolupament: src/environments/environment.ts - Per a producció: src/environments/environment.prod.ts

Desplegament en Apache

# Si es puja a una subcarpeta
ng build --base-href='/subcarpeta/'

# Per a que Apache no interprete les rutes
# S'usa .htaccess o { useHash: true } en les rutes

Fitxer .htaccess:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Generador d'htaccess: https://julianpoemp.github.io/ngx-htaccess-generator/


Biblioteques de Tercers

Bootstrap

# Opció 1: CDN (recomanat per simplicitat)
# Afegir a index.html
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

# Opció 2: npm
npm install bootstrap

# Afegir a angular.json
"styles": [
  "node_modules/bootstrap/dist/css/bootstrap.min.css",
  "src/styles.scss"
],
"scripts": [
  "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]

ng-bootstrap

ng add @ng-bootstrap/ng-bootstrap

# Ja funciona sense jQuery (Bootstrap 5+)

Angular Material

ng add @angular/material

# Documentació i ejemples
# https://material.angular.io/

Ngx Charts

npm install @swimlane/ngx-charts --save

# Afegir a app.config.ts
import { NgxChartsModule } from '@swimlane/ngx-charts';

# Veure ejemplos a:
# https://swimlane.github.io/ngx-charts/

Angular + Supabase

npm install @supabase/supabase-js

# environment.ts
export const environment = {
  production: false,
  supabaseUrl: 'YOUR_SUPABASE_URL',
  supabaseKey: 'YOUR_SUPABASE_KEY'
};

# Supabase funciona amb Promeses (convertible a Observable amb from())

Telèfons Mòbils

Tipus d'Aplicacions

Pàgina Web Responsive

Angular amb Bootstrap o Material està totalment preparat per a crear pàgines responsives. Els navegadors moderns inclouen simuladors de dispositius mòbils.

Progressive Web App (PWA)

Característiques: - Progressiva: Funciona en navegadors antics - Responsiva: Adapta-se a qualsevol dispositiu - Connectivitat independent: Funciona offline gràcies a Service Workers - Com una app: Experiència similar a una aplicació nativa - Actualitzacions: Via Service Workers sense necessitat d'App Store - Segura: Servida per HTTPS - Detectable: Manifest.json i registro de Service Worker - Instal·lable: Icona PNG, sense instal·lació complexa - Vincular·la: Únicament una URL

App Híbrida

Stack típic: - Framework: Angular, React, o Vue.js - Motor: Ionic, Cordova, o Capacitor - Accés a hardware: Càmera, GPS, sensors, etc.

# Crear app híbrida amb Ionic
npm install -g ionic
ionic start myApp --type=angular

App Quasi Nativa

Tecnologies com React Native o Flutter que es compilen a codi natiu, perdent compatibilitat amb la web però guanyant rendiment.


Recursos Addicionals No Tractats

Aquests temes no s'han desenvolupat però són importants:

  • Internacionalització (i18n): https://angular.dev/guide/i18n
  • Tests: https://angular.dev/guide/testing (Jasmine, Jest)
  • Redux/NgRx: Gestió d'estat avançada
  • Performance: Optimitzacions i Lazy Loading
  • Debugging: DevTools, console, breakpoints

Conclusió

Angular és un framework poderós per a construir aplicacions web escalables. La seva corba d'aprenentatge és pronunciada però la productivitat a llarg termini justifica l'esforç inicial.

Recomanacions finals: 1. Domina els conceptes bàsics (Components, Servicis, Rutes) 2. Practica amb projectes petits 3. Aprèn les millors pràctiques d'arquitectura 4. Estàbileix un estàndard de codi en el teu equip 5. Mantingut el codi simple i llegible