Introduzione ai Framework III: Angular

Niccolò Maltoni

Obiettivi e prerequisiti

Obiettivi della lezione

  • Comprendere la filosofia e l’architettura di Angular come framework completo
  • Configurare un progetto Angular con Angular CLI
  • Costruire componenti, gestire il data binding e gli eventi
  • Implementare comunicazione tra componenti con @Input() e @Output()
  • Introdurre Dependency Injection e i Services
  • Sviluppare una piccola applicazione pratica (Todo App in Angular)

Prerequisiti

  • Conoscenza di TypeScript essenziale (tipi, interfacce, classi)
  • Concetti di OOP: classi, ereditarietà, decoratori
  • Esperienza con React (utile per confronti)
  • Node.js ^18.9.1 e npm installati

Perché Angular? Framework completo

Angular vs React vs Vue: filosofie a confronto

Aspetto Vue (progressivo) React (libreria) Angular (framework)
Filosofia Incrementale + flessibile Minimalista, JSX-focused Batteries included, opinato
Curva apprendimento Bassa Media Alta (TypeScript, Decoratori)
Struttura Opzionale Build custom Rigorosa, opinionata
Linguaggio JavaScript/TypeScript JavaScript/JSX TypeScript obbligatorio
State management Reactive data useState, Context RxJS, Signals, Services
Dimensione app Piccola → Media Piccola → Grande Media → Molto Grande
Community Crescente Enorme Enterprise-focused

Quando scegliere Angular

Usa Angular se: - Progetto grande e complesso con struttura rigorosa - Team enterprise che apprezzerebbe uno standard fisso - Necessità di Dependency Injection e architecture patterns chiari - Applicazioni real-time con RxJS (reactive programming)

Evita Angular se: - Prototipo rapido o progetto piccolo - Team preferisce flessibilità e libertà stilistica - Risorse limitate (overhead di apprendimento TypeScript + Angular)

Setup Angular: Angular CLI

Installazione e primo progetto

1. Installa Angular CLI globalmente:

npm install -g @angular/cli@latest

2. Crea un nuovo progetto:

ng new my-angular-app
cd my-angular-app

3. Avvia il dev server:

ng serve --open
# oppure: npm start

Nota (Windows PowerShell): se riscontri errori di policy, esegui:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

Struttura progetto Angular

my-angular-app/
├── src/
│   ├── app/
│   │   ├── app.component.ts       # Component root
│   │   ├── app.component.html     # Template
│   │   ├── app.component.css      # Stili
│   │   └── services/              # Servizi (DI)
│   ├── main.ts                    # Entry point
│   └── index.html                 # Template HTML
├── angular.json                   # Config Angular
├── tsconfig.json                  # Config TypeScript
├── package.json
└── dist/                          # Output build (produzione)

TypeScript essenziale per Angular

Tipi base e annotazioni

// Tipi primitivi
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let items: string[] = ["A", "B", "C"];
let anyValue: any = 42;  // ❌ Evita any

// Unioni di tipi
let status: "pending" | "success" | "error" = "pending";

Interfacce: contratti per dati

// Definisci la forma di un oggetto
interface Product {
  id: number;
  title: string;
  price: number;
  inStock?: boolean;  // Opzionale
}

// Usa l'interfaccia
const product: Product = {
  id: 1,
  title: "Laptop",
  price: 999
};

Classi e ereditarietà

class Animal {
  constructor(public name: string) {}
  speak() { console.log(`${this.name} fa un suono`); }
}

class Dog extends Animal {
  speak() { console.log(`${this.name} abbaia`); }
}

const dog = new Dog("Rex");
dog.speak();  // "Rex abbaia"

Componenti Angular: struttura fondamentale

Anatomia di un componente

Un componente Angular è composto da 4 elementi:

// product.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-product',           // Tag HTML custom
  templateUrl: './product.component.html',  // File HTML
  styleUrls: ['./product.component.css']    // File CSS
})
export class ProductComponent {
  // Classe TypeScript: stato e logica
  productTitle: string = 'Laptop';
  price: number = 999;
  
  getDescription(): string {
    return `${this.productTitle} - €${this.price}`;
  }
}

Generare un componente (consigliato):

ng generate component components/Product
# oppure abbreviato: ng g c components/Product

Questo crea automaticamente 4 file: - product.component.ts (logica) - product.component.html (template) - product.component.css (stili) - product.component.spec.ts (test)

Usare un componente

// app.component.ts
import { Component } from '@angular/core';
import { ProductComponent } from './components/product/product.component';

@Component({
  selector: 'app-root',
  template: `<app-product></app-product>`,
  imports: [ProductComponent]  // Importa il componente
})
export class AppComponent {}

Data binding: sincronizzazione modello-vista

Interpolazione: { }

Renderizza proprietà e espressioni direttamente nel template:

<!-- product.component.html -->
<div>
  <h2>{{ productTitle }}</h2>
  <p>Prezzo: €{{ price }}</p>
  <p>Totale: €{{ price * 1.1 }}</p>
  <p>{{ isAvailable ? 'Disponibile' : 'Esaurito' }}</p>
</div>

Property binding: [property]

Assegna una proprietà TypeScript a una proprietà DOM:

<!-- Binding su proprietà HTML -->
<img [src]="imageUrl" [alt]="productTitle" />
<button [disabled]="!isAvailable">Compra</button>
<div [style.color]="color">Testo colorato</div>
<div [class.active]="isSelected">Elemento</div>

Event binding: (event)

Ascolta eventi DOM e chiama metodi TypeScript:

<!-- Click event -->
<button (click)="onBuyClick()">Compra</button>

<!-- Event binding con parametro -->
<button (click)="addToCart(productTitle)">Aggiungi: {{ productTitle }}</button>

<!-- Form submit -->
<form (ngSubmit)="handleSubmit()">
  <input type="text" [(ngModel)]="name" />
  <button type="submit">Invia</button>
</form>

Classe TypeScript:

export class ProductComponent {
  isAvailable: boolean = true;

  onBuyClick(): void {
    console.log('Prodotto acquistato!');
  }

  addToCart(title: string): void {
    console.log(`Aggiunto: ${title}`);
  }
}

Two-way binding: [(ngModel)]

Sincronizza dati in entrambe le direzioni (componente ↔︎ form):

<!-- Template -->
<input type="text" [(ngModel)]="username" placeholder="Nome utente" />
<p>Hai scritto: {{ username }}</p>

<input type="number" [(ngModel)]="quantity" />
<button (click)="order(quantity)">Ordina x{{ quantity }}</button>
// Classe
export class FormComponent {
  username: string = '';
  quantity: number = 1;

  order(qty: number): void {
    console.log(`Ordine di ${qty} unità da ${this.username}`);
  }
}

Direttive strutturali: manipolazione del DOM

*ngIf: rendering condizionale

<!-- Mostra se condition è true, altrimenti rimuove dal DOM -->
<div *ngIf="isLoggedIn">
  <p>Benvenuto, {{ username }}!</p>
</div>

<div *ngIf="!isLoggedIn">
  <p>Per favore, effettua il login.</p>
</div>

<!-- if/else-if/else -->
<div *ngIf="status === 'loading'; else loaded">
  <p>Caricamento...</p>
</div>
<ng-template #loaded>
  <p>Dati caricati!</p>
</ng-template>

*ngFor: iterazione su array

<!-- Lista semplice -->
<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

<!-- Con indice e length -->
<div *ngFor="let product of products; let i = index; let len = count">
  <p>#{{ i + 1 }} di {{ len }}: {{ product.title }}</p>
</div>

<!-- Filtrare e trasformare -->
<div *ngFor="let p of (products | filter:'expensive')">
  {{ p.title }}: €{{ p.price }}
</div>

Classe TypeScript:

export class ListComponent {
  products = [
    { id: 1, title: 'Laptop', price: 999 },
    { id: 2, title: 'Mouse', price: 25 },
    { id: 3, title: 'Keyboard', price: 80 }
  ];
}

Comunicazione tra componenti

@Input(): parent → child

Ricevi dati da un componente genitore:

// product-detail.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-product-detail',
  template: `<h3>{{ name }}</h3><p>€{{ price }}</p>`
})
export class ProductDetailComponent {
  @Input() name: string = '';
  @Input() price: number = 0;
}

Utilizzo nel genitore:

<!-- app.component.html -->
<app-product-detail [name]="'Laptop'" [price]="999"></app-product-detail>
<app-product-detail [name]="product.name" [price]="product.price"></app-product-detail>

@Output() e EventEmitter: child → parent

Invia dati al genitore tramite evento:

// product-card.component.ts
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-product-card',
  template: `
    <h3>{{ title }}</h3>
    <button (click)="onAddToCart()">Aggiungi al carrello</button>
  `
})
export class ProductCardComponent {
  title: string = 'Laptop';

  @Output() addedToCart = new EventEmitter<string>();

  onAddToCart(): void {
    this.addedToCart.emit(this.title);  // Emetti evento
  }
}

Genitore ascolta l’evento:

<!-- app.component.html -->
<app-product-card (addedToCart)="handleAddToCart($event)"></app-product-card>

<p>Carrello: {{ cartItems }}</p>
// app.component.ts
export class AppComponent {
  cartItems: string[] = [];

  handleAddToCart(productName: string): void {
    this.cartItems.push(productName);
    console.log(`Aggiunto: ${productName}`);
  }
}

Dependency Injection e Services

Cos’è la Dependency Injection (DI)

DI è un pattern che fornisce dipendenze esterne a una classe, anziché crearle internamente:

// ❌ Senza DI: la classe crea la dipendenza
class UserComponent {
  private service = new UserService();  // Rigido, difficile testare
}

// ✅ Con DI: la dipendenza viene iniettata
class UserComponent {
  constructor(private service: UserService) {}  // Flessibile, testabile
}

Creare e usare servizi

1. Genera un servizio:

ng generate service services/ProductService
# oppure: ng g s services/ProductService

2. Crea il servizio (con @Injectable):

// product.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'  // Disponibile globalmente (singleton)
})
export class ProductService {
  private products = [
    { id: 1, title: 'Laptop', price: 999 },
    { id: 2, title: 'Mouse', price: 25 }
  ];

  getProducts() {
    return this.products;
  }

  getProductById(id: number) {
    return this.products.find(p => p.id === id);
  }

  addProduct(product: any) {
    this.products.push(product);
  }
}

3. Inietta il servizio nel componente:

// products.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../services/product.service';

@Component({
  selector: 'app-products',
  template: `
    <ul>
      <li *ngFor="let p of products">{{ p.title }} - €{{ p.price }}</li>
    </ul>
  `
})
export class ProductsComponent implements OnInit {
  products: any[] = [];

  // Inietta il servizio nel constructor
  constructor(private productService: ProductService) {}

  ngOnInit(): void {
    // Carica i dati dal servizio
    this.products = this.productService.getProducts();
  }
}

Vantaggi della DI

  • Testabilità: facile mockare dipendenze
  • Riusabilità: lo stesso servizio in più componenti
  • Manutenibilità: logica centralizzata
  • Loose coupling: componenti independenti

Esercizio (15-20 min)

Esercizi consigliati

Esercizio 1: componente semplice (5 min) - Crea componente WelcomeComponent - Proprietà: username: string e greeting: string - Template: mostra “{{ greeting }}, {{ username }}!” - Usa property binding su un input disabled

Esercizio 2: property + event binding (7 min) - Crea CounterComponent - Stato: count: number = 0 - 3 pulsanti: “+1”, “-1”, “Reset” - Mostra il valore corrente con interpolazione - Usa (click) per i pulsanti

Esercizio 3: data binding + direttive (8 min) - Crea TodoListComponent - Array di task: { id, title, completed } - Usa *ngFor per renderizzare la lista - Usa *ngIf per mostrare “No tasks” se lista vuota - Usa [(ngModel)] per aggiungere checkbox completed

Esercizio 4: @Input() (5 min) - Crea TaskItemComponent con @Input() task - Mostra task.title e checkbox - Genitore passa dati tramite [task]="myTask"

Esercizio: Todo App in Angular

Obiettivo e task

Ricostruire una Todo App completa utilizzando Angular con: - Componenti per UI strutturata - Data binding per sincronizzazione dati - Direttive per rendering condizionale e liste - Services per logica CRUD condivisa - @Input() / @Output() per comunicazione

Task implementativi

Esercizio Todo App - acceptance criteria

Setup iniziale:

ng new todo-app
cd todo-app
ng g s services/TodoService
ng g c components/TodoList
ng g c components/TodoForm
ng g c components/TodoItem

1. TodoService (logica condivisa) - Proprietà: todos: Todo[] array iniziale con 3-4 task di esempio - Metodi: getTodos(), addTodo(title), deleteTodo(id), toggleTodo(id) - Interfaccia: export interface Todo { id: number; title: string; completed: boolean; }

2. TodoForm Component (input nuovo task) - Proprietà: newTaskTitle: string = '' - Event binding su submit: (ngSubmit)="onAdd()" - Two-way binding: [(ngModel)]="newTaskTitle" - Inietta TodoService e chiama addTodo() al submit - Resetta l’input dopo aggiunta

3. TodoItem Component (singolo task) - @Input() todo: Todo - @Output() deleted = new EventEmitter<number>() - Template: checkbox [checked]=“todo.completed”, title, bottone delete - Uso di (click) per emit delete

4. TodoList Component (orchestratore) - Inietta TodoService nel constructor - ngOnInit(): carica this.todos = this.todoService.getTodos() - Template: include TodoForm + lista con *ngFor di TodoItem - Ascolta (deleted) da TodoItem e chiama todoService.deleteTodo(id) - Mostra “No tasks” con *ngIf="!todos || todos.length === 0"

5. AppComponent (root) - Importa TodoList - Template: <app-todo-list></app-todo-list> + heading

Acceptance criteria: - App carica senza errori in console (ng serve) - Aggiungere task funziona (input accetta, submit aggiunge, input resetta) - Checkbox toggle: clickare checkbox marca/marca come completato - Delete: bottone elimina il task dalla lista - Task persistenti durante sessione (array in memory) - Input form pulito, layout leggibile - Nessun warning in console (ng serve) - Destructuring props in TodoItem: @Input() todo!: Todo

Bonus (opzionale): - Salva todos in localStorage e ripristina al refresh - Aggiungi campo edit (doppio click per editare) - Filtri: “Tutti”, “Completati”, “Attivi” - Contatore task (totali + completati)

Risorse ufficiali