Poprzednim razem dosyć pobieżnie przeleciałem przez możliwości jakie oferuje Knockout.js. Rozwijając tamten wpis, opiszę dzisiaj na prostym przykładzie sposób pracy z kolekcjami.

Załóżmy, że nasza aplikacja prezentuje listę produktów w postaci tabeli wyświetlającej nazwę oraz cenę. Najczęściej w takim przypadku dane pobierane są z serwera, a następnie wyświetlane na stronie. Dodanie oraz usunięcie produktu odbywa się poprzez przeładowanie strony i proces powtarza się od początku. Wprawdzie można wykorzystać AJAX do obróbki danych, ale i tak pozostaje nam na głowie aktualizacja widoku. Dzięki Knockout.js nie musimy już się tym więcej zajmować.

Zacznijmy od widoku.

<table>
	<thead>
		<tr>
			<th>Nazwa</th>
			<th>Cena</th>
			<th></th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td></td>
			<td></td>
			<td><a href="#">usuń</a></td>
		</tr>
	</tbody>
</table>

Jak widać jest to typowa tabela przygotowana do uzupełnienia danymi. Ponieważ korzystamy z Knockout.js, musimy przygotować ViewModel, który będziemy bindować do naszego widoku.

function ItemsViewModel(items)
{
	var self = this;
	self.items = ko.observableArray(items);
}

ViewModel to nic innego jak zwykła klasa JavaScript, do której przekazujemy dane pobrane z bazy (atrybut items). Warto zwrócić uwagę na przypisanie var self = this;. Dzięki temu będziemy zawsze mieli dostęp do właściwego obiektu (o czym za chwilę). Reszta kodu to nic innego jak wskazanie, że items, to tablica, którą biblioteka ma obserwować w poszukiwaniu zmian. Pozwoli to odwzorować każdą zaistniałą zmianę w widoku.

Pora zaktualizować widok tak, aby korzystał z przygotowanego ViewModelu.

<table>
	<thead>
		<tr>
			<th>Nazwa</th>
			<th>Cena</th>
			<th></th>
		</tr>
	</thead>
	<tbody data-bind="foreach: items">
		<tr>
			<td data-bind="text: name"></td>
			<td data-bind="text: price"></td>
			<td><a href="#" data-bind="click: $root.deleteItem">usuń</a></td>
		</tr>
	</tbody>
</table>

Na pierwszy rzut oka zmian nie widać. Jedyne co się zmieniło, to pojawienie się kilku atrybutów data-bind. Pierwszy z nich – foreach: items – jest odpowiedzialny za iterowanie po wszystkich elementach naszej kolekcji. Każdy element będzie reprezentował jeden wiersz tabeli. Text: name oraz text: price oznaczają, że w tym tagu tekstem będzie wartość właściwości obiektu o nazwie name oraz price. Ostatnim bindowaniem jest click: $root.deleteItem. Bindowanie to oznacza, że do zdarzenia click, przypisana zostanie metoda deleteItem. A dlaczego przed nazwą metody jest $root? Ponieważ aktualnym obiektem jest konkretny produkt. $root oznacza, że odwołujemy się do głównego obiektu ViewModel.

Dodajmy teraz do ItemsViewModel metodę deleteItem.

self.deleteItem = function(item) {
    self.items.remove(item);
}

Metoda jako parametr przyjmuje aktualny obiekt, czyli w tym przypadku pojedynczy produkt. Tutaj właśnie widać po co utworzyliśmy zmienną self. Wewnątrz metody this wskazuje na coś zupełnie innego niż self. Jeśli nie utworzylibyśmy zmiennej self, wówczas nie mielibyśmy dostępu do kolekcji.

Na koniec dodajmy jeszcze prosty formularz, który pozwoli definiować nowe produkty.

<input type="text" name="name" id="name" placeholder="nazwa" />
<input type="text" name="price" id="price" placeholder="cena" />
<button data-bind="click: addItem">Dodaj</button>

A teraz dodajmy metodę addItem.

self.addItem = function() {
    self.items.push(
        {
            name: $("#name").val(),
            price: $("#price").val()
        }
    );
}

Dla uproszenia (i na przyszłość) wykorzystałem jQuery do pobrania wartości pól formularza.

Ostatnią rzeczą jaką nam pozostało zrobić, to wskazać bibliotece z jakiego ViewModelu ma korzystać i możemy przetestować naszą stronę.

$(document).ready(function() {
    // dane pobrane z bazy lub wczytane ajaxem
    var itemsFromDb = [
        { name: "Coś", price: 123},
        { name: "Coś innego", price: 321}
    ];

    ko.applyBindings(new ItemsViewModel(itemsFromDb));
});

Działający przykład wraz z kodem źródłowym znajdziecie na jsFiddle. Miłej zabawy.