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)

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>
Per iniziare va scaricata la libreria Knockout JS dal sito https://knockoutjs.com/ e va referenziata nella pagina 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>
poi si definisce il view-model, ovvero la classe che definisce le proprietà e i metodi usati nella view:

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;
    });
}
in seguito si crea la view:

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>
e poi si realizza il binding (collegamento) tra il view-model e la view:

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">
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

HTML

<span data-bind="text: totale"></span>
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 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}">
se la proprietà calcolata isNegative del view-model ritorna true allora viene applicata la classe bg-danger.
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.

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à

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);
});
il risultato è questo

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.
Tags:
Esempi224 JavaScript184 Knockout js8 MVVM6
Potrebbe interessarti anche: