Angular - Guia Completa de Desenvolupament¶
Índex¶
- TypeScript
- Hola Món en Angular
- Elements del Framework
- Formularis
- Arquitectura
- Desplegament
- Biblioteques de Tercers
- 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
experimentalDecoratorsentsconfig.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)¶
- Es carrega
index.html(HTML estàtic únic) - S'executa
main.ts(primer codi que s'executa) - Es carrega el mòdul principal (
AppModule) - Es inicia el component bootstrap (
AppComponent) - 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
Navegar a les Rutes¶
<!-- Amb routerLink -->
<a [routerLink]="['home']"
[routerLinkActive]="['active']">
Home
</a>
<!-- Amb ruta amb paràmetres -->
<a [routerLink]="['planet', product.id]">
{{product.name}}
</a>
Navegar per Codi¶
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 | Sí | Sí | No (un sol valor) |
| Cancelable | Sí | 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 aElementRefper 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