Has vist una llibreria sense opinions (React), un framework progressiu que s'adopta per capes (Vue) i, abans dels dos, la teva pròpia aplicació en JavaScript pur on totes les decisions eren teves. Angular és a l'altre extrem de l'eix que 10-01 anomenava «llibreria davant de framework»: és una plataforma completa, amb encaminador, client HTTP, sistema de formularis, injecció de dependències, entorn de proves, generador de codi i renderitzat al servidor inclosos i mantinguts pel mateix equip. I amb TypeScript no com a opció sinó com a requisit de fet. Això el fa el més costós d'aprendre dels tres i, alhora, el més previsible: dos projectes Angular d'empreses diferents s'assemblen molt més entre si que dos projectes React. En aquesta lliçó veuràs què significa «amb opinions» quan es porta fins al final; el mínim de TypeScript per llegir el codi —tipus, interfícies i decoradors—, remetent a 11-07 per aprendre'l de debò; la CLI i l'estructura d'un projecte; els components independents (standalone) amb els seus imports, que avui són la forma recomanada; les quatre formes d'enllaç de dades i la sintaxi de control de flux actual (@if, @for amb el seu track obligatori —el mateix data-id de sempre—, @switch i @defer); les senyals (signal, computed, effect, i les entrades i sortides basades en senyals) com a model de reactivitat actual d'Angular, comparades amb els ref i computed de Vue, amb una menció de Zone.js i de la tendència zoneless; la injecció de dependències, que és la peça que més diferencia Angular i que ja vas practicar sense saber-ho a 08-04; HttpClient i el just de RxJS —què és un flux, subscribe, pipe, l'async de la plantilla i per què cal donar-se de baixa— aplicat al teu llistarTasques de 07-02; l'encaminador amb càrrega diferida, reprenent 09-05; els formularis reactius amb validadors que reutilitzen les teves regles R1–R10; les proves amb TestBed i una nota sobre SSR. I al final, la mateixa llista de tasques amb filtre, per quarta vegada.
Contingut
- Angular com a plataforma completa
- Què significa «amb opinions» portat fins al final
- TypeScript: el mínim per llegir aquesta lliçó
- Decoradors: què són i per què els fa servir Angular
- La CLI i l'estructura d'un projecte
- Components independents (
standalone) - Plantilla i estils: les tres formes d'escriure'ls
- Enllaç de dades en les seves quatre formes
- La sintaxi de control de flux actual
@fori el seutrackobligatori@defer: càrrega diferida declarativa- Senyals:
signal,computedieffect - Entrades i sortides basades en senyals
- Zone.js, la detecció de canvis i la tendència zoneless
- Senyals d'Angular davant dels
refde Vue - Injecció de dependències: serveis i
inject() - Per què la injecció de dependències diferencia Angular
- La semblança amb el que vas fer a 08-04
HttpClienti RxJS: el just- L'
asyncde la plantilla i per què cal donar-se de baixa - Quan senyals i quan observables
- L'encaminador amb càrrega diferida
- Formularis reactius davant de formularis de plantilla
- Validadors amb les regles R1–R10
- Proves amb TestBed
- Una nota sobre SSR
- Nómada Tasques en Angular: la llista completa
- Comparació amb les tres versions anteriors
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Angular com a plataforma completa
La diferència comença a l'inventari. Això ve inclòs, és oficial i s'actualitza alhora:
| Necessitat | Angular | React | Vue |
|---|---|---|---|
| Components i reactivitat | Inclòs | Inclòs | Inclòs |
| Encaminament | Inclòs (Angular Router) | Es tria | Oficial a part (Vue Router) |
| Peticions HTTP | Inclòs (HttpClient) |
Es tria | Es tria |
| Formularis i validació | Inclòs (dos sistemes) | Es tria | Es tria |
| Injecció de dependències | Inclosa | No existeix | No existeix |
| Programació asíncrona avançada | Inclosa (RxJS) | Es tria | Es tria |
| Generació de codi | Inclosa (ng generate) |
No | Parcial |
| Entorn de proves | Inclòs (TestBed) | Es tria | Oficial a part |
| Internacionalització | Inclosa | Es tria | Es tria |
| Renderitzat al servidor | Inclòs | Meta-framework | Meta-framework (Nuxt) |
| Actualitzacions automàtiques de codi | Incloses (ng update) |
No | Parcial |
| Llenguatge | TypeScript | Tots dos | Tots dos |
Aquesta última fila d'ng update mereix atenció perquè és una conseqüència poc comentada de tenir-ho tot sota el mateix sostre: quan Angular canvia una API, publica migracions automàtiques que reescriuen el teu codi. Actualitzar un projecte Angular gran acostuma a ser executar una ordre i revisar el resultat. En un projecte React amb vint dependències de tercers, cadascuna s'actualitza al seu ritme i les incompatibilitats són teves. És un avantatge real de la integració vertical, i és la contrapartida directa del cost 5 de 10-01, la rotació de l'ecosistema.
- Què significa «amb opinions» portat fins al final
A la pràctica, «amb opinions» significa cinc coses concretes:
- No tries eines, tries Angular. No hi ha cap decisió a prendre sobre encaminador, client HTTP o sistema de formularis. Això elimina la fatiga de decisió i elimina també la possibilitat de fer servir alguna cosa millor per al teu cas.
- L'estructura del projecte ve donada.
ng generate component targeta-tascacrea quatre fitxers amb noms, ubicació i contingut predictibles. Tots els projectes s'assemblen. - Hi ha una forma correcta de fer cada cosa, documentada, i la resta de la comunitat la segueix.
- La verbositat és deliberada. Angular prefereix explícit i llarg abans que concís i màgic. Veuràs més línies per fer el mateix, i aquestes línies diuen exactament què passa.
- La corba és més costeruda al principi i més plana després. Hi ha més conceptes per aprendre abans de ser productiu (injecció de dependències, decoradors, observables), i menys sorpreses un cop apresos.
El perfil on Angular llueix és reconeixible: aplicacions grans, de llarga vida, amb equips nombrosos i rotació de personal, típicament internes o corporatives. I el perfil on fa nosa també: un prototip, un widget, un web petit, una persona sola amb pressa.
- TypeScript: el mínim per llegir aquesta lliçó
Angular està escrit en TypeScript i la seva documentació, els seus exemples i les seves eines ho donen per fet. Es pot fer servir amb JavaScript, però ningú no ho fa i l'ecosistema no l'acompanya. Així que necessites el mínim per llegir el codi. Això no és un curs de TypeScript: a 11-07 veuràs què és, per què ha guanyat i com aprendre'l de debò. Aquí van quatre idees.
TypeScript és JavaScript amb anotacions de tipus. Tot JavaScript vàlid és TypeScript vàlid. Les anotacions es comproven en compilar i desapareixen del resultat: al navegador s'executa JavaScript normal.
// Anotacions en variables, paràmetres i valor de retorn
let hores: number = 12;
let titol: string = 'Redissenyar la sala polivalent';
let revisor: string | null = null; // unió: una cosa o una altra
function horesObertes(tasques: Tasca[]): number {
return tasques.filter((t) => t.estat !== 'feta')
.reduce((suma, t) => suma + t.horesEstimades, 0);
}Les interfícies descriuen la forma d'un objecte. Són la documentació que el compilador comprova:
// src/app/domini/tasca.ts
export type Prioritat = 'alta' | 'mitjana' | 'baixa'; // tipus literal: només aquests tres valors
export type Estat = 'pendent' | 'en-curs' | 'feta';
export interface Tasca {
id: number;
titol: string;
responsable: string | null; // R8: null, mai ''
prioritat: Prioritat;
estat: Estat;
etiquetes: string[];
horesEstimades: number;
dataLimit: string; // ISO 'yyyy-mm-dd'
revisor: string | null;
}Fixa't en Prioritat. És un tipus d'unió de literals, i fa una cosa que cap comentari no aconsegueix: si escrius prioritat: 'urgent', el compilador ho rebutja abans d'executar res. Les regles R1–R10 que portes tot el curs comprovant a mà es converteixen, en bona part, en coses que el compilador impedeix expressar.
Els genèrics són tipus amb paràmetres. Tasca[] és un array de tasques; Signal<number> és un senyal que conté un número; Observable<Tasca[]> és un flux que emet arrays de tasques. Es llegeixen com «X de Y».
Els modificadors d'accés són la versió de TypeScript de l'encapsulació de 05-03:
class ServeiTauler {
private tasques: Tasca[] = []; // només dins de la classe (comprovat en compilar)
readonly avui = '2026-09-20'; // no es pot reassignar
}Amb un matís que convé saber: el private de TypeScript desapareix en compilar i només protegeix durant el desenvolupament, mentre que els #camps de JavaScript de 05-03 són privats de debò en temps d'execució. Angular fa servir majoritàriament private.
- Decoradors: què són i per què els fa servir Angular
Els decoradors són la sintaxi amb @ que veuràs pertot arreu:
@Component({ … })
export class TargetaTascaComponent { }
@Injectable({ providedIn: 'root' })
export class TasquesService { }Un decorador és una funció que rep la classe i li afegeix metadades. @Component({...}) no canvia el comportament de la classe: li adjunta informació —quina és la seva plantilla, quin selector fa servir, quins altres components importa— que Angular llegeix en compilar i en executar.
La raó que Angular els faci servir és coherent amb la seva filosofia: la configuració viu al costat del que configura, de manera declarativa i llegible. A React aquesta informació està repartida entre el nom del fitxer, les importacions i el JSX; a Angular és en un objecte explícit sobre la classe.
Per llegir aquesta lliçó n'hi ha prou d'entendre'ls com a «metadades declarades sobre una classe». No necessites escriure'n cap.
- La CLI i l'estructura d'un projecte
La CLI d'Angular fa molt més que crear projectes:
npm install -g @angular/cli
ng new nomada-angular # crea el projecte i pregunta per SSR, estils, etc.
cd nomada-angular
ng generate component components/targeta-tasca # o: ng g c components/targeta-tasca
ng generate service dades/tasques
ng generate guard seguretat/autenticat
ng serve # servidor de desenvolupament
ng build # compilació de producció
ng test # proves unitàries
ng update # migracions automàtiques en actualitzarng generate component crea quatre fitxers i els registra on correspongui:
src/app/components/targeta-tasca/ targeta-tasca.component.ts ← lògica targeta-tasca.component.html ← plantilla targeta-tasca.component.css ← estils (amb àmbit per defecte) targeta-tasca.component.spec.ts ← esquelet de proves
Aquest quart fitxer diu molt sobre les opinions d'Angular: la prova es genera amb el component, no s'afegeix després si a algú li ve de gust. És la mateixa idea que 08-03 defensava, convertida en comportament per defecte de l'eina.
I l'estructura general:
src/
main.ts ← arrencada
index.html
styles.css ← estils globals
app/
app.config.ts ← configuració de l'aplicació (proveïdors)
app.routes.ts ← rutes
app.component.ts ← component arrel
domini/ ← tipus i regles: TypeScript pur
dades/ ← serveis de dades
components/ ← componentsCompara-ho amb el teu js/model/, js/dades/, js/vista/, js/util/. La separació és la mateixa que vas decidir tu; la diferència és que aquí ve donada i tothom la fa servir igual.
- Components independents (
standalone)
standalone)Durant anys, Angular ho organitzava tot en NgModule: un component no existia fins que es declarava en un mòdul, i els mòduls importaven altres mòduls. Era una capa d'indirecció que costava d'explicar i que sovint sobrava.
Avui la forma recomanada són els components independents, que declaren directament què necessiten:
// src/app/components/filtre-responsable.component.ts
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-filtre-responsable',
template: `
<label class="filtre" for="filtre-responsable">
Responsable:
<select
id="filtre-responsable"
[value]="valor() ?? ''"
(change)="enCanviar($event)"
>
<option value="">Tots</option>
@for (nom of responsables(); track nom) {
<option [value]="nom">{{ nom }}</option>
}
</select>
</label>
`,
styles: `
.filtre { display: flex; gap: 0.5rem; align-items: center; }
`
})
export class FiltreResponsableComponent {
responsables = input.required<string[]>();
valor = input<string | null>(null);
canviat = output<string | null>();
enCanviar(esdeveniment: Event): void {
const seleccio = (esdeveniment.target as HTMLSelectElement).value;
this.canviat.emit(seleccio || null); // R8: null, mai ''
}
}Els elements del decorador:
selectorés l'etiqueta HTML amb què es fa servir:<app-filtre-responsable>. El prefixapp-és la convenció per evitar col·lisions amb elements natius.template(otemplateUrl) és la plantilla.styles(ostyleUrls) són els estils, amb àmbit per defecte, com elscopedde Vue de 10-04 però sense haver de demanar-ho.imports—que aquí no cal perquè no es fa servir cap component ni directiva externa— llista el que la plantilla necessita.
Els components independents són avui el valor per defecte de la CLI. Veuràs molt codi antic amb NgModule; continua funcionant i hi ha migracions automàtiques, però per a codi nou no és la forma recomanada.
- Plantilla i estils: les tres formes d'escriure'ls
| Forma | Quan | Com |
|---|---|---|
| Fitxers separats | Components grans | templateUrl: './x.component.html', styleUrls: ['./x.component.css'] |
| En línia amb accents greus | Components petits | template: \…`` |
| Barreja | Plantilla fora, estils dins | Totes dues coses |
Aquesta és una diferència real amb Vue, i va en les dues direccions. Angular per defecte fa servir tres fitxers per a un component (lògica, plantilla, estils), cosa que allunya les parts; però l'àmbit d'estils ve activat sense demanar-ho i les eines de l'editor naveguen entre els tres sense fricció. Per a components petits, la forma en línia deixa una cosa molt semblant a un fitxer .vue.
L'àmbit d'estils s'implementa amb atributs únics afegits pel compilador, igual que a Vue. Es pot canviar a Shadow DOM real amb encapsulation: ViewEncapsulation.ShadowDom, o desactivar-lo — cosa que gairebé mai no convé.
- Enllaç de dades en les seves quatre formes
Angular té una sintaxi molt explícita per distingir la direcció de la dada, i un cop apresa es llegeix molt bé:
<!-- 1 · Interpolació: valor → text -->
<h3>{{ tasca.titol }}</h3>
<p>{{ tasca.horesEstimades }} h</p>
<!-- 2 · Propietat: valor → atribut/propietat de l'element -->
<button [disabled]="seguent() === null">Començar</button>
<li [attr.data-id]="tasca.id" [class.tasca--feta]="tasca.estat === 'feta'">
<!-- 3 · Esdeveniment: element → mètode del component -->
<button (click)="avancar(tasca.id)">Començar</button>
<form (submit)="crear($event)">
<!-- 4 · Doble: propietat + esdeveniment alhora -->
<input [(ngModel)]="titol">La mnemotècnia oficial és útil: els claudàtors apunten cap endins (la dada entra a l'element), els parèntesis apunten cap enfora (l'esdeveniment surt de l'element), i [()] —«caixa de plàtan dins d'una caixa»— fa les dues coses.
I [(ngModel)] és, com el v-model de Vue, pur sucre:
<input [(ngModel)]="titol">
<!-- equival a -->
<input [ngModel]="titol" (ngModelChange)="titol = $event">Una propietat que baixa i un esdeveniment que puja. Un altre cop el flux unidireccional amb una drecera al damunt. Avís pràctic: ngModel pertany a FormsModule i cal importar-lo al component; i per a formularis de certa entitat, Angular recomana els formularis reactius de l'apartat 23, no ngModel.
Per a classes i estils hi ha formes específiques:
<li
class="tasca"
[class.tasca--feta]="tasca.estat === 'feta'"
[class.tasca--vencuda]="vencuda()"
[ngClass]="'tasca--' + tasca.prioritat"
[style.opacity]="tasca.estat === 'feta' ? 0.6 : 1"
>
- La sintaxi de control de flux actual
Durant anys Angular feia servir directives estructurals amb asterisc (*ngIf, *ngFor, ngSwitch). Avui hi ha una sintaxi de blocs integrada al compilador, que és la recomanada per a codi nou:
@if (carregant()) {
<p role="status">Carregant tasques…</p>
} @else if (error()) {
<p role="alert">No s'han pogut carregar: {{ error()!.message }}</p>
} @else {
<ul class="llista-tasques">
@for (tasca of visibles(); track tasca.id) {
<app-targeta-tasca [tasca]="tasca" (avancar)="avancar($event)" />
} @empty {
<p class="llista-buida">Cap tasca no coincideix amb el filtre.</p>
}
</ul>
}
@switch (tasca.estat) {
@case ('pendent') { <span class="marca">○</span> }
@case ('en-curs') { <span class="marca">◐</span> }
@case ('feta') { <span class="marca">●</span> }
}Quatre avantatges sobre les directives antigues, tots pràctics:
- No cal importar res.
*ngIfrequeria importarCommonModuleoNgIf; oblidar-ho produïa una fallada silenciosa en què el contingut simplement no apareixia. - Hi ha
@elsede debò. Amb*ngIfcalia fer servir<ng-template>i referències, cosa considerablement més incòmoda. @emptycobreix l'estat buit sense un@ifaddicional. És justament el cas que 06-06 et va obligar a tractar a part.- El compilador l'entén millor i genera codi més eficient, a més de millors missatges d'error.
Compara les tres sintaxis per al mateix:
| Operació | Angular | Vue | React |
|---|---|---|---|
| Condicional | @if (x) { … } @else { … } |
v-if / v-else |
{x ? … : …} |
| Llista | @for (t of ts; track t.id) { … } |
v-for + :key |
ts.map(t => <X key={t.id}/>) |
| Estat buit | @empty { … } |
v-if="ts.length === 0" |
if (ts.length === 0) return … |
| Selecció múltiple | @switch / @case |
v-if encadenats |
Objecte de cerca, o ternaris |
| Càrrega diferida | @defer |
defineAsyncComponent |
lazy + Suspense |
@for i el seu track obligatori
@for i el seu track obligatoriPer quarta vegada al mòdul, el mateix concepte. I a Angular és on està millor resolt, perquè track no és opcional:
Si escrius @for sense track, el codi no compila. No és un avís a la consola com a React ni una regla d'ESLint com a Vue: és un error de compilació que impedeix construir el projecte.
Aquesta decisió resumeix la filosofia d'Angular. La identitat estable a les llistes és tan important —ho saps des de 06-06, quan reconciliar sense data-id t'hauria costat el focus, les transicions i l'estat intern— que el framework no et deixa oblidar-la. És «amb opinions» en la seva forma més útil: l'opinió t'obliga a fer el correcte.
Les mateixes tres regles, amb el mateix parany: track $index està permès i és el correcte només si la llista no es reordena ni es filtra mai. Amb un id disponible, es fa servir l'id.
Angular ofereix a més variables contextuals dins del bloc:
@for (tasca of visibles(); track tasca.id; let i = $index, primera = $first) {
<li [class.destacada]="primera">{{ i + 1 }}. {{ tasca.titol }}</li>
}Disponibles: $index, $first, $last, $even, $odd, $count.
@defer: càrrega diferida declarativa
@defer: càrrega diferida declarativaAquest bloc mereix atenció pròpia perquè connecta directament amb 09-05 i perquè no té equivalent tan integrat als altres frameworks:
@defer (on viewport) {
<app-informe-carrega [tasques]="tasques()" />
} @placeholder (minimum 500ms) {
<div class="esquelet" style="min-height: 300px" aria-hidden="true"></div>
} @loading (after 100ms; minimum 300ms) {
<p role="status">Carregant l'informe…</p>
} @error {
<p role="alert">No s'ha pogut carregar l'informe.</p>
}El que fa: el component app-informe-carrega i totes les seves dependències se separen en un tros a part durant la compilació, i només es descarreguen quan es compleix la condició. Els disparadors disponibles són on viewport, on interaction, on hover, on idle, on timer(…), on immediate, i when <expressió>, a més de prefetch on … per precarregar sense renderitzar.
Compara-ho amb el que vas escriure a 09-05:
// La teva versió, a mà
let promesaInforme = null;
boto.addEventListener('click', async () => {
indicador.hidden = false;
indicador.setAttribute('aria-busy', 'true');
try {
promesaInforme ??= import('./informe/informe.js');
const { muntarInforme } = await promesaInforme;
muntarInforme(contenidor, tauler);
} catch (error) {
promesaInforme = null; // permetre el reintent
mostrarFallada(contenidor);
} finally {
indicador.hidden = true;
}
});I amb l'IntersectionObserver amb rootMargin que també vas escriure. @defer fa tot això —la divisió del codi, el disparador, l'indicador amb el seu retard mínim per evitar espurneigs, el forat reservat que evita el CLS, i la gestió de la fallada— de manera declarativa i comprovada pel compilador. És exactament l'argument de 10-01: no és que no ho sabessis fer; és que aquí ho escrius en vuit línies de plantilla i no pots oblidar-te de l'estat d'error.
- Senyals:
signal, computed i effect
signal, computed i effectLes senyals són el model de reactivitat actual d'Angular, i la seva semblança amb els ref de Vue no és casual: són la mateixa família 2 de 10-01.
import { signal, computed, effect } from '@angular/core';
// Un senyal escrivible
const responsable = signal<string | null>(null);
// Llegir: es crida com una funció
console.log(responsable()); // null
// Escriure: dues formes
responsable.set('Iván'); // valor nou directe
responsable.update((actual) => actual ?? 'Iván'); // en funció de l'actualLa diferència sintàctica amb Vue: on Vue fa servir .value (una propietat), Angular fa servir () (una crida). El motiu és el mateix de l'apartat 12 de 10-04 —JavaScript no permet interceptar la lectura d'una variable—, resolt amb una altra eina: una funció en lloc d'un getter.
computed declara un derivat, amb memòria cau i dependències automàtiques:
const tasques = signal<Tasca[]>(BACKLOG);
const visibles = computed(() => {
const r = responsable();
return r === null ? tasques() : tasques().filter((t) => t.responsable === r);
});
const horesObertes = computed(() =>
visibles().filter((t) => t.estat !== 'feta')
.reduce((s, t) => s + t.horesEstimades, 0)
);Idèntic en semàntica al computed de Vue: mandrós, cachejat, componible, amb les dependències descobertes en llegir. I per tant idèntic també a la teva memòria cau per versió de 09-02, amb la invalidació automàtica.
effect executa efectes secundaris quan canvien les seves dependències:
effect(() => {
document.title = `Nómada Tasques (${pendents()})`;
});
// Amb neteja, que és un altre cop el teu destruir()
effect((enNetejar) => {
const canal = new CanalTauler();
canal.subscriure(enRebre);
enNetejar(() => canal.tancar());
});Amb el mateix advertiment d'useEffect i de watch: effect no serveix per derivar valors. Escriure en un senyal des de dins d'un effect per calcular alguna cosa és el mateix antipatró que ja has vist dues vegades, i Angular ho desaconsella explícitament (fins al punt que escriure senyals dins d'un efecte requereix una opció especial).
Hi ha un detall rellevant sobre immutabilitat. Un senyal detecta el canvi comparant amb Object.is, com React i no com Vue: no hi ha cap proxy que intercepti l'escriptura en una propietat imbricada.
// ❌ No detecta res: la referència de l'array no canvia
tasques()[5].estat = 'feta';
// ✅ Referència nova
tasques.update((ts) => ts.map((t) => (t.id === 6 ? { ...t, estat: 'feta' } : t)));És a dir: a Angular amb senyals, la immutabilitat de 04-07 torna a ser obligatòria, igual que a React. És un bon exemple que les tres famílies de 10-01 es creuen: Angular té reactivitat de gra fi amb requisit d'immutabilitat; Vue té gra fi amb mutació permesa; React té DOM virtual amb immutabilitat obligatòria.
- Entrades i sortides basades en senyals
La comunicació entre components fa servir input() i output(), que substitueixen els decoradors @Input() i @Output() de l'Angular anterior:
import { Component, input, output, computed } from '@angular/core';
import { Tasca, Estat } from '../domini/tasca';
import { SEGUENT, ETIQUETA, estaVencuda } from '../domini/regles';
@Component({
selector: 'app-targeta-tasca',
template: `
<li
class="tasca"
[class]="'tasca--' + tasca().prioritat"
[class.tasca--feta]="tasca().estat === 'feta'"
[class.tasca--vencuda]="vencuda()"
[attr.data-id]="tasca().id"
>
<h3 class="tasca__titol">{{ tasca().titol }}</h3>
<p class="tasca__meta">
{{ tasca().responsable ?? 'sense assignar' }} · {{ tasca().horesEstimades }} h
@if (vencuda()) { <span class="tasca__avis"> · ⚠ vençuda</span> }
</p>
<ul class="tasca__etiquetes">
@for (etiqueta of tasca().etiquetes; track etiqueta) {
<li class="etiqueta">{{ etiqueta }}</li>
}
</ul>
<button
type="button"
[disabled]="seguent() === null"
[attr.aria-label]="ETIQUETA[tasca().estat] + ': ' + tasca().titol"
(click)="avancar.emit(tasca().id)"
>
{{ ETIQUETA[tasca().estat] }}
</button>
</li>
`,
styles: `
.tasca { border-left: 4px solid var(--vora); padding: 0.75rem; }
.tasca--alta { border-left-color: var(--vermell); }
.tasca--feta .tasca__titol { text-decoration: line-through; opacity: 0.6; }
.tasca__etiquetes { list-style: none; display: flex; gap: 0.3rem; padding: 0; }
`
})
export class TargetaTascaComponent {
tasca = input.required<Tasca>();
avancar = output<number>();
protected readonly ETIQUETA = ETIQUETA;
vencuda = computed(() => estaVencuda(this.tasca()));
seguent = computed<Estat | null>(() => SEGUENT[this.tasca().estat]);
}Punts importants:
input.required<Tasca>()declara una entrada obligatòria i tipada. Si el pare no la passa, falla en compilar. Compara-ho amb les props de React, on només TypeScript ho detecta, o ambdefinePropsde Vue, que valida en temps d'execució en desenvolupament.- Una entrada és un senyal. Es llegeix amb
tasca()i es pot fer servir directament com a dependència d'uncomputed. Això significa quevencudaes recalcula sola quan el pare passa una altra tasca, sense res semblant a l'array de dependències. output<number>()declara un esdeveniment tipat. El pare l'escolta amb(avancar)="…".ETIQUETAs'exposa com a propietat perquè les plantilles d'Angular només veuen membres de la classe, no les importacions del mòdul. És una diferència real amb JSX (on tot l'àmbit del fitxer està disponible) i amb Vue (on<script setup>ho exposa tot automàticament): a Angular cal fer-ho explícit.
- Zone.js, la detecció de canvis i la tendència zoneless
Per entendre l'Angular que veuràs en projectes reals cal saber d'on ve la seva reactivitat.
Durant gairebé tota la seva història, Angular no sabia què havia canviat: ho comprovava tot. El mecanisme era Zone.js, una llibreria que apedaça les API asíncrones del navegador —setTimeout, addEventListener, fetch, Promise— per avisar Angular cada vegada que passa alguna cosa. En rebre l'avís, Angular recorre l'arbre de components avaluant totes les expressions de totes les plantilles i comparant els resultats amb els anteriors.
graph TD E["Qualsevol esdeveniment asíncron<br/>(clic, timeout, resposta HTTP)"] --> Z["Zone.js el detecta"] Z --> D["Angular executa la detecció de canvis"] D --> T["Recorre TOTS els components<br/>i avalua TOTES les expressions"] T --> C["Actualitza el DOM on alguna cosa difereixi"]
Funciona, i és la raó que Angular «simplement actualitzi» quan mutes una propietat. Però té tres costos: comprova molt més del necessari; Zone.js pesa i apedaça API globals, cosa que complica la depuració i la interoperabilitat; i no dona informació de gra fi, així que cal optimitzar a mà amb ChangeDetectionStrategy.OnPush.
Les senyals canvien això. Com que un senyal sap exactament quines expressions el van llegir, Angular pot actualitzar només el que depèn del que ha canviat. Aquest és el fonament de la tendència zoneless: aplicacions on tota la reactivitat passa per senyals i Zone.js deixa de caldre, amb la qual cosa desapareix del paquet i la detecció de canvis passa a ser dirigida per dades en lloc de per sondeig.
A la pràctica, per escriure Angular modern la regla és: fes servir senyals per a tot l'estat. Si ho fas, el model mental és el mateix que el de Vue i no necessites pensar en Zone.js. Però en llegir codi existent et trobaràs propietats normals de classe actualitzant-se soles: això és Zone.js treballant, i ara ja saps per què.
- Senyals d'Angular davant dels
ref de Vue
ref de Vueref de Vue |
signal d'Angular |
|
|---|---|---|
| Llegir | x.value (plantilla: x) |
x() (plantilla: x()) |
| Escriure | x.value = 5 |
x.set(5) o x.update(f) |
| Derivar | computed(() => …) |
computed(() => …) |
| Efecte | watchEffect, watch |
effect |
| Detecció de canvi | Proxy: intercepta escriptures en propietats | Object.is sobre el valor |
| Objectes imbricats | Reactius en profunditat | No: cal reemplaçar la referència |
| Immutabilitat | No cal | Obligatòria |
| Es perd en desestructurar | Sí (toRefs ho evita) |
No: un senyal és una funció, es passa sencer |
| Fora de components | Sí | Sí |
Dues files mereixen comentari.
La de la immutabilitat és la diferència pràctica més visible, i va acompanyada d'una compensació: com que no hi ha proxy, les senyals d'Angular són més barates i més previsibles; no cal preguntar-se si un objecte està embolcallat ni si un Map és reactiu. A canvi, escrius el map immutable de 04-07 a cada actualització.
La de desestructurar és un avantatge clar de les senyals: com que el valor s'obté cridant una funció, passar el senyal a un altre lloc no trenca mai res. Tot l'apartat 15 de 10-04 —els quatre límits de la reactivitat de Vue— senzillament no s'hi aplica. És un cas on la sintaxi més incòmoda (() a cada lectura) compra una propietat valuosa.
- Injecció de dependències: serveis i
inject()
inject()Aquí hi ha el que de debò diferencia Angular. Els altres dos frameworks no tenen res equivalent.
Un servei és una classe amb lògica que no pertany a cap component: accés a dades, estat compartit, regles de negoci, registre d'esdeveniments.
// src/app/dades/tasques.service.ts
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Tasca } from '../domini/tasca';
import { SEGUENT } from '../domini/regles';
@Injectable({ providedIn: 'root' }) // ← una única instància per a tota l'aplicació
export class TasquesService {
private readonly http = inject(HttpClient); // ← injecció per funció
private readonly _tasques = signal<Tasca[]>([]);
readonly tasques = this._tasques.asReadonly(); // exposada com a només lectura
readonly responsables = computed(() =>
[...new Set(this.tasques().map((t) => t.responsable).filter(Boolean))].sort()
);
readonly horesObertes = computed(() =>
this.tasques().filter((t) => t.estat !== 'feta')
.reduce((s, t) => s + t.horesEstimades, 0)
);
carregar(): void {
this.http.get<Tasca[]>('/api/tasques')
.subscribe((tasques) => this._tasques.set(tasques));
}
avancar(id: number): void {
this._tasques.update((tasques) =>
tasques.map((t) => {
if (t.id !== id) return t;
const desti = SEGUENT[t.estat];
return desti === null ? t : { ...t, estat: desti }; // R6
})
);
}
}I el seu ús des d'un component:
@Component({ … })
export class TaulerComponent {
private readonly servei = inject(TasquesService);
tasques = this.servei.tasques;
horesObertes = this.servei.horesObertes;
}Tres coses que cal entendre d'aquest codi:
@Injectable({ providedIn: 'root' }) registra el servei a l'injector arrel. Angular en crea una sola instància i la dona a tothom que la demani. És un singleton, però gestionat pel framework en lloc de per un mòdul exportant un objecte.
inject(TasquesService) demana la instància. No hi ha new, no hi ha importació d'una instància concreta: es demana el tipus i l'injector decideix què donar. Aquesta indirecció és la clau de tot el que ve després.
L'estat és al servei, no al component. El patró _tasques privat escrivible més tasques públic de només lectura (asReadonly()) és l'encapsulació de 05-03 aplicada a l'estat: el servei és l'únic que el pot canviar, i els components només llegeixen. És exactament la disciplina que la teva classe Tauler imposava amb #tasques i el getter que en retornava una còpia.
- Per què la injecció de dependències diferencia Angular
La pregunta raonable és: què aporta demanar inject(TasquesService) en lloc d'importar una instància?
Aporta que qui decideix quina instància es lliura no és qui la fa servir. I d'aquí en surten quatre capacitats:
1 · Substituir en proves. En provar el component, se li dona un servei fals sense tocar el component:
TestBed.configureTestingModule({
providers: [{ provide: TasquesService, useClass: TasquesServiceFals }]
});2 · Canviar la implementació per entorn. El mateix component fa servir un servei contra l'API real en producció i contra dades locals en desenvolupament, decidit a la configuració.
3 · Àmbits diferents. Un servei pot ser únic per a tota l'aplicació (providedIn: 'root'), o crear-se una instància per component si es declara als seus providers. Això permet, per exemple, un servei d'estat per a cada plafó obert.
4 · Interceptors. HttpClient es pot embolcallar amb interceptors que afegeixen capçaleres d'autenticació, reintenten (el teu ambReintents de 07-03), registren o gestionen errors, sense que cap servei ni component se n'assabenti.
El cost, dit clar: és un concepte més per aprendre, i un que no existeix a React ni a Vue. Per a una aplicació petita és cerimònia; per a una de gran amb moltes capes i proves exhaustives, és la peça que manté el codi desacoblat.
- La semblança amb el que vas fer a 08-04
I aquí hi ha la connexió que fa que tot això et resulti familiar. A 08-04 vas escriure això per poder provar el teu repositori:
// js/dades/repositori-local.js — 08-04
export class RepositoriLocal {
#magatzem;
constructor(magatzem = window.localStorage) { // ← injecció per paràmetre
this.#magatzem = magatzem;
}
desar(tauler) {
this.#magatzem.setItem('nomada:tauler', JSON.stringify(tauler));
}
}// test/repositori-local.test.js
const magatzemFals = new MagatzemEnMemoria();
const repo = new RepositoriLocal(magatzemFals); // ← s'injecta el dobleAixò és injecció de dependències. No la va inventar Angular: és un patró de disseny amb dècades, i tu el vas aplicar per la raó correcta —per poder provar sense dependre de localStorage—. El que fa Angular són dues coses més:
- Convertir-la en la norma, no en una cosa que es fa quan algú se'n recorda.
- Automatitzar el cablejat: a la teva versió, qui construeix
RepositoriLocalha de saber quin magatzem passar-li, i aquest coneixement es propaga cap amunt. Amb un injector, es declara una vegada quina implementació correspon a cada tipus i el framework ho resol en tot l'arbre.
Si el patró et va semblar útil a 08-04 —i ho era, perquè sense ell aquelles proves haurien necessitat un navegador real—, Angular te'l dona a tota l'aplicació sense esforç addicional.
HttpClient i RxJS: el just
HttpClient i RxJS: el justHttpClient és el client HTTP inclòs, i retorna observables en lloc de promeses:
import { inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { catchError, retry, map, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ApiTasquesService {
private readonly http = inject(HttpClient);
llistarTasques(responsable: string | null) {
let params = new HttpParams();
if (responsable) params = params.set('responsable', responsable);
return this.http.get<Tasca[]>('/api/tasques', { params }).pipe(
retry({ count: 3, delay: 500 }), // el teu ambReintents de 07-03
map((tasques) => tasques.filter((t) => t.titol.trim() !== '')), // R2
catchError((error) => {
console.error('Error en llistar tasques', error);
return of([]); // degradació digna
})
);
}
}El mínim de RxJS per llegir això, sense més:
Un observable és un flux de valors en el temps. Una promesa produeix un valor (o una fallada) i acaba; un observable en pot produir zero, un o molts, i pot no acabar mai. Un clic de ratolí és un flux. Un WebSocket —el teu CanalTauler de 07-04— és un flux. Una petició HTTP és un flux que emet un valor i acaba, i per això sembla una promesa amb sintaxi estranya.
subscribe engega el flux. Un observable és mandrós: fins que algú no s'hi subscriu, no passa res. Aquest és el parany número u per a qui ve de promeses: cridar llistarTasques() sense subscribe no fa cap petició. Una promesa, en canvi, comença a executar-se tan bon punt es crea.
this.api.llistarTasques(null).subscribe({
next: (tasques) => this.tasques.set(tasques),
error: (fallada) => this.error.set(fallada),
complete: () => this.carregant.set(false)
});pipe encadena operadors. Un operador transforma el flux i retorna un altre flux. Els que apareixen constantment:
| Operador | Què fa | El teu equivalent |
|---|---|---|
map |
Transforma cada valor | Array.prototype.map de 04-04 |
filter |
Descarta valors | Array.prototype.filter |
retry |
Reintenta en fallar | El teu ambReintents de 07-03 |
catchError |
Captura l'error i retorna un altre flux | El catch de 02-05 |
debounceTime |
Espera que pari d'emetre | El teu debounce de 09-02 |
switchMap |
Canvia a un altre flux i cancel·la l'anterior | El teu AbortController de 07-03 |
takeUntilDestroyed |
Es dona de baixa en destruir el component | El teu destruir() de 09-03 |
switchMap mereix un exemple, perquè resol elegantment la condició de cursa de 10-02:
// El filtre és un flux; cada canvi cancel·la la petició anterior
readonly tasques = toSignal(
toObservable(this.responsable).pipe(
debounceTime(300), // no consultar a cada tecla
switchMap((r) => this.api.llistarTasques(r)) // cancel·la la petició anterior
),
{ initialValue: [] as Tasca[] }
);Aquest switchMap és la resposta canònica al problema que a React vas resoldre avortant a mà i a Vue amb enNetejar. Quan arriba un valor nou, es cancel·la la subscripció anterior —i amb ella la petició HTTP— i se subscriu a la nova. La cursa és impossible per construcció.
I toSignal/toObservable són els ponts entre els dos mons: converteixen un observable en senyal i a l'inrevés. Són l'eina que permet escriure l'aplicació amb senyals i fer servir RxJS només on aporta.
- L'
async de la plantilla i per què cal donar-se de baixa
async de la plantilla i per què cal donar-se de baixaA la plantilla, el pipe async se subscriu i es dona de baixa automàticament:
@if (tasques$ | async; as tasques) {
<ul>
@for (t of tasques; track t.id) { <li>{{ t.titol }}</li> }
</ul>
}És la forma recomanada quan es treballa amb observables en plantilles, precisament perquè elimina el problema de donar-se de baixa.
I aquest problema és real. Un observable que no acaba —un WebSocket, un flux d'esdeveniments, un temporitzador— manté viva la seva subscripció encara que el component desaparegui. És literalment la fuita de memòria de 09-03: el gestor continua en memòria, continua processant, i manté viu el component i tot el seu subarbre.
// ❌ Fuita: la subscripció sobreviu al component
export class TaulerComponent implements OnInit {
ngOnInit() {
this.canal.missatges$.subscribe((m) => this.aplicar(m));
}
}
// ✅ Baixa automàtica en destruir el component
export class TaulerComponent {
private readonly destruirRef = inject(DestroyRef);
constructor() {
this.canal.missatges$
.pipe(takeUntilDestroyed(this.destruirRef))
.subscribe((m) => this.aplicar(m));
}
}takeUntilDestroyed és el teu destruir(), un cop més, amb la crida garantida pel framework. Les tres formes correctes de gestionar la baixa, per ordre de preferència: el pipe async a la plantilla, takeUntilDestroyed, i —si no hi ha més remei— guardar la subscripció i cridar unsubscribe() a ngOnDestroy.
- Quan senyals i quan observables
Angular té ara dos sistemes reactius convivint, cosa que desconcerta. El criteri actual és raonablement clar:
| Fes servir… | Per a… | Exemples |
|---|---|---|
| Senyals | Estat síncron que la interfície mostra | Filtre actual, llista de tasques, si un plafó està obert |
| Observables | Fluxos asíncrons amb composició complexa | HTTP, WebSocket, esdeveniments del DOM amb retard, seqüències amb cancel·lació |
| Els ponts | Passar d'un món a l'altre | toSignal per pintar un flux; toObservable per compondre sobre un senyal |
A la pràctica, l'Angular modern tendeix a: senyals per a gairebé tot l'estat de l'aplicació, RxJS per a la capa de dades i les interaccions complexes, i toSignal a la frontera. És un model amb més peces que Vue o React, i aquesta és una crítica legítima a la corba d'aprenentatge d'Angular — amb la contrapartida que RxJS resol problemes de coordinació asíncrona que als altres frameworks es resolen a mà o amb llibreries.
- L'encaminador amb càrrega diferida
// src/app/app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'tauler', pathMatch: 'full' },
{
path: 'tauler',
// Càrrega diferida: aquest component va al seu propi tros
loadComponent: () => import('./components/tauler.component')
.then((m) => m.TaulerComponent),
title: 'Tauler · Nómada Tasques'
},
{
path: 'tasca/:id',
loadComponent: () => import('./components/detall-tasca.component')
.then((m) => m.DetallTascaComponent)
},
{
path: 'informe',
loadComponent: () => import('./components/informe.component')
.then((m) => m.InformeComponent),
canActivate: [autenticatGuard] // guarda de ruta
},
{ path: '**', loadComponent: () => import('./components/no-trobat.component')
.then((m) => m.NoTrobatComponent) }
];Aquest loadComponent amb import() dinàmic és exactament la divisió de codi de 09-05, integrada a l'encaminador: cada ruta es compila al seu propi tros i només es descarrega en navegar-hi. És la fila «Angular porta divisió de codi a l'encaminador» que 09-05 anunciava.
I hi ha una peça que al teu encaminador.js no tenies: les guardes. canActivate executa una funció abans d'activar la ruta i pot impedir la navegació o redirigir:
export const autenticatGuard: CanActivateFn = () => {
const sessio = inject(SessioService);
const router = inject(Router);
return sessio.autenticat() ? true : router.createUrlTree(['/entrar']);
};A la plantilla, la navegació es fa amb directives:
<nav>
<a routerLink="/tauler" routerLinkActive="actiu">Tauler</a>
<a [routerLink]="['/tasca', tasca.id]">{{ tasca.titol }}</a>
</nav>
<router-outlet />routerLink genera un <a href> real —important per a l'accessibilitat i el SEO— i intercepta el clic per navegar sense recarregar, que és exactament el que feia el teu encaminador amb la History API de 07-06.
- Formularis reactius davant de formularis de plantilla
Angular inclou dos sistemes, i aquesta és la comparació que decideix quin fer servir:
De plantilla (ngModel) |
Reactius (FormGroup) |
|
|---|---|---|
| On es defineix l'estructura | A l'HTML | A TypeScript |
| Tipatge | Feble | Fort, comprovat en compilar |
| Validació | Atributs a la plantilla | Funcions validadores |
| Validació asíncrona | Incòmoda | Integrada |
| Camps dinàmics | Difícil | FormArray |
| Proves | Requereix renderitzar | Es prova el model directament |
| Formularis senzills | Ràpid d'escriure | Més cerimònia |
| Recomanació d'Angular | Casos simples | Tota la resta |
Els reactius en acció:
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-formulari-tasca',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="formulari" (ngSubmit)="enviar()" novalidate>
<label for="titol">Títol</label>
<input id="titol" formControlName="titol"
[attr.aria-invalid]="titol.invalid && titol.touched">
@if (titol.touched && titol.hasError('required')) {
<p class="error" role="alert">El títol és obligatori (R2).</p>
}
@if (titol.touched && titol.hasError('nomesEspais')) {
<p class="error" role="alert">El títol no pot ser només espais (R2).</p>
}
<label for="hores">Hores estimades</label>
<input id="hores" type="number" formControlName="horesEstimades">
@if (hores.touched && hores.invalid) {
<p class="error" role="alert">Entre 1 i 40 hores (R3).</p>
}
<button type="submit" [disabled]="formulari.invalid">Crear tasca</button>
</form>
`
})
export class FormulariTascaComponent {
private readonly fb = inject(FormBuilder);
formulari = this.fb.nonNullable.group({
titol: ['', [Validators.required, senseNomesEspais]],
horesEstimades: [1, [Validators.required, Validators.min(1), Validators.max(40)]],
dataLimit: ['', [Validators.required, noAnteriorAAvui]],
responsable: [null as string | null]
});
get titol() { return this.formulari.controls.titol; }
get hores() { return this.formulari.controls.horesEstimades; }
enviar(): void {
if (this.formulari.invalid) {
this.formulari.markAllAsTouched(); // mostra tots els errors d'una tirada
return;
}
const dades = this.formulari.getRawValue(); // tipat: {titol: string, …}
// …crear la tasca
}
}
- Validadors amb les regles R1–R10
Un validador és una funció pura que rep el control i retorna null si és vàlid o un objecte d'errors si no. És a dir: exactament la forma de les teves funcions de validació de 03-03, amb una altra signatura.
// src/app/domini/validadors.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { AVUI } from './regles';
/** R2: el títol no pot estar buit ni contenir només espais. */
export function senseNomesEspais(control: AbstractControl): ValidationErrors | null {
const valor = control.value as string;
return typeof valor === 'string' && valor.trim() === '' ? { nomesEspais: true } : null;
}
/** R4: la data límit no pot ser anterior a la de creació. */
export function noAnteriorAAvui(control: AbstractControl): ValidationErrors | null {
const valor = control.value as string;
if (!valor) return null;
return valor < AVUI ? { dataPassada: { minim: AVUI, actual: valor } } : null;
}
/** R9: les etiquetes es guarden en minúscules i sense duplicats. */
export function etiquetesNormalitzades(control: AbstractControl): ValidationErrors | null {
const valor = (control.value ?? []) as string[];
const normalitzades = valor.map((e) => e.trim().toLowerCase());
const hiHaDuplicats = new Set(normalitzades).size !== normalitzades.length;
const hiHaMajuscules = valor.some((e) => e !== e.toLowerCase());
if (hiHaDuplicats) return { etiquetesDuplicades: true };
if (hiHaMajuscules) return { etiquetesEnMajuscules: true };
return null;
}
/** R7: ningú no pot superar 40 h assignades a la mateixa setmana. Validador de grup. */
export function limitSetmanal(carregaActual: Map<string, number>): ValidatorFn {
return (grup: AbstractControl): ValidationErrors | null => {
const responsable = grup.get('responsable')?.value as string | null;
const hores = Number(grup.get('horesEstimades')?.value ?? 0);
if (!responsable) return null; // R8: sense responsable, no s'aplica
const total = (carregaActual.get(responsable) ?? 0) + hores;
return total > 40 ? { limitSetmanal: { responsable, total, maxim: 40 } } : null;
};
}Tres observacions:
- Són funcions pures i es proven amb Jest sense Angular.
senseNomesEspais({value: ' '} as AbstractControl)retorna{nomesEspais: true}. És la mateixa facilitat de prova que tenien els teus validadors de 03-03 i els teus reductors de 10-03. limitSetmanalés una fàbrica de validadors —una funció que retorna una funció, de 03-06— perquè necessita dades externes. I és un validador de grup, perquè la R7 depèn de dos camps alhora.- L'objecte d'error porta dades, no només
true. Això permet missatges concrets: «Iván arribaria a 45 h de les 40 permeses» en lloc de «error». És la mateixa idea que el teuErrorDeValidacioamb.campdejs/model/errors.js.
- Proves amb TestBed
Angular porta el seu propi entorn, que munta un mini-injector per a la prova:
import { TestBed } from '@angular/core/testing';
import { TaulerComponent } from './tauler.component';
import { TasquesService } from '../dades/tasques.service';
import { signal } from '@angular/core';
import { BACKLOG } from '../dades/backlog';
describe('TaulerComponent', () => {
let serveiFals: Partial<TasquesService>;
beforeEach(async () => {
serveiFals = {
tasques: signal(BACKLOG).asReadonly(),
avancar: jasmine.createSpy('avancar')
};
await TestBed.configureTestingModule({
imports: [TaulerComponent],
providers: [{ provide: TasquesService, useValue: serveiFals }] // ← el doble
}).compileComponents();
});
it('mostra les 6 tasques del backlog canònic', () => {
const fixture = TestBed.createComponent(TaulerComponent);
fixture.detectChanges();
const files = fixture.nativeElement.querySelectorAll('[data-id]');
expect(files.length).toBe(6);
});
it('en filtrar per Lucía queda una tasca de 14 h', () => {
const fixture = TestBed.createComponent(TaulerComponent);
fixture.componentInstance.responsable.set('Lucía');
fixture.detectChanges();
const files = fixture.nativeElement.querySelectorAll('[data-id]');
expect(files.length).toBe(1);
expect(fixture.nativeElement.textContent).toContain('Actualitzar el web de reserves');
});
});La línia que importa és providers: [{ provide: TasquesService, useValue: serveiFals }]. Això és el que compra la injecció de dependències. El component demana TasquesService; en producció rep el real, a la prova rep el doble, i el component no canvia ni se n'assabenta. És exactament el que vas fer a mà a 08-04 passant MagatzemEnMemoria al constructor de RepositoriLocal, amb el cablejat automatitzat.
Angular inclou Karma i Jasmine per defecte, però es pot fer servir Jest o Vitest, i Testing Library té versió per a Angular, amb les mateixes consultes per rol que ja fas servir des de 08-05. La filosofia de provar el que veu la usuària no canvia de framework.
- Una nota sobre SSR
Angular inclou renderitzat al servidor sense llibreries externes:
Això afegeix un servidor que renderitza l'HTML abans d'enviar-lo, amb hidratació al client perquè la pàgina continuï sent interactiva, i suport per a hidratació incremental combinada amb @defer, que permet decidir quines parts s'hidraten i quan.
No ho desenvolupem aquí: el renderitzat al client, al servidor, estàtic i híbrid és el tema de la lliçó següent, on veuràs per què aquesta decisió acostuma a importar més que la del framework.
- Nómada Tasques en Angular: la llista completa
Quarta i última versió de la mateixa pantalla. El domini, en TypeScript però conceptualment idèntic:
// src/app/domini/regles.ts
import { Tasca, Estat } from './tasca';
export const SEGUENT: Record<Estat, Estat | null> = {
pendent: 'en-curs',
'en-curs': 'feta',
feta: null
};
export const ETIQUETA: Record<Estat, string> = {
pendent: 'Començar',
'en-curs': 'Marcar feta',
feta: 'Feta'
};
export const PESOS = { alta: 3, mitjana: 2, baixa: 1 } as const;
export const AVUI = '2026-09-20';
export const estaVencuda = (t: Tasca, avui = AVUI): boolean =>
t.estat !== 'feta' && t.dataLimit < avui;El servei, que és on viu l'estat:
// src/app/dades/tauler.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { Tasca } from '../domini/tasca';
import { SEGUENT } from '../domini/regles';
import { BACKLOG } from './backlog';
@Injectable({ providedIn: 'root' })
export class TaulerService {
private readonly _tasques = signal<Tasca[]>(BACKLOG);
readonly tasques = this._tasques.asReadonly();
readonly responsables = computed(() =>
[...new Set(this.tasques().map((t) => t.responsable).filter((r): r is string => r !== null))]
.sort()
);
readonly resum = computed(() => {
const totes = this.tasques();
const obertes = totes.filter((t) => t.estat !== 'feta');
return {
total: totes.length,
horesTotals: totes.reduce((s, t) => s + t.horesEstimades, 0),
horesObertes: obertes.reduce((s, t) => s + t.horesEstimades, 0)
};
});
/** Avança l'estat d'una tasca respectant la R6. Immutable: el senyal compara per referència. */
avancar(id: number): void {
this._tasques.update((tasques) =>
tasques.map((t) => {
if (t.id !== id) return t;
const desti = SEGUENT[t.estat];
return desti === null ? t : { ...t, estat: desti };
})
);
}
}La targeta és la de l'apartat 13. La llista:
// src/app/components/llista-tasques.component.ts
import { Component, input, output } from '@angular/core';
import { Tasca } from '../domini/tasca';
import { TargetaTascaComponent } from './targeta-tasca.component';
@Component({
selector: 'app-llista-tasques',
imports: [TargetaTascaComponent], // ← el component independent declara el que fa servir
template: `
<ul class="llista-tasques">
@for (tasca of tasques(); track tasca.id) {
<app-targeta-tasca [tasca]="tasca" (avancar)="avancar.emit($event)" />
} @empty {
<p class="llista-buida">Cap tasca no coincideix amb el filtre.</p>
}
</ul>
`,
styles: `
.llista-tasques { list-style: none; padding: 0; display: grid; gap: 0.5rem; }
.llista-buida { color: var(--gris); font-style: italic; }
`
})
export class LlistaTasquesComponent {
tasques = input.required<Tasca[]>();
avancar = output<number>();
}I el component arrel:
// src/app/app.component.ts
import { Component, inject, signal, computed } from '@angular/core';
import { TaulerService } from './dades/tauler.service';
import { FiltreResponsableComponent } from './components/filtre-responsable.component';
import { LlistaTasquesComponent } from './components/llista-tasques.component';
@Component({
selector: 'app-root',
imports: [FiltreResponsableComponent, LlistaTasquesComponent],
template: `
<main class="tauler">
<header class="tauler__capcalera">
<h1>Nómada Tasques</h1>
<app-filtre-responsable
[responsables]="servei.responsables()"
[valor]="responsable()"
(canviat)="responsable.set($event)"
/>
<p class="tauler__resum">
{{ visibles().length }} de {{ servei.resum().total }} tasques ·
{{ horesVisibles() }} h obertes
</p>
</header>
<app-llista-tasques [tasques]="visibles()" (avancar)="servei.avancar($event)" />
</main>
`,
styles: `
.tauler__capcalera { display: flex; gap: 1rem; align-items: baseline; flex-wrap: wrap; }
.tauler__resum { color: var(--gris); margin-left: auto; }
`
})
export class AppComponent {
protected readonly servei = inject(TaulerService);
readonly responsable = signal<string | null>(null);
readonly visibles = computed(() => {
const r = this.responsable();
return r === null ? this.servei.tasques()
: this.servei.tasques().filter((t) => t.responsable === r);
});
readonly horesVisibles = computed(() =>
this.visibles().filter((t) => t.estat !== 'feta')
.reduce((s, t) => s + t.horesEstimades, 0)
);
}Els números canònics, comprovats: sense filtre, «6 de 6 tasques · 45 h obertes». Amb «Iván», «3 de 6 · 25 h». Amb «Lucía», «1 de 6 · 14 h». Amb «Marta», «2 de 6 · 6 h».
- Comparació amb les tres versions anteriors
| Concepte | JavaScript pur | React | Vue | Angular |
|---|---|---|---|---|
| Llenguatge | JavaScript | JS o TS | JS o TS | TypeScript |
| Unitat | Mòdul + <template> + CSS |
Funció .jsx |
Fitxer .vue |
Classe amb decorador |
| Estils aïllats | No | No per defecte | Sí (scoped) |
Sí, per defecte |
| Estat | Objecte + render() |
useState |
ref |
signal |
| Derivats | Memòria cau per versió | useMemo |
computed |
computed |
| Immutabilitat | Recomanada | Obligatòria | No cal | Obligatòria |
| Identitat a les llistes | data-id + reconciliar |
key (avís si falta) |
:key (error d'ESLint) |
track (error de compilació) |
| Neteja | destruir() a mà |
return de l'efecte |
onUnmounted |
DestroyRef, takeUntilDestroyed |
| Dades remotes | demanarJson propi |
Es tria | Es tria | HttpClient inclòs |
| Estat compartit | Objecte de mòdul | Es tria (10-03) | Pinia | Servei injectat |
| Injecció de dependències | A mà (08-04) | No existeix | No existeix | Integrada |
| Divisió de codi | import() a mà |
lazy |
defineAsyncComponent |
@defer i loadComponent |
| Formularis | FormData + validació pròpia |
Es tria | v-model |
Dos sistemes inclosos |
| Proves | Jest + Testing Library | Testing Library | Vue Test Utils | TestBed inclòs |
I les mètriques de la mateixa pantalla, amb les quatre versions:
| Mètrica | JavaScript pur | React | Vue | Angular |
|---|---|---|---|---|
| Línies de la vista | ~210 | ~130 | ~120 | ~180 |
| Línies d'infraestructura pròpia | ~60 | 0 | 0 | 0 |
| Dependències de producció | 0 | 2 | 1 | ~8 (paquets @angular/*) |
| Pes del motor (comprimit, aprox.) | 0 | ~45 kB | ~35 kB | ~60–90 kB segons el que es faci servir |
| Conceptes previs necessaris | DOM, mòduls | JSX, hooks, reconciliació | Plantilles, refs | TS, decoradors, DI, senyals, RxJS |
| Temps fins a ser productiu | — | Setmanes | Dies o setmanes | Setmanes o mesos |
| Previsibilitat entre projectes | Cap | Baixa | Mitjana | Alta |
El que guanya Angular, sense adorns: cohesió total. Tot encaixa perquè ho va fer el mateix equip. El tipatge és de sèrie i atrapa errors abans d'executar. El track obligatori impedeix una fallada real. Els serveis injectats donen estat compartit i capacitat de prova sense instal·lar res. Les migracions automàtiques fan viables les actualitzacions a deu anys vista. I dos projectes diferents s'assemblen, cosa que redueix el cost d'incorporar algú.
El que perd, amb la mateixa franquesa: és el més car d'aprendre, amb diferència. TypeScript, decoradors, injecció de dependències, senyals, RxJS i dos sistemes de formularis són molt abans de la primera pantalla. És el més verbós: 180 línies davant de 120 per al mateix. És el més pesat a l'arrencada. I és el que pitjor encaixa en projectes petits, on tota aquesta infraestructura no té res per sostenir.
Errors Habituals i Consells
Oblidar els parèntesis en llegir un senyal. tasca.titol en lloc de tasca().titol compara o accedeix sobre la funció, no sobre el valor. A la plantilla, {{ tasques }} mostra el codi de la funció. TypeScript detecta molts d'aquests casos, però no tots.
Mutar el valor d'un senyal. tasques()[0].estat = 'feta' no dispara res: les senyals comparen amb Object.is. Cal fer servir update amb un map immutable, com a React.
Oblidar imports en un component independent. Si la plantilla fa servir <app-targeta-tasca> o [formGroup] i no és a imports, l'error de compilació és clar però desconcerta al principi. És el preu que cada component declari el que necessita.
Fer servir propietats de classe en lloc de senyals. Funciona per Zone.js, però és el model antic: perds el gra fi, impedeixes l'estratègia zoneless i barreges dos sistemes de reactivitat. Per a codi nou, senyals.
Cridar un mètode a la plantilla en lloc d'un computed. {{ calcularHores() }} s'executa a cada cicle de detecció de canvis, que amb Zone.js pot ser moltíssimes vegades per segon. computed es cacheja. És la causa clàssica que una aplicació Angular vagi lenta sense motiu aparent.
No subscriure's a un observable. Els observables són mandrosos: sense subscribe no passa res. És el parany número u per a qui ve de promeses, que s'executen en crear-se.
No donar-se de baixa. És la fuita de memòria de 09-03 amb un altre nom. Fes servir el pipe async a la plantilla o takeUntilDestroyed; no deixis mai una subscripció a un flux infinit sense gestionar.
Posar lògica de negoci als components. A Angular és especialment evitable perquè els serveis existeixen precisament per a això, amb injecció i proves incloses. La disciplina de 10-01 s'aplica igual: el domini és TypeScript pur, el component pinta.
Fer servir ngModel per a formularis complexos. Angular recomana els formularis reactius per a tot el que no sigui trivial: tipatge, validació asíncrona, camps dinàmics i proves sense renderitzar.
Consell: aprèn TypeScript abans que Angular. Intentar aprendre els dos alhora multiplica la confusió, perquè no sabràs si un error és del llenguatge o del framework. La lliçó 11-07 és el punt de partida.
Consell: comença amb senyals i deixa RxJS per a la capa de dades. L'Angular modern permet escriure gairebé tota l'aplicació amb senyals i fer servir observables només on aporten —HTTP, WebSocket, seqüències amb cancel·lació—. toSignal és el pont.
Consell: fes servir ng generate sempre. Genera l'estructura correcta, el fitxer de proves i els registres necessaris. Escriure components a mà és una font d'errors tontos que la CLI evita.
Exercicis
Exercici 1 · De React a Angular
Tradueix aquest component de React (de 10-02) a un component Angular independent amb senyals. Mantén el mateix comportament i respecta les regles R6 i R10.
function ResumPerResponsable({ tasques, responsableActiu }) {
const [expandit, setExpandit] = useState(false);
const perPersona = tasques
.filter((t) => t.estat !== 'feta')
.reduce((acc, t) => {
const n = t.responsable ?? 'sense assignar';
const previ = acc[n] ?? { tasques: 0, hores: 0 };
acc[n] = { tasques: previ.tasques + 1, hores: previ.hores + t.horesEstimades };
return acc;
}, {});
const files = Object.entries(perPersona).sort(([a], [b]) => a.localeCompare(b, 'ca'));
const visibles = expandit ? files : files.slice(0, 2);
return (
<section>
<h3>Càrrega per responsable</h3>
<ul>
{visibles.map(([nom, { tasques: n, hores }]) => (
<li key={nom} className={nom === responsableActiu ? 'actiu' : ''}>
{nom}: {n} tasques · {hores} h
</li>
))}
</ul>
{files.length > 2 && (
<button onClick={() => setExpandit(!expandit)}>
{expandit ? 'Veure menys' : `Veure ${files.length - 2} més`}
</button>
)}
</section>
);
}Indica en comentaris què correspon a useState, a les props i al càlcul derivat.
Exercici 2 · Un servei amb HttpClient, senyals i cancel·lació
Escriu un servei ApiTaulerService que:
- Exposi
tasques,carregantierrorcom a senyals de només lectura. - Tingui un mètode
filtrarPer(responsable: string | null)que actualitzi un senyal intern. - Carregui les tasques del servidor cada vegada que canviï el responsable, amb
debounceTime(300)i cancel·lant la petició anterior. - Reintenti tres vegades amb mig segon d'espera davant d'una fallada de xarxa.
- Davant d'un error definitiu, deixi la llista buida i ompli
errorsense trencar el flux.
Fes servir toObservable, switchMap, retry, catchError i toSignal. Explica quin operador resol la condició de cursa de 10-02 i per què.
Exercici 3 · Validadors per a les regles R3, R6 i R10
Escriu tres funcions pures, sense dependències d'Angular a la seva lògica, i les seves proves:
- Un validador
horesEnRangper a la R3 (major que 0, màxim 40) que retorni un error amb dades útils per al missatge. - Una funció
potAvancar(estatActual, desti)que implementi la R6 fent servirSEGUENT, i explica per què no és un validador de formulari. - Un validador de grup
dataCoherentAmbEstatque apliqui la R10 a l'inrevés: si l'estat és'feta', la data límit passada no ha de marcar error; si no és'feta'i la data ja ha passat, ha de retornar un avís (no un error bloquejant).
Després respon: per què convé que aquestes funcions visquin a domini/ i no al component?
Solucions
Solució 1
// src/app/components/resum-per-responsable.component.ts
import { Component, input, signal, computed } from '@angular/core';
import { Tasca } from '../domini/tasca';
interface FilaCarrega {
nom: string;
tasques: number;
hores: number;
}
@Component({
selector: 'app-resum-per-responsable',
template: `
<section class="resum">
<h3>Càrrega per responsable</h3>
<ul>
@for (fila of visibles(); track fila.nom) {
<li [class.actiu]="fila.nom === responsableActiu()">
{{ fila.nom }}: {{ fila.tasques }} {{ fila.tasques === 1 ? 'tasca' : 'tasques' }}
· {{ fila.hores }} h
</li>
}
</ul>
@if (files().length > 2) {
<button type="button" (click)="alternar()">
{{ expandit() ? 'Veure menys' : 'Veure ' + (files().length - 2) + ' més' }}
</button>
}
</section>
`,
styles: `
.actiu { font-weight: 600; }
ul { list-style: none; padding: 0; }
`
})
export class ResumPerResponsableComponent {
// ← props de React: entrades basades en senyals
tasques = input.required<Tasca[]>();
responsableActiu = input<string | null>(null);
// ← useState de React: senyal escrivible local
readonly expandit = signal(false);
// ← càlcul derivat durant el renderitzat: computed cachejat
readonly files = computed<FilaCarrega[]>(() => {
const perPersona = this.tasques()
.filter((t) => t.estat !== 'feta') // només obertes
.reduce<Record<string, { tasques: number; hores: number }>>((acc, t) => {
const nom = t.responsable ?? 'sense assignar'; // R8
const previ = acc[nom] ?? { tasques: 0, hores: 0 };
acc[nom] = { tasques: previ.tasques + 1, hores: previ.hores + t.horesEstimades };
return acc;
}, {});
return Object.entries(perPersona)
.map(([nom, dades]) => ({ nom, ...dades }))
.sort((a, b) => a.nom.localeCompare(b.nom, 'ca'));
});
readonly visibles = computed(() =>
this.expandit() ? this.files() : this.files().slice(0, 2)
);
alternar(): void {
this.expandit.update((v) => !v); // com setExpandit(v => !v)
}
}Diferències notables respecte a l'original:
- Les files es transformen a un array d'objectes amb
noma dins, en lloc de parells[clau, valor]. A la plantilla d'Angular, la desestructuració de@forés més limitada que a JSX, i un objecte es llegeix millor. track fila.només obligatori: sense això, el projecte no compila. A la versió React,keyera un avís que es pot ignorar.- La lògica d'agrupació podria —i probablement hauria de— viure a
domini/carrega.tscom a funció pura, deixant el component només amb la presentació. Amb el backlog canònic retorna: Iván 3 / 25 h, Lucía 1 / 14 h, Marta 1 / 6 h.
Solució 2
// src/app/dades/api-tauler.service.ts
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { switchMap, debounceTime, retry, catchError, tap, of, startWith } from 'rxjs';
import { Tasca } from '../domini/tasca';
@Injectable({ providedIn: 'root' })
export class ApiTaulerService {
private readonly http = inject(HttpClient);
private readonly _responsable = signal<string | null>(null);
private readonly _carregant = signal(false);
private readonly _error = signal<string | null>(null);
readonly carregant = this._carregant.asReadonly();
readonly error = this._error.asReadonly();
readonly tasques = toSignal(
toObservable(this._responsable).pipe(
debounceTime(300),
tap(() => { this._carregant.set(true); this._error.set(null); }),
switchMap((responsable) => {
let params = new HttpParams();
if (responsable) params = params.set('responsable', responsable);
return this.http.get<Tasca[]>('/api/tasques', { params }).pipe(
retry({ count: 3, delay: 500 }), // 07-03: reintents
tap(() => this._carregant.set(false)),
catchError((fallada) => {
this._error.set(fallada.message ?? 'Error de xarxa');
this._carregant.set(false);
return of([] as Tasca[]); // el flux NO mor
})
);
}),
startWith([] as Tasca[])
),
{ initialValue: [] as Tasca[] }
);
readonly horesObertes = computed(() =>
this.tasques().filter((t) => t.estat !== 'feta')
.reduce((s, t) => s + t.horesEstimades, 0)
);
filtrarPer(responsable: string | null): void {
this._responsable.set(responsable);
}
}Quin operador resol la condició de cursa: switchMap. La seva semàntica és «canvia al flux nou i cancel·la la subscripció a l'anterior». Quan la Marta passa d'«Iván» a «Lucía», la petició d'Iván es cancel·la en el moment en què arriba el valor de Lucía. En cancel·lar-se la subscripció, HttpClient avorta la petició HTTP subjacent —fa servir internament el mateix mecanisme que el teu AbortController de 07-03—, així que la seva resposta no arriba mai al tap ni al toSignal. La cursa és impossible per construcció, no per una comprovació que algú hagi escrit.
Compara les tres solucions al mateix problema:
| Framework | Solució | Risc d'oblit |
|---|---|---|
| React | AbortController creat i avortat a mà a l'efecte |
Alt: cal recordar el return |
| Vue | enNetejar(() => controlador.abort()) al watchEffect |
Mitjà: cal recordar la neteja |
| Angular | switchMap |
Cap: és la semàntica de l'operador |
Dos detalls del codi: catchError retorna of([]) en lloc de rellançar, perquè si l'error es propaga, el flux acaba i filtrarPer deixa de funcionar per sempre — és l'error clàssic de RxJS. I retry va dins del switchMap, aplicat a la petició concreta; si fos a fora, reintentaria tot el flux extern, inclòs el debounceTime.
Solució 3
// src/app/domini/validadors.ts
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { Estat, SEGUENT, AVUI } from './regles';
/** R3: horesEstimades > 0 i <= 40. */
export function horesEnRang(control: AbstractControl): ValidationErrors | null {
const valor = Number(control.value);
if (Number.isNaN(valor)) return { horesNoNumeriques: { actual: control.value } };
if (valor <= 0) return { horesForaDeRang: { actual: valor, minim: 1, maxim: 40 } };
if (valor > 40) return { horesForaDeRang: { actual: valor, minim: 1, maxim: 40,
suggeriment: 'Divideix la tasca en diverses' } };
return null;
}
/** R6: només es permeten les transicions declarades a SEGUENT. */
export function potAvancar(actual: Estat, desti: Estat): boolean {
return SEGUENT[actual] === desti;
}
/** R10, en forma d'avís: vençuda = data passada i estat diferent de 'feta'. */
export function dataCoherentAmbEstat(grup: AbstractControl): ValidationErrors | null {
const estat = grup.get('estat')?.value as Estat | undefined;
const data = grup.get('dataLimit')?.value as string | undefined;
if (!estat || !data) return null;
if (estat !== 'feta' && data < AVUI) {
return { tascaVencuda: { dataLimit: data, avui: AVUI, bloquejant: false } };
}
return null; // si està 'feta', una data passada és completament normal
}Les proves, amb Jest i sense Angular:
describe('horesEnRang (R3)', () => {
const control = (value: unknown) => ({ value }) as AbstractControl;
test('accepta valors vàlids', () => {
expect(horesEnRang(control(1))).toBeNull();
expect(horesEnRang(control(12))).toBeNull();
expect(horesEnRang(control(40))).toBeNull();
});
test('rebutja 0 i negatius', () => {
expect(horesEnRang(control(0))?.['horesForaDeRang']).toBeDefined();
expect(horesEnRang(control(-3))?.['horesForaDeRang']).toBeDefined();
});
test('rebutja més de 40 i suggereix dividir', () => {
const error = horesEnRang(control(55));
expect(error?.['horesForaDeRang'].suggeriment).toContain('Divideix');
});
});
describe('potAvancar (R6)', () => {
test('permet les transicions del diagrama', () => {
expect(potAvancar('pendent', 'en-curs')).toBe(true);
expect(potAvancar('en-curs', 'feta')).toBe(true);
});
test('rebutja els salts i els retrocessos', () => {
expect(potAvancar('pendent', 'feta')).toBe(false);
expect(potAvancar('feta', 'en-curs')).toBe(false);
});
});Per què potAvancar no és un validador de formulari. Un validador comprova si un valor introduït és acceptable. La R6 no parla de valors sinó de transicions: depèn de l'estat anterior, que no és al formulari. És una regla del model, i el seu lloc natural és el servei —on ja l'aplica avancar()— i el botó, que s'inhabilita quan SEGUENT[estat] és null. Ficar regles de transició a la capa de formularis és un error d'ubicació que acaba duplicant la lògica.
Per què el domini va a domini/. Quatre raons, i les quatre les has vistes ja en aquest mòdul:
- Es prova sense framework. Les proves de dalt s'executen en mil·lisegons, sense TestBed, sense jsdom i sense renderitzar res.
- Es reutilitza a totes les capes. El component, el servei, l'encaminador i —si el servidor comparteix codi— l'API fan servir les mateixes funcions.
- Sobreviu al framework. És exactament la disciplina de 10-01:
domini/regles.jsha estat el mateix fitxer a les quatre versions d'aquesta pantalla. Canviar de framework és reescriure la vista, no l'aplicació. - És on el negoci es llegeix. Algú que vulgui saber quines regles regeixen Nómada Tasques obre una carpeta, no remena pels components.
Conclusió
Has vist el quart i últim enfocament del mòdul, i el més diferent de tots.
Saps què és Angular com a plataforma completa: encaminador, client HTTP, dos sistemes de formularis, injecció de dependències, RxJS, generació de codi, entorn de proves, internacionalització, SSR i migracions automàtiques, tot oficial i actualitzat alhora. Entens què significa «amb opinions» portat fins al final —no tries eines, l'estructura ve donada, hi ha una forma correcta de cada cosa, la verbositat és deliberada, i la corba és costeruda al principi i plana després— i en quin perfil de projecte compensa: aplicacions grans, longeves, amb equips nombrosos i rotació de personal.
Tens el mínim de TypeScript per llegir el codi —anotacions que desapareixen en compilar, interfícies que descriuen la forma d'un objecte, tipus d'unió literal que converteixen les regles R1–R10 en coses que el compilador impedeix expressar, genèrics que es llegeixen com «X de Y», i modificadors d'accés amb el matís que private no és #privat—, amb la remissió a 11-07 per aprendre'l de debò. I saps què són els decoradors: metadades declarades sobre una classe, coherents amb la filosofia que la configuració visqui al costat del que configura.
Coneixes la CLI i el que diu de les opinions d'Angular —que ng generate component creï un fitxer de proves per defecte no és un detall—, i els components independents amb els seus imports, que avui substitueixen els NgModule. Domines les quatre formes d'enllaç de dades amb la mnemotècnia dels claudàtors cap endins i els parèntesis cap enfora, i [(ngModel)] desmitificat com el v-model de Vue: una propietat que baixa i un esdeveniment que puja. I manejes la sintaxi de control de flux actual —@if/@else, @for amb @empty, @switch— amb els seus quatre avantatges sobre les directives antigues, i @defer amb els seus disparadors, el seu @placeholder, el seu @loading i el seu @error, que fa declarativament tot el que vas escriure a mà a 09-05.
Saps que el track de @for és obligatori i que sense ell el projecte no compila: la mateixa clau estable del teu reconciliar de 06-06, elevada d'avís a error, que és «amb opinions» en la seva forma més útil. Domines les senyals —signal amb set i update, computed cachejat i mandrós, effect amb la seva neteja— i saps que, a diferència de Vue, comparen amb Object.is i per tant la immutabilitat de 04-07 torna a ser obligatòria, amb la contrapartida que un senyal no es perd mai en desestructurar. Coneixes Zone.js i per què existia —comprovar-ho tot perquè no se sabia què havia canviat—, i la tendència zoneless que les senyals fan possible.
Entens la injecció de dependències: serveis amb @Injectable({providedIn: 'root'}), inject() que demana un tipus en lloc d'importar una instància, el patró de senyal privat escrivible més senyal públic de només lectura que és l'encapsulació de 05-03 aplicada a l'estat, i les quatre capacitats que compra —substituir en proves, canviar per entorn, controlar l'àmbit, i interceptar—. I saps que ja la vas practicar a 08-04, quan vas passar MagatzemEnMemoria al constructor de RepositoriLocal per poder provar sense localStorage: Angular fa d'aquest patró la norma i automatitza el cablejat.
Tens el just de RxJS: un observable és un flux de valors en el temps, és mandrós i no passa res sense subscribe —el parany número u per a qui ve de promeses—, pipe encadena operadors, i els set que apareixen sempre tenen equivalent en alguna cosa que ja vas escriure: map, filter, retry (el teu ambReintents), catchError, debounceTime (el teu debounce de 09-02), switchMap (el teu AbortController) i takeUntilDestroyed (el teu destruir()). Saps per què cal donar-se de baixa —és la fuita de 09-03 amb un altre nom— i les tres formes correctes de fer-ho, i tens el criteri per triar entre senyals i observables, amb toSignal i toObservable com a ponts.
Coneixes l'encaminador amb càrrega diferida via loadComponent, que és l'import() dinàmic de 09-05 integrat, amb guardes que el teu encaminador.js no tenia; els formularis reactius davant dels de plantilla amb la taula que decideix; i validadors que són funcions pures implementant la R2, la R4, la R7 i la R9, amb errors que porten dades per produir missatges útils, en la línia del teu ErrorDeValidacio amb .camp. I saps provar amb TestBed, on la línia providers: [{provide: TasquesService, useValue: fals}] és la demostració pràctica de què compra la injecció de dependències.
Has escrit la quarta versió de la mateixa pantalla —~180 línies, amb el domini en TypeScript però conceptualment idèntic als tres anteriors— i tens la taula comparativa completa: Angular guanya en cohesió, tipatge de sèrie, track obligatori, serveis injectats, migracions automàtiques i previsibilitat entre projectes; i perd en corba d'aprenentatge, verbositat, pes i encaix en projectes petits.
Ja has vist els quatre enfocaments amb la mateixa pantalla escrita quatre vegades. Queda la pregunta que dona sentit a tot el mòdul, i que no té una resposta única: com es tria. A l'última lliçó posaràs les quatre versions una al costat de l'altra amb les seves mètriques, ordenaràs els criteris que de debò importen —producte, equip, mercat, longevitat, rendiment, SEO, accessibilitat, cost—, i descobriràs que hi ha una decisió que acostuma a pesar més que la del framework: on es renderitza. I tancaràs connectant amb el projecte final, que es construeix en JavaScript pur i que a partir d'ara serà una decisió informada: Triar el Framework Adequat.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
