Framework I: Vue.js

Niccolò Maltoni

La necessità di framework moderni

L’uso dei framework JavaScript, come Vue.js, risponde alla necessità di gestire la complessità delle applicazioni web moderne, fornendo una struttura che facilita lo sviluppo.

  • Il JavaScript di base è un linguaggio di scripting che aggiunge interattività alle pagine web e manipola gli elementi del Document Object Model (DOM).
  • I framework sono essenziali per gestire file complessi e moduli, ottimizzando il codice e garantendo prestazioni elevate.

L’obiettivo di questi strumenti è semplificare lo sviluppo, migliorare la manutenibilità del codice e offrire un’esperienza utente più fluida.

Nota importante

I framework non sono una bacchetta magica. Richiedono tempo per essere appresi e utilizzati efficacemente. Non aspettatevi di diventare esperti in Vue.js/React/Angular in poche ore.

In questa lezione vedremo solamente i concetti fondamentali, applicando quanto abbiamo appreso finora di JavaScript. Per diventare proficienti con un framework in particolare è necessario approfondirlo nello specifico.

Perché un framework? Il problema

Partiamo da JavaScript puro. Un contatore classico:

index.html
<button id="btn">+</button>
<p id="count">0</p>
index.js
let count = 0;
document.getElementById('btn').addEventListener('click', () => {
  count++;
  document.getElementById('count').textContent = count;
});

Funziona. Ma se l’app cresce?

Con JS puro, all’aumentare della complessità:

  • Stato sparso: variabili count, items, user sparse nel codice
  • Aggiornamenti manuali: ogni modifica richiede querySelector + update
  • UI che si desincronizza: facile dimenticare un textContent da aggiornare

Vue.js in una riga

Vue.js è un framework JavaScript progressivo per costruire interfacce utente.

  • Progressivo: si usa da una singola pagina HTML fino a una SPA completa
  • Dichiarativo: descrivi cosa vuoi mostrare, Vue si occupa del come
  • Reattivo: lo stato cambia, il DOM si aggiorna automaticamente

Possiamo iniziare a usare Vue senza setup complessi: è sufficiente includere la libreria via CDN:

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

oppure, se vogliamo usare i moduli ECMAScript:

<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
  // ...
</script>

Prima app Vue.js

<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8">
  <title>Prima app Vue</title>
</head>
<body>
  <div id="app">
    <h1>{{ messaggio }}</h1>
  </div>

  <script type="module">
    import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';

    createApp({
      data() {
        return { 
          messaggio: 'Ciao Vue!' 
        };
      }
    }).mount('#app');
  </script>
</body>
</html>

Prima app Vue.js: dettaglio

Ogni applicazione Vue inizia creando una nuova istanza dell’applicazione con la funzione createApp:

import { createApp } from 'vue'

const app = createApp({
  /* opzioni del root component (componente root) */
})

L’oggetto che passiamo a createApp è in realtà un componente. Ogni app richiede un “componente radice” che può contenere altri componenti come suoi figli.

Un’istanza dell’applicazione non renderizzerà nulla fino a quando non verrà chiamato il suo metodo .mount(). Questo metodo si aspetta un argomento “contenitore”, che può essere sia un elemento reale del DOM sia un selettore di stringa:

html
<div id="app"></div>
js
app.mount('#app')

Il contenuto del componente root dell’app verrà renderizzato all’interno dell’elemento contenitore.

Il metodo .mount() dovrebbe sempre essere chiamato dopo che siano state effettuate tutte le configurazioni dell’app e le registrazioni delle risorse.

Reattività

Vediamo un esempio con un pulsante che incrementa un contatore:

<div id="app">
  <button @click="count++">+</button>
  <p>Conteggio: {{ count }}</p>
</div>

<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';

  createApp({
    data() {
      return { count: 0 }
    }
  }).mount('#app');
</script>

Possiamo notare l’assenza di qualsiasi codice che manipoli direttamente il DOM. Niente querySelector. Niente textContent. Niente updateDOM().

Vue utilizzerà automaticamente l’innerHTML del contenitore come template se il componente root non ha già un’opzione template.

Come funziona la reattività

In Vue, lo stato (i dati dentro data()) è reattivo: quando cambia, il DOM si aggiorna automaticamente.

Vue fa questo tracciando quali proprietà dello stato usa il template. Se count compare tra doppie parentesi graffe {{ }}, Vue sa che il template dipende da count.

Quando count cambia, Vue:

  1. Rileva il cambiamento
  2. Nota che il template dipende da count
  3. Ricalcola il template
  4. Aggiorna solo il DOM che è stato modificato

Questo comportamento è trasparente: non dobbiamo fare nulla di speciale. Basta usare le proprietà di data() nel template, e Vue fa il resto.

Template: sintassi delle espressioni

Nel template, inseriamo testo e logica usando la sintassi mustache {{ }}:

<p>{{ messaggio }}</p>
<p>Conteggio: {{ count }}</p>
<p>Doppio: {{ count * 2 }}</p>
<p>Connesso: {{ connesso ? 'Sì' : 'No' }}</p>

Ogni volta che una proprietà di data() usata in {{ }} cambia, Vue aggiorna il template.

All’interno di {{ }} è possibile scrivere espressioni JavaScript arbitrarie:

  • Accesso a variabili: {{ messaggio }} interpola il valore di data().messaggio
  • Invocazione di metodi: {{ nomeMetodo() }} chiama un metodo e mostra il risultato
  • Espressioni aritmetiche o booleane: {{ count + 1 }}, {{ connesso ? 'online' : 'offline' }}

Tuttavia, non è possibile scrivere istruzioni come if, for, while dentro {{ }}.

Per questo ci sono le direttive (che vedremo tra poco).

Confronto: dichiarativo vs imperativo

JS puro (imperativo)

<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8">
  <title>JS puro - esempio</title>
</head>
<body>
  <button id="btn">+</button>
  <p id="count">0</p>

  <script>
    let count = 0;
    const btn = document.getElementById('btn');
    const el = document.getElementById('count');

    btn.addEventListener('click', () => {
      count++;
      // devo aggiornare il DOM manualmente
      el.textContent = count;
    });
  </script>
</body>
</html>

Dico come aggiornare il DOM.

Vue (dichiarativo)

<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8">
  <title>Vue - esempio</title>
</head>
<body>
  <div id="app">
    <button @click="count++">+</button>
    <p>Conteggio: {{ count }}</p>
  </div>

  <script type="module">
    import { createApp } from 'https://unpkg.com/vue@3';

    createApp({
      data() {
        return { count: 0 }
      }
    }).mount('#app');
  </script>
</body>
</html>

Dico cosa mostrare. Vue aggiorna il DOM da solo.

Esercizio: contatore con pulsanti

Consiglio

Partendo dall’app contatore vista sopra:

  1. Aggiungete un pulsante - che decrementa count
  2. Aggiungete un pulsante Reset che riporta count a zero

Output atteso: tre pulsanti funzionanti, il contatore si aggiorna senza toccare il DOM manualmente.

Le direttive v-*

Una direttiva è un normale attributo HTML con prefisso v- che viene interpretato in modo speciale da Vue, permettendo di applicare un comportamento reattivo al DOM.

  • Il prefisso v- indica che si tratta di una direttiva Vue
  • L’argomento (opzionale) specifica un target, come un evento o un attributo
  • Il modificatore (opzionale) aggiunge un comportamento speciale, come .prevent per @submit.prevent
  • Il valore è un’espressione JavaScript che viene valutata e collegata al DOM

Come vedremo, alcune direttive possiedono anche una forma abbreviata per comodità.

Binding di attributi

Le parentesi graffe non possono essere utilizzate all’interno degli attributi HTML. Si può usare, invece, la direttiva v-bind:

<div v-bind:id="dynamicId"></div>

Dato che v-bind è utilizzato così di frequente, ha una sintassi abbreviata dedicata, che consiste nel sostituire v-bind: con solamente i due punti “:

<div :id="dynamicId"></div>

La direttiva v-bind indica a Vue di mantenere sincronizzato l’attributo id dell’elemento con la proprietà dinamica dynamicId del componente.

Se il valore associato è null o undefined, l’attributo verrà rimosso dall’elemento renderizzato. Per gli attributi booleani, come disabled, anche se il valore è falsy, l’attributo non sarà presente.

Rendering condizionale

La direttiva v-if viene utilizzata per renderizzare in maniera condizionale un blocco di codice. Il blocco verrà renderizzato nel DOM solo se l’espressione della direttiva restituisce un valore truthy.

<div id="app">
  <p v-if="connesso">Benvenuto, sei connesso.</p>
  <p v-else>Effettua il login per continuare.</p>

  <button @click="connesso = !connesso">Toggle login</button>
</div>

<script>
  createApp({
    data() {
      return { connesso: false }
    }
  }).mount('#app')
</script>

Ovviamente esistono anche le direttive v-else-if e v-else per indicare “blocchi alternativi” a v-if.

Esiste anche una direttiva v-show che nasconde un elemento invece di rimuoverlo dal DOM. La scelta tra v-if e v-show dipende dalle esigenze specifiche dell’applicazione.

Rendering delle liste

Per mostrare una lista di elementi basati su un array possiamo utilizzare la direttiva v-for, che itera su un array e ripete un elemento per ogni valore.

La direttiva v-for utilizza una sintassi nella forma di item in items, dove items è l’array di dati sorgente e item è un alias per l’elemento dell’array su cui si sta iterando:

<div id="app">
  <ul>
    <li v-for="ricetta in ricette" :key="ricetta.id">
      {{ ricetta.nome }} — {{ ricetta.categoria }}
    </li>
  </ul>
</div>

<script>
  createApp({
    data() {
      return {
        ricette: [
          { id: 1, nome: 'Pasta al pomodoro', categoria: 'primo' },
          { id: 2, nome: 'Tiramisù', categoria: 'dolce' }
        ]
      }
    }
  }).mount('#app')
</script>

Gestione degli eventi

La direttiva v-on (abbreviata con la chiocciola @) collega un evento DOM a un handler.

<div id="app">
  <button @click="count++">+</button>
  <p>Conteggio: {{ count }}</p>
</div>

<script>
  createApp({
    data() {
      return { count: 0 }
    }
  }).mount('#app')
</script>
  • @click="count++" è un inline handler: l’espressione viene eseguita direttamente nel template
  • v-on:click è la forma completa; @click è l’abbreviazione
  • Possiamo usare qualunque espressione JavaScript: e.g. @click="console.log('Ciao')"

La variabile $event ci permette di accedere all’evento DOM originale:

<button @click="saluta('Ciao')">Saluta</button>
<button @click="elimina(ricetta.id, $event)">Elimina</button>

Definizione di metodi

Gli inline handler sono comodi per azioni semplici.

Quando dobbiamo svolgere azioni più complesse (validazione, calcoli, più linee), possiamo definire dei metodi all’interno della proprietà methods:

<div id="app">
  <button @click="incrementa">+</button>
  <button @click="decrementa">-</button>
  <button @click="azzera">Reset</button>
  <p>Conteggio: {{ count }}</p>
</div>

<script>
  createApp({
    data() {
      return { count: 0 }
    },
    methods: {
      incrementa() { this.count++ },
      decrementa() { this.count-- },
      azzera()     { this.count = 0 }
    }
  }).mount('#app')
</script>

Nei metodi, possiamo accedere allo stato del componente tramite this.

Modificatori di eventi

I modificatori sono suffissi speciali che modificano il comportamento di una direttiva.

<form @submit.prevent="aggiungi">
  <input type="text" placeholder="Nome ricetta">
  <button type="submit">Aggiungi</button>
</form>

<script>
  createApp({
    methods: {
      aggiungi(event) {
        // con .prevent, event.preventDefault() è già stata chiamata
        console.log('Form inviato senza refresh!')
      }
    }
  }).mount('#app')
</script>
  • .prevent → chiama event.preventDefault() (evita il refresh della pagina)
  • .stop → chiama event.stopPropagation() (ferma la propagazione)
  • .once → il listener viene eseguito solo una volta

Computed properties

Le computed properties sono valori derivati dallo stato, calcolati automaticamente.

<div id="app">
  <p>Ricette totali: {{ ricette.length }}</p>
  <p>Ricette dolci: {{ ricetteDolci }}</p>
</div>

<script>
  createApp({
    data() {
      return {
        ricette: [
          { id: 1, nome: 'Tiramisù', categoria: 'dolce' },
          { id: 2, nome: 'Pasta al pomodoro', categoria: 'primo' },
          { id: 3, nome: 'Panna cotta', categoria: 'dolce' }
        ]
      }
    },
    computed: {
      ricetteDolci() {
        return this.ricette.filter(r => r.categoria === 'dolce').length
      }
    }
  }).mount('#app')
</script>

Vue ricalcola la computed property solo quando le sue dipendenze cambiano (caching automatico).

Binding bidirezionale e input reattivo

La direttiva v-model crea un collegamento bidirezionale tra l’input e lo stato:

  • Quando l’utente digita → lo stato data() si aggiorna
  • Quando lo stato cambia → l’input mostra il nuovo valore
<div id="app">
  <input v-model="nome">
  <p>Ciao, {{ nome }}!</p>
</div>

<script>
  createApp({
    data() { return { nome: '' } },
  }).mount('#app')
</script>

Dietro le quinte, v-model è una scorciatoia per:

<!-- equivalente a v-model="nome": -->
<input :value="nome" @input="nome = $event.target.value">

Esercizio: metodo raddoppia

Consiglio

Partendo dall’app contatore con i tre pulsanti:

  1. Aggiungete un metodo raddoppia che moltiplica count per 2
  2. Aggiungete il pulsante corrispondente nel template

Output atteso: quattro pulsanti funzionanti. Il metodo usa this.count.

I componenti: pezzi di UI riutilizzabili

I componenti ci permettono di dividere l’interfaccia utente in pezzi indipendenti e riutilizzabili.

Un componente è un pezzo di UI con il proprio template, i propri dati e il proprio comportamento.

Definizione

const app = createApp({ /* ... */ })

app.component('saluto-utente', {
  props: ['nome'],
  template: `<p>Ciao, {{ nome }}!</p>`
})

Registriamo un componente globalmente con app.component().

Uso nel template

<div id="app">
  <saluto-utente nome="Alice"></saluto-utente>
  <saluto-utente nome="Bruno"></saluto-utente>
  <saluto-utente nome="Carla"></saluto-utente>
</div>

Una volta registrato, possiamo usarlo come un normale tag HTML.

I componenti registrati globalmente possono essere utilizzati nel template di qualsiasi componente dell’applicazione.

Le Props: passare dati ai componenti

Le props sono attributi personalizzati che puoi registrare su un componente per passargli dei dati.

Dichiarazione

app.component('ricetta-card', {
  props: ['nome', 'categoria'],
  template: `
    <div class="card">
      <h3>{{ nome }}</h3>
      <p>{{ categoria }}</p>
    </div>
  `
})

Le props si dichiarano con un array di stringhe.

Passaggio dei valori

<ricetta-card
  nome="Tiramisù"
  categoria="dolce">
</ricetta-card>

<ricetta-card
  :nome="ricetta.nome"
  :categoria="ricetta.categoria">
</ricetta-card>

Valori statici senza :, valori dinamici con : (v-bind).

Le props formano un legame unidirezionale verso il basso: quando la proprietà genitore si aggiorna, il dato fluisce al figlio, ma non viceversa.

Componenti e liste: il vantaggio reale

Il vantaggio reale dei componenti emerge quando lavoriamo con liste di dati.

app.component('ricetta-card', {
  props: ['ricetta'],
  template: `
    <div class="card">
      <h3>{{ ricetta.nome }}</h3>
      <p>Categoria: {{ ricetta.categoria }}</p>
      <p>Ingredienti: {{ ricetta.ingredienti.join(', ') }}</p>
    </div>
  `
})
<div id="app">
  <ricetta-card
    v-for="r in ricette"
    :key="r.id"
    :ricetta="r">
  </ricetta-card>
</div>

Ogni istanza del componente mantiene il proprio stato separato. La prop :ricetta riceve un oggetto diverso per ogni iterazione.

Esercizio: app ricettario

Consiglio

Riprendiamo il ricettario delle lezioni precedenti e lo ricostruiamo in Vue con stato reattivo e template dichiarativo.

Costruite una mini app ricettario usando tutti i concetti della lezione.

Struttura dati:

{ id, nome, categoria, ingredienti }

Funzionalità da implementare:

  1. Definite i dati iniziali in data() con la struttura indicata
  2. Mostrate la lista ricette con v-for e :key="ricetta.id"
  3. Aggiungete filtro per categoria con v-model + computed
  4. Mostrate il conteggio nel formato x su y (visualizzate su totale)
  5. Implementate il form di aggiunta con v-model + @submit.prevent
  6. Implementate la cancellazione di una ricetta con @click e metodo dedicato
  7. Estraete la card in un componente ricetta-card riutilizzabile, passando i dati via props