Knockoutjs jest javascriptowym frameworkiem, pozwalającym na wprowadzenie w obrębie interfejsu użytkownika, wzorca MVVM (Model View ViewModel) - w ramach projektu, przy którym obecnie pracuję często go stosujemy - warto się z nim zapoznać (więcej o knockoutjs tutaj). W wielkim skrócie, pozwala on na “podpięcie” elementów interfejsu, takich jak na przykład textbox’y, do modelu danych - aktualizacja wartości w tak podpiętym textbox’ie powoduje aktualizację wartości w modelu, a co za tym idzie w innych elementach UI podłączonych do tego modelu.

Problem

Wartości modelu, które możemy aktualizować, nazywane są obiektami obserwowalnymi (jest to w zasadzie javascriptowa implementacja wzorca obserwator). Możliwe jest również obserwowanie tablic - niestety nie działa to w sposób w jaki często by się nam przydało - cytat z dokumentacji:

Key point: an observableArray tracks which objects are IN the array, NOT the state of those objects.

Co więc w przypadku, kiedy jednak chcielibyśmy obserwować stan obiektów w tablicy? Należy użyć tytułowej tablicy obserwowalnej zawierającej obserwowalne obiekty.

Przykład

Weźmy prosty przykład: chcemy wyświetlić tablicę z dwoma kolumnami (imię i liczba), z możliwością usuwania wierszy z tabeli oraz z podsumowaniem (imiona i suma liczb). Poniżej fragment kodu HTML:

<table>
    <thead>
        <th>Name</th>
        <th>Amount</th>
    </thead>
    <tbody data-bind='foreach: rows'>
        <tr>
            <td>
                <input data-bind='value: row.name' />
            </td>
            <td>
                <input data-bind='value: row.amount' />
            </td>
            <td>
                <a href="#" data-bind="click: $parent.removeRow">Remove</a>
            </td>
        </tr>
    </tbody>
</table>

<p data-bind='text: names'></p>
<p data-bind='text: sum'></p>

Najpierw zwróćmy uwagę na tag ‘tbody’ - mamy tutaj podpięcie_‘foreach’_ do tablicy obserwowalnej ‘rows’ - binding ten pozwala na wyrenderowanie wszystkich tagów znajdujących się wewnątrz aktualnego tagu, tyle razy ile elementów znajduje się w tablicy. Dodatkowo, wewnątrz tagu ‘tbody’ (rodzica), mamy dostęp do nazwanych elementów tablicy: spójrzmy na dwa tagi input - pierwszy podpina atrybut ‘value’ tagu input do wartości_‘name’_ nazwanego elementu_‘row’; w drugim tagu‘input’, również mamy binding do atrybutu‘value’_ - tym razem podpinamy wartość ‘amount’.

W trzeciej kolumnie naszej tabeli mamy tag typu kotwica - (‘a’ z atrybutem href ustawionym na #). W tym przypadku korzystam z kolejnej ciekawej funkcjonalności knockouta czyli podpinania funkcji pod zdarzenia elementów DOM - w tym przypadku do zdarzenia ‘click’ bindujemy funkcję_‘removeRow’_ (odnosimy się do niej poprzez zmienną kontekstu ‘$parent’, która wskazuje na kontekst poza poza obecnym kontekstem).

Na samym dole dwa bindingi do atrybutu_‘text’_ tagu_‘p’_ (text jest bindingiem, który ustawia nie tyle atrybut tagu co wartość innerText/textContent elementu DOM) - tym razem podpinamy się nie do obiektów obserwowalnych, a do “obserwowalnych funkcji wyliczalnych” (w knockoucie, funkcje takie, zależą od obiektów obserwowalnych, i są przeliczane za każdym razem gdy wartość zmiennej obserwowalnej się zmieni).

Implementacja

Html przygotowany, czas przejść do skryptu - zacznijmy od utworzenia view modelu i dodaniu do niego opisywanej tablicy obserwowalnej:

// data model
var ViewModel = function() {
    var self = this;

    // observable array of observables
    self.rows = ko.observableArray([
        {
            row: {
                name: ko.observable('Tytus'),
                amount: ko.observable(150)
            }
        },
        {
            row: {
                name: ko.observable('Romek'),
                amount: ko.observable(90)
            }
        },
        {
            row: {
                name: ko.observable('Atomek'),
                amount: ko.observable(65)
            }
        }
    ]);
};

Na początku szybko rozważmy przypisanie

var self = this;

Jest to popularna konwencja stosowana w skryptach operujących na knockoucie - słowo kluczowe this jest często redefiniowane na przykład w funkcjach (także gdy razem z knockoutem używamy jQuery), dlatego w obrębie stosując_‘self’_ zamiast możemy łatwo uniknąć pomyłek (przykład użycia this wewnątrz funkcji za chwilę).

Przejdźmy wreszcie do tablicy - jak widać, mamy tutaj przypisanie do zmiennej_‘rows’_ wartości zwracanej przez metodę observableArray (należącej do klasy ‘ko’ - w ten sposób odwołujemy się do wszystkich funkcjonalności knockouta) - w tym momencie, każdy binding do tej tablicy, będzie reagował na zmiany ilości elementów w tej tablicy. Funkcję ‘observableArray’, wywołujemy przekazując do niej wartość początkową - w naszym przypadku jest to tablica obiektów ‘row’: każdy z nich zawiera właściwości name i amount - wartości te są zmiennymi obserwowalnymi (ko.observable) - dzięki temu możemy obserwować zmiany poszczególnych wartości w tablicy (zmieniać może je użytkownik, aktualizując wartości textbox’ów w tablicy), co wykorzystamy za chwilę do sumowania wartości amount i sklejania imion.

Sumowanie

Przejdźmy zatem do implementacji sumowania - do naszego view modelu dodajemy funkcję:

// computed sum
self.sum = ko.computed(function() {
    var result = 0;
    ko.utils.arrayForEach(self.rows(), function(item) {
        result += parseInt(item.row.amount());
    });

    return 'Suma: ' + result;
}, self);

Do zmiennej ‘sum’, przypisana jest wartość zwracana przez metodę ‘computed’ knockouta - przekazujemy do niej funkcję wykonującą sumowanie wartości amount poszczególnych elementów tablicy obserwowalnej, oraz zmienną_‘self’_ (po to by knockout znał kontekst view modelu). Warto tutaj zwrócić uwagę na metodę arrayForEach knockouta - pozwala ona na wykonanie funkcji przekazanej w drugim jej parametrze dla każdego z elementów tablic przekazanej w pierwszym parametrze. Knockout oprócz arrayForEach, dostarcza kilka przydatnych funkcji do manipulowania obiektami obserwowalnymi - o pozostałych można poczytać na blogu Knock Me Out Ryana Niemayera (polecam!).

Funkcja names

Druga z funkcji wyliczalnych, wygląda następująco:

// computed names
self.names = ko.computed(function() {
    var result = 'Imiona: ';
    ko.utils.arrayForEach(self.rows(), function(item) {
        result = result + ' ' + item.row.name();
    });

    return result;
}, self);

Jak widać, funkcja bardzo podobna do poprzedniej - zamiast zwracać sumę liczb, zwracana jest list imion, oddzialona spacją. Opisane funkcje, tak jak to zostało opisane przy listingu dotyczącym kodu html, reagują na każdą zmianę imienia lub liczby w wierszach wyświetlanych w tablicy.

Usuwanie wierszy

Pozostał nam już tylko opis usuwania wierszy z tablicy (zdarzenie ‘click’ i przypięta do niej funkcja ‘removeRow’):

// removing row
self.removeRow = function() {
    self.rows.remove(this);
}

Ta funkcja nie jest obserwowalna, ponieważ nie musi się aktualizować za każdym razem kiedy wartość obserwowalna ulegnie zmianie. W wywołaniu usuwania wartości z tablicy widzimy, że użyłem_‘this’_ - w tym przypadku pracujemy w kontekście konkretnego wiersza więc_‘this’_ oznacza co innego niż ‘self’ - właśnie z tego powodu na początku dokonaliśmy opisanego wcześniej przypisania.

Podsumowanie

Mam nadzieje, że powyższy przykład przyda się komuś, ponieważ w trakcie stawiania pierwszych kroków w knockoucie, zachowanie tablic obserwowalnych nie jest do końca oczywiste - sam trochę się naszukałem gdy po raz pierwszy potrzebowałem zrobić coś podobnego do tego co tutaj opisuję;)

Działająca wersja przykładu dostępna jest tutaj - jeśli ktoś nie zna jsfiddle, polecam się zapoznać - bardzo przydatne do testowania różnych javascriptowych rozwiązań, do tego napisane przez polskiego programistę, Piotra Zalewę!