Knockout JS come funziona
Knockout JS è un framework JavaScript che permette di realizzare applicazioni basate sul pattern Model-View-View Model (MVVM).
Come altri framework simili come Anngular JS e React si basa sul concetto di binding tra la view (il template html) e il view-model (la classe JavaScript). Ovvero un legame diretto unidirezionale o bidirezionale tra view-model e view, dove un cambiamento o azione nel view-model produce un cambiamento della view.
Come primo esempio vediamo un app simile a quella realizzata per AngularJS come funziona che somma due numeri (sull'onchange della input)
Questa app è realizzata con questo codice:
Per iniziare va scaricata la libreria Knockout JS dal sito https://knockoutjs.com/
e va referenziata nella pagina html:
poi si definisce il view-model, ovvero la classe che definisce le proprietà e i metodi usati nella view:
in seguito si crea la view:
e poi si realizza il binding (collegamento) tra il view-model e la view:
Nella view tramite l'attributo data-bind realizzo il collegamento con il view-model.
Ad esempio il tag input:
tramite la proprietà di nome value dell'attributo data-bind realizzo un collegamento a due vie con la proprietà n1 del view-model (classe SgartKoTest1 self.n1).
Questo vuol dire che ogni volta che scrivo nella textbox, la corrispondente proprietà nel view-model si aggiorna e viceversa. Ovvero se aggiorno la proprietà nel view-model (JavaScript) si aggiorna il valore nella textbox.
Un altro binding utilizzato nella view è text usato per visualizzare il totale in un elemento html
in questo caso si tratta di un binding a una via, ovvero qualsiasi cambiamento nel view-model si rifeltte sulla view (ovviamente non vale il vicevera in quanto l'elemento non è editabile).
se la proprietà calcolata isNegative del view-model ritorna true allora viene applicata la classe bg-danger.
Per quanto riguarda il view-model la prima cosa da notare è che le proprietà devono essere di tipo ko.observable altrimenti il binding non funziona:
Un altro tipo di proprietà è ko.pureComputed, ovvero un valore calcolato, che dipende dalle altre proprietà di tipo observable:
Esistono anche le proprietà per gestire le collection, sempre di tipo observable, ko.observableArray il cui binding si fa con foreach.
In questo caso la view diventa
il risultato è questo
Ho creato questa mini guida (non esaustiva) come mio promemoria in quanto mi capita di dover fare manutenzione a vecchi progetti scritti in Knockout js e non sempre mi ricordo la sintassi esatta.
Attualmente i nuovi progetti li sviluppo solo con Angular 6 o superirore (non Angular JS che ormai è superato) o React.
Per qualsiasi approfondimento su Knockout JS fai riferimento alla guida ufficiale.
Come altri framework simili come Anngular JS e React si basa sul concetto di binding tra la view (il template html) e il view-model (la classe JavaScript). Ovvero un legame diretto unidirezionale o bidirezionale tra view-model e view, dove un cambiamento o azione nel view-model produce un cambiamento della view.
Come primo esempio vediamo un app simile a quella realizzata per AngularJS come funziona che somma due numeri (sull'onchange della input)
Test 1 Knockout JS
Questa app è realizzata con questo codice:
HTML
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Sgart Knockout JS - esempio 1</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script type="text/javascript" src="/lib/knockout.min.js"></script>
</head>
<body>
<div id="sgart-app-ko-test-1" class="app-ko container">
<h2>Test 1 <small>Knockout JS</small></h2>
<!-- inizializzo le variabili n1 e n2 -->
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">Numero 1</label>
<div class="col-sm-10">
<!-- uso l'attibuto "data-bind" con "value" per fare il binding bidirezionale -->
<input type="number" class="form-control" data-bind="value: n1">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Numero 2</label>
<div class="col-sm-10">
<input type="number" class="form-control" data-bind="value: n2">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Totale</label>
<!-- uso data-bind con "css" per cambiare la classe applicata in base al risultato -->
<label class="col-sm-10" data-bind="css: {'bg-danger': isNegative}">
<!-- uso data-bind con "text" fare il binding unidirezionale -->
<span data-bind="text: totale"></span>
</div>
</form>
</div>
<script>
// creo la classe che rappresenta il view-model
function SgartKoTest1() {
// salvo il riferimento a this
var self = this;
self.n1 = ko.observable(5);
self.n2 = ko.observable(7);
self.totale = ko.pureComputed(function () {
return parseFloat(self.n1()) + parseFloat(self.n2());
});
self.isNegative = ko.pureComputed(function () {
//return self.totale < 0; // non posso usarlo in quanto non ha dipendenze con le propery observable
return parseFloat(self.n1()) + parseFloat(self.n2()) < 0;
});
}
document.addEventListener("DOMContentLoaded", function () {
// istanzio il "view-model"
var vm = new SgartKoTest1();
// prendo il riferimento alla "view"
var view = document.getElementById("sgart-app-ko-test-1");
// faccio il "binding" tra il "view-model" e la "view"
ko.applyBindings(vm, view);
});
</script>
</body>
</html>
HTML
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Sgart Knockout JS</title>
<!-- css Bootstrap 3 -->
<link rel="stylesheet" type="text/css" href="/percorsoCss/bottstrap.min.css" />
<!-- libreria Knockout Js -->
<script type="text/javascript" src="/percorsoLibreria/knockout-3.5.1.min.js"></script>
</head>
<body>
<div id="sgart-app-ko-test-1" class="app-ko">
<!-- TODO: template della view -->
</div>
<script>
<!-- TODO: JavaScript view-model e binding -->
<script>
</body>
</html>
JavaScript
// creo la classe che rappresenta il view-model
function SgartKoTest1() {
// salvo il riferimento a this (l'istanza) per poterla usare successivamente
var self = this;
// a differenza di angular, tutte le proprietà devono essere degli oggetti "observable"
self.n1 = ko.observable(5);
self.n2 = ko.observable(7);
// anche le funzioni calcolate hanno un loro "wrapper" specifico
self.totale = ko.pureComputed(function () {
return parseFloat(self.n1()) + parseFloat(self.n2());
});
self.isNegative = ko.pureComputed(function () {
//return self.totale < 0; // non posso usarlo in quanto non ha dipendenze con le property observable
return parseFloat(self.n1()) + parseFloat(self.n2()) < 0;
});
}
HTML
<div id="sgart-app-ko-test-1" class="app-ko">
<h2>Test 1 <small>Knockout JS</small></h2>
<!-- inizializzo le variabili n1 e n2 -->
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">Numero 1</label>
<div class="col-sm-10">
<!-- uso l'attibuto "data-bind" con "value" per fare il binding bidirezionale -->
<input type="number" class="form-control" data-bind="value: n1">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Numero 2</label>
<div class="col-sm-10">
<input type="number" class="form-control" data-bind="value: n2">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Totale</label>
<!-- uso data-bind con "css" per cambiare la classe applicata in base al risultato -->
<label class="col-sm-10" data-bind="css: {'bg-danger': isNegative}">
<!-- uso data-bind con "text" fare il binding unidirezionale -->
<span data-bind="text: totale"></span>
</div>
</form>
</div>
JavaScript
// mi assicuro che il DOM sia completamente caricato
document.addEventListener("DOMContentLoaded", function () {
// istanzio il "view-model"
var vm = new SgartKoTest1();
// prendo il riferimento alla "view"
var view = document.getElementById("sgart-app-ko-test-1");
// faccio il "binding" tra il "view-model" e la "view"
ko.applyBindings(vm, view);
});
L'oggetto globale ko è messo a disposizione dalla libreria Knockout Js
Nella view tramite l'attributo data-bind realizzo il collegamento con il view-model.
Ad esempio il tag input:
HTML
<input type="number" class="form-control" data-bind="value: n1">
Questo vuol dire che ogni volta che scrivo nella textbox, la corrispondente proprietà nel view-model si aggiorna e viceversa. Ovvero se aggiorno la proprietà nel view-model (JavaScript) si aggiorna il valore nella textbox.
Un altro binding utilizzato nella view è text usato per visualizzare il totale in un elemento html
HTML
<span data-bind="text: totale"></span>
Se serve fare il binding con valori che contengono tag html si può usare data-bind="html: totale" anzichè text. Questo perche il binding html non fa l'escape dei tag.
Un ultimo binding usato nell'esempio è css con il quale cambio il colore di sfondo quando il numero diventa negativo:HTML
<label class="col-sm-10" data-bind="css: {'bg-danger': isNegative}">
A differenza di altri framework come Angular che usano vari attributi html per implementare i vari binding, Knockout JS utiliiza un solo attributo data-bind deve all'interno vanno indicate le varie espressioni con sisntassi diverse in base al tipo di binding.
Per quanto riguarda il view-model la prima cosa da notare è che le proprietà devono essere di tipo ko.observable altrimenti il binding non funziona:
JavaScript
self.n1 = ko.observable(5);
A differenza degli altri framework come Angular e React che lavorano con oggetti plain, Knockout JS impone l'uso di proprietà di tipo observable.
Questo complica un po' lo sviluppo delle applicazioni in quanto forza a ragionare con oggetti wrapper e non con le semplici proprietà JavaScript,
Personalmente preferisco usare Angular o React in quanto li trovo più completi, flessibili e moderni.
Questo complica un po' lo sviluppo delle applicazioni in quanto forza a ragionare con oggetti wrapper e non con le semplici proprietà JavaScript,
Personalmente preferisco usare Angular o React in quanto li trovo più completi, flessibili e moderni.
Un altro tipo di proprietà è ko.pureComputed, ovvero un valore calcolato, che dipende dalle altre proprietà di tipo observable:
JavaScript
self.totale = ko.pureComputed(function () {
return parseFloat(self.n1()) + parseFloat(self.n2());
});
Attenzione: non si può fare una proprietà calcolata bassata su altre proprietà calcolate
Va notato che nella view le proprietà vengono usate indicando solo il nome senza parentesi, mentre nel view-model, per accedere al valore, devono essere usate le parentesi self.n1().Se voglio aggiornare un valore nel view-model, devo passare il nuovo valore nelle parentesi, ad esempio: self.nomeProprieta(nuovoValore). Come divevo le proprietà sono degli oggetti wrapper, quindi vanno sempre gestite con la sintassi delle funzioni.
Esistono anche le proprietà per gestire le collection, sempre di tipo observable, ko.observableArray il cui binding si fa con foreach.
In questo caso la view diventa
HTML
<div id="sgart-app-ko-test-2" class="app-ko container">
<h2>Test 2, collection <small>Knockout JS</small></h2>
<table class="table">
<thead>
<tr>
<td>ID</td>
<td>Titolo</td>
</tr>
</thead>
<!-- il binding per "foreach" si fa sul parent di quello che deve essere ripeturo -->
<tbody data-bind="foreach: items">
<tr>
<td data-bind="text: id"></td>
<td data-bind="html: title"></td>
</tr>
</tbody>
</table>
</div>
Una differenza importante con gli altri framework è che il foreach va applicato all'esterno (parent) di quello che si vuole venga ripetuto. In questo caso è posizionato su tbody ma verranno ripetuti solo gli elementi tr con i loro contenuti.
Altra cosa da notare è il binding html che, non facendo l'escape, visualizza anche l'elemento in bold (riga 2).
Il corrispondente view-model saràAltra cosa da notare è il binding html che, non facendo l'escape, visualizza anche l'elemento in bold (riga 2).
JavaScript
function SgartKoTest2() {
var self = this;
self.items = ko.observableArray([
{id: 1, title: "Testo riga 1"},
{id: 2, title: "Testo <b>riga</b> 2"},
{id: 3, title: "Testo riga 3"},
]);
}
document.addEventListener("DOMContentLoaded", function () {
var vm = new SgartKoTest2();
var view = document.getElementById("sgart-app-ko-test-2");
ko.applyBindings(vm, view);
});
Test 2, collection Knockout JS
ID | Titolo |
1 | Testo riga 1 |
2 | Testo riga 2 |
3 | Testo riga 3 |
Ho creato questa mini guida (non esaustiva) come mio promemoria in quanto mi capita di dover fare manutenzione a vecchi progetti scritti in Knockout js e non sempre mi ricordo la sintassi esatta.
Attualmente i nuovi progetti li sviluppo solo con Angular 6 o superirore (non Angular JS che ormai è superato) o React.
Per qualsiasi approfondimento su Knockout JS fai riferimento alla guida ufficiale.