Tra le librerie JavaScript che consentono di realizzare interfacce Web interattive, Knockout.js merita un approfondimento speciale in quanto mette insieme diverse idee consentendoci di ottenere risultati molto interessanti. Non si tratta di una libreria orientata alla semplificazione della gestione del DOM, come jQuery o Prototype, né di una libreria di widget per il Web come jQuery UI o Kendo UI.
Knockout non è in competizione con questo tipo di librerie, che possono essere benissimo utilizzate insieme ad essa, ma intende offrire un pattern, un modello di programmazione che consenta allo sviluppatore di concentrarsi maggiormente sulla progettazione dell'interfaccia utente riducendo drasticamente la quantità di codice da scrivere.
In questo articolo esamineremo questo framework e lo metteremo alla prova realizzando una semplice applicazione.
Le idee chiave di KnockoutJS
Knockout.js è fondato su tre principi chiave che sono anche funzionalità pratiche:
Il binding dichiarativo
È il meccanismo che consente di associare oggetti JavaScript ad elementi del DOM con un approccio dichiarativo, direttamente nel codice HTML, utilizzando una sintassi semplice e concisa. Sfruttando HTML5, infatti, Knockout.js utilizza l'attributo data-bind
per mettere in corrispondenza in maniera semplice e rapida elementi dell'HTML con proprietà di oggetti JavaScript.
Il tracciamento delle dipendenze
Knockout.js è in grado di sfruttare le relazioni implicite tra gli oggetti JavaScript per aggiornare a cascata oggetti ed elementi dell'interfaccia utente.
Ciò è possibile grazie ad un meccanismo che consente di definire variabili calcolate e observable, per cui, ad esempio, se abbiamo definito un elemento NomeCognome
derivato dalla concatenazione di Nome
e Cognome
, ogni volta che il valore di Nome
o di Cognome
cambiano, il sistema aggiornerà automaticamente NomeCognome
, senza bisogno di scrivere una riga di codice.
Questo meccanismo è attivo anche in presenza di un una catena complessa di relazioni, consentendoci di mantenere con poco sforzo l'interfaccia utente sincronizzata con il modello dei dati sottostanti.
Il templating
In presenza di una struttura di dati complessa può far comodo gestire la loro visualizzazione tramite template. KnockoutJS supporta meccanismi per la definizione e il rendering di template sia nativi che esterni, come jQuery.tmpl o Underscore.
Il pattern MVVM
Queste caratteristiche rappresentano i cardini su cui si basa il pattern architetturale proposto da Knockout.js: il Model-View-ViewModel. Questo pattern è una variante di MVC che, in estrema sintesi, ci mette a disposizione un modello di sviluppo di interfacce Web che consente di ragionare sulla progettazione con un approccio il più possibile dichiarativo, riducendo la quantità di codice necessario.
Prima di entrare nel pratico vale la pena approfondire MVVM, dal momento che questo rappresenta il fondamento principale della libreria. Analogamente a quanto avviene per il pattern MVC, anche MVVM individua tre elementi nell'architettura di un'interfaccia utente:
- Model, indica il modello ad oggetti o l'insieme dei dati da gestire ed è indipendente dall'interfaccia utente
- View, rappresenta l'insieme degli elementi visuali di un'interfaccia utente, come ad esempio pulsanti, caselle di testo, finestre e simili, con i quali l'utente interagisce direttamente
- View Model, è un modello ad oggetti che rappresenta lo stato della view, fungendo anche da tramite tra view e model; ad esempio, in una lista di elementi, il view model sarà un oggetto che contiene gli elementi della lista e mette a disposizione metodi per aggiungere o eliminare elementi dalla lista
Rispetto al pattern MVC, il view model può essere visto come una sorta di controller specializzato che mette in corrispondenza le informazioni rappresentate dal model con quelle rappresentate dalla view.
Nelle applicazioni realizzate con Knockout.js, il view è rappresentato dal codice HTML, mentre il view model ed il model vengono implementati mediante oggetti JavaScript.
Creare view model
Knockout.js è scritta in puro JavaScript, è rilasciata con licenza MIT e non dipende da altre librerie. Il file compresso da utilizzare in produzione è di circa 38 KB e può essere scaricato dal sito del progetto su GitHub.
Per utilizzare la libreria è sufficiente caricarla nella pagina HTML, come mostrato di seguito:
<script type='text/javascript' src='knockout-2.0.0.js'></script>
Una volta caricata la libreria, i passi fondamentali per utilizzarla consistono nella dichiarazione del legame tra HTML (cioè la view) e view model. Nella creazione del view model e nel collegamento tra view model e model.
L'attibuto data-bind
L'approccio dichiarativo del legame tra view e view model è basato sull'attributo data-bind, attributo non nativo ma mutuato da HTML5. Questo attributo può essere usato in corrispondenza a qualsiasi elemento HTML. Il seguente codice mostra un esempio di utilizzo con l'elemento <span>
:
<p>Stiamo lavorando con <span data-bind="text: nomeLibreria"></span></p>
Le informazioni associate con l'attributo data-bind stabiliscono un legame tra il testo dell'elemento, come può essere la proprietà innerText
o textContent
, e la proprietà nomeLibreria
del view model. La specifica di questo legame è chiamata binding.
Creare un view model
La creazione di un view model corrisponde alla dichiarazione di un normale oggetto JavaScript, come può essere quello mostrato nel seguente codice:
var viewModel = {
nomeLibreria: 'Knockout'
};
A questo punto non ci resta che attivare il legame tra view e view model chiamando il metodo applyBindings() di Knockout.js:
ko.applyBindings(viewModel);
La chiamata a questo metodo deve essere effettuata dopo il caricamento del DOM, per cui può essere inserita in un blocco di codice in fondo alla pagina o all'interno di un apposito gestore come ad esempio $(document).ready()
di jQuery. Il risultato sarà corrispondente al seguente codice HTML:
<p>Stiamo lavorando con <span>Knockout</span></p>
Il risultato è visibile nell'esempio 1 (che troviamo anche in allegato).
Il parametro che abbiamo passato al metodo applyBindings() indica il view model da utilizzare per tutte le dichiarazioni data-bind contenute nella pagina.
Passando un secondo parametro abbiamo la possibilità di specificare un elemento della pagina a cui applicare il view model, restringendo il campo d'azione su una porzione del documento HTML e consentendoci di avere più view model da associare a diverse regioni della pagina.
Il seguente codice rappresenta un esempio di chiamata di questo tipo:
ko.applyBindings(viewModel, document.getElementById('IDElemento'));
Creare observable
Una delle caratteristiche fondamentali di Knockout.js è il tracciamento automatico delle dipendenze tra oggetti JavaScript ed elementi dell'interfaccia utente. Questo meccanismo è basato sulla possibilità di definire le proprietà del view model come observable, cioè come speciali oggetti in grado di individuare le dipendenze e di notificare le modifiche.
Riprendendo l'esempio di view model introdotto in precedenza, possiamo dichiarare la sua unica proprietà come observable nel seguente modo:
var viewModel = {
nomeLibreria: ko.observable('Knockout')
};
Il valore passato come parametro al metodo observable() indica il valore iniziale della proprietà. Non è necessario fare alcuna ulteriore modifica alla view. Da questo momento KnockoutJS sarà in grado di mantenere sicronizzati view e view model automaticamente.
Per vedere come ciò avvenga, aggiungiamo un pulsante alla nostra pagina HTML ed associamo all'evento click la seguente funzione:
function modificaViewModel() {
viewModel.nomeLibreria('la libreria Knockout!');
}
Questa funzione non fa altro che modificare il valore della proprietà nomeLibreria del view model. Dal momento che la proprietà nomeLibreria
è stata definita come observable, questo farà in modo che l'elemento <span>
correlato venga aggiornato automaticamente ottenendo un risultato corrispondente al seguente codice HTML:
Stiamo lavorando con la libreria Knockout!
Il risultato è visibile nell'esempio 2 (che troviamo anche in allegato).
Le proprietà dichiarate come observable vengono definite internamente come funzioni, per cui per leggere ad esempio il valore corrente di nomeLibreria
utilizzeremo la sintassi:
viewModel.nomeLibreria()
mentre, come abbiamo già visto, per modificarne il valore occorre utilizzare la sintassi:
viewModel.nomeLibreria('Nuovo valore').
Creare observable calcolati
Ci sono situazioni in cui il valore da mostrare in una interfaccia utente è un valore calcolato, come ad esempio la concatenazione di proprietà di tipo stringa. Anche in questo caso possiamo farci aiutare da KnockoutJS con un tipo particolare di observable: gli observable calcolati o computed observable.
Supponiamo di avere il seguente view model:
var viewModel = {
nomeLibreria: ko.observable('Knockout'),
autore: ko.observable('Steve Sanderson')
};
e di voler ottenere un output del genere:
<p>Stiamo lavorando con <span>Knockout di Steve Sanderson</span></p>
Possiamo aggiungere una nuova proprietà al view model dipendente dalle prime due e dichiarandola come observable calcolato tramite il metodo computed():
viewModel.credits = ko.computed(function() {
return viewModel.nomeLibreria() + ' di ' + viewModel.autore();
});
ed associando il testo dell'elemento <span>
a questa nuova proprietà:
<p>Stiamo lavorando con <span data-bind="text: credits"></span></p>
Questo accorgimento ci consentirà di mantenere automaticamente la sincronizzazione tra gli elementi coinvolti nella catena di dipendenze.
Il risultato è visibile nell'esempio 3 (che troviamo anche in allegato).
Gestire l'aspetto degli elementi di una pagina
Dopo aver esplorato i principi di base a cui fa riferimento KnockoutJS, mettiamo alla prova il framework costruendo una semplice applicazione e scoprendo man mano le funzionalità che ci vengono messe a disposizione.
L'applicazione che andremo a realizzare è costituita da una pagina per l'invio dell'ordine di libri. La pagina consente la selezione di uno o più libri da un elenco, l'indicazione della relativa quantità e l'aggiunta ad un carrello.
Costruiremo la pagina in maniera incrementale soffermandoci di volta in volta sulle funzionalità di Knockout.js che ci consentono di implementarla.
Creazione del view
La prima cosa che andremo a realizzare è il view del nostro modello MVVM. Come abbiamo sottolineato nei paragrafi precedenti, il view non è che il codice HTML della nostra applicazione, quindi in questo caso non dobbiamo far altro che concentrarci sulla realizzazione degli elementi che costituiranno la nostra pagina.
Nello specifico, andremo a visualizzare l'elenco dei libri in un <select>
, prepareremo uno <span>
in cui visualizzare il prezzo del libro selezionato, una casella di testo per indicare la quantità di libri da ordinare, un pulsante per l'aggiunta del libro nel carrello. Il carrello sarà costituito dall'elenco dei libri scelti dall'utente e da un pulsante per inoltrare l'ordine al server.
Il seguente codice è l'HTML del nostro view:
<form>
<div>
<table>
<thead><tr><th width="25%">Libro</th><th width="25%">Prezzo</th><th width="25%">Quantità</th><th width="25%"></th></tr></thead>
<tbody>
<tr>
<td>
<select>
<option>--- Seleziona un libro ---</option>
</select>
</td>
<td><span></span></td>
<td><input type="text" /></td>
<td><input type="button" value="Aggiungi al carrello" /></td>
</tr>
</tbody>
</table>
</div>
<div>
<p>Carrello</p>
<ul></ul>
<p>Totale EUR <span>0.0</span></p>
<input type="button" value="Acquista" />
</div>
</form>
Gestione della visibilità e dell'attivazione degli elementi di una pagina
Partendo dal view, cominciamo a ragionare su come vogliamo presentare le informazioni e su come gestire l'interazione con l'utente.
Una prima cosa che vorremmo ottenere è rendere invisibile il carrello quando è vuoto. Per far questo cominciamo a delineare sia il model che il view model. Ad esempio, potremmo rappresentare l'elenco dei libri da ordinare, cioè il model, come un array inizialmente vuoto. Il view model farà riferimento a questo array per gestire la visibilità tramite il binding visible. Questo si traduce nella seguente dichiarazione:
var libriOrdinati = [];
var viewModel = {
carrello: ko.observableArray(libriOrdinati)
}
e nella seguente modifica del codice HTML relativo al
<div data-bind="visible: carrello().length > 0">
<p>Carrello</p>
<ul></ul>
<p>Totale EUR <span>0.0</span></p>
<input type="button" value="Acquista" />
</div>
Riassumendo, abbiamo dichiarato la proprietà carrello del nostro view model come una proprietà di tipo observableArray
legata all'array libriOrdinati
. Mentre per le proprietà definite come observable, Knockout.js tiene sotto controllo il variare del valore, per le proprietà definite come observableArray
il framework traccia il variare del numero di elementi dell'array.
Per legare la visibilità del carrello al numero di elementi del carrello utilizziamo il binding visible specificando l'espressione booleana da valutare.
Queste semplici istruzioni impostano quindi la visibilità del carrello della nostra applicazione legandola all'effettiva presenza di libri contenuti in esso.
Altra impostazione che vogliamo applicare è l'abilitazione del pulsante "Aggiungi al carrello" soltanto quando il valore della quantità è maggiore di zero. Per ottenere ciò, aggiungiamo la proprietà quantitaDaOrdinare
al view model e leghiamola alla casella di testo della quantità tramite il binding value:
var libriOrdinati = [];
var viewModel = {
carrello: ko.observableArray(libriOrdinati),
quantitaDaOrdinare: ko.observable(0)
}
...
<input type="text" data-bind="value: quantitaDaOrdinare" />
Come si intuisce, il binding value rappresenta il valore contenuto in un elemento di un form, nel nostro caso di una casella di testo.
Quindi leghiamo l'abilitazione del pulsante "Aggiungi al carrello" alla quantità tramite il binding enable che controlla l'abilitazione di un elemento tramite un'espressione booleana:
<input type="button" data-bind="enable: quantitaDaOrdinare() > 0" value="Aggiungi al carrello" />
Queste impostazioni sono sufficienti per ottenere quello che volevamo.
Per completare, vogliamo che il campo quantità sia attivo soltanto quando dal
var libriOrdinati = [];
var viewModel = {
carrello: ko.observableArray(libriOrdinati),
quantitaDaOrdinare: ko.observable(0),
libroSelezionato: ko.observable()
}
...
<select data-bind="value: libroSelezionato">
Il valore iniziale della proprietà libroSelezionato
è indefinito dal momento che inizialmente non è selezionato nessun libro. Questo è il valore che dovremo verificare per gestire l'abilitazione del campo quantità. Aggiungiamo quindi al campo quantità il binding enable legandolo ad un'espressione booleana che valuta il valore di libroSelezionato
:
<input type="text" data-bind="value: quantitaDaOrdinare, enable: libroSelezionato() != undefined"/>
A questo punto della realizzazione della nostra applicazione avremo il risultato mostrato di seguito nell'esempio 4 (che troviamo anche in allegato).
Popolare un elemento select
Procedendo nella creazione della nostra applicazione, vediamo come gestire l'associazione di dati ad un elemento <select>
. Supponiamo che i dati sui libri del nostro catalogo siano memorizzati sotto forma di un array di oggetti JSON, eventualmente generati da una chiamata Ajax al server, analogo al seguente:
var catalogoLibri = [{ID: 1001, Autore: "H. Hesse", Titolo: "Narciso e Boccadoro", Prezzo: 8.00},
{ID: 1002, Autore: "A. Manzoni", Titolo: "Storia della colonna infame", Prezzo: 6.00},
{ID: 1003, Autore: "F. Kafka", Titolo: "America", Prezzo: 12.00},
{ID: 1004, Autore: "Acheng Zhong", Titolo: "Il re degli scacchi", Prezzo: 7.00}];
Aggiungiamo al view model la proprietà libri e la associamo all'array catalogoLibri
:
var viewModel = {
carrello: ko.observableArray(libriOrdinati),
quantitaDaOrdinare: ko.observable(0),
libroSelezionato: ko.observable(),
libri: catalogoLibri
}
Da notare che non abbiamo dichiarato la proprietà libri come observable. Questa scelta è dettata dal fatto che il catalogo dei libri non varia durante l'esecuzione dell'applicazione. Inoltre, avremmo potuto associare direttamente l'array alla proprietà libri, ma per una questione di organizzazione concettuale è opportuno tenere separate le responsabilità del model da quelle del view model.
A questo punto non ci rimane altro che definire i binding che legano l'elemento <select>
alla proprietà libri del view model:
<select data-bind="value: libroSelezionato, options: libri, optionsText: 'Titolo', optionsCaption: '--- Seleziona un libro ---'">
In pratica, tramite il binding options abbiamo indicato a KnockoutJS che la fonte dati da utilizzare per generare gli elementi <option>
è la proprietà libri del view model; con il binding optionsText indichiamo che il testo da visualizzare per ciascuna voce è fornito dalla proprietà Titolo; infine tramite il binding optionsCaption indichiamo il testo da visualizzare quando nessuna voce è selezionata.
Con questa soluzione visualizziamo i titoli dei libri nel <select>
e possiamo direttamente verificare il meccanismo di abilitazione e disabilitazione legato alla selezione del libro ed implementato in precedenza. Quando selezioniamo un libro, la proprietà libroSelezionato del view model conterrà il corrispondente oggetto, mentre quando è selezionata la caption il suo valore sarà undefined.
Per visualizzare insieme autore e titolo di un libro, possiamo ricorrere ad una funzione che concatena le due informazioni, come di seguito mostrato:
<select data-bind="value: libroSelezionato,
options: libri,
optionsText: function(item) { return item.Autore + '-' + item.Titolo },
optionsCaption: '--- Seleziona un libro ---'">
A questo punto non ci resta che visualizzare il prezzo del libro selezionato nell'apposito <span>
impostando il binding text come nel seguente esempio:
<span data-bind="text: visualizzaPrezzo()"></span>
e definendo la seguente funzione:
function visualizzaPrezzo() {
var result = "";
if (viewModel.libroSelezionato()) {
result = "EUR " + viewModel.libroSelezionato().Prezzo.toFixed(2)
}
return result;
}
La funzione visualizzaPrezzo()
restituisce il valore opportunamente formattato della proprietà Prezzo del libro selezionato nel caso in cui libroSelezionato è stato valorizzato dalla selezione dell'utente, altrimenti restituisce una stringa vuota.
Il risultato è mostrato di seguito nell'esempio 5 (che troviamo anche in allegato):