Model View ViewModel mit Knockout.js

Infrastruktur anbinden

Zu guter Letzt bedarf es noch einer Option, die Liste auszugeben. Die einfachste Lösung hierfür besteht in der Verwendung eines ul-Elements, das man mit einer foreach-Datenbindung versieht und so das enthaltene li-Element für jeden Listeneintrag wiederholt:

<button data-bind="click: send">Senden</button>
<ul data-bind="foreach: messages">
<li data-bind="text: text"></li>
</ul>

<script type="text/javascript" src="knockout-2.2.1.js">

In einigen Fällen kann es sinnvoll sein, eine Vorlage wie in diesem Fall das li-Element nicht inline, sondern als eigenständigen Schnipsel zu definieren. Das ist beispielsweise dann nützlich, wenn man sie an verschiedenen Stellen benötigt oder dynamisch vom Webserver nachladen möchte.

Zu diesem Zweck kennt Knockout die template-Eigenschaft, die man eigenständig, aber auch im Zusammenspiel mit dem bereits vorgestellten foreach-Attribut verwenden kann. Die zugrunde liegende Idee ist, die Vorlage in einen Skriptblock auszulagern, den man über eine ID ansprechen kann und den der Webbrowser aufgrund des angegebenen Typs ignoriert:

</ul>
<script type="text/html" id="message-template">
<li data-bind="text: text"></li>
</script>

<script type="text/javascript" src="knockout-2.2.1.js">

Zusätzlich muss man die Definition der Liste derart ändern, dass sie die template-Eigenschaft verwendet, und ihren bisherigen Inhalt entfernen:

<button data-bind="click: send">Senden</button>
<ul data-bind="template: { name: 'message-template',
foreach: 'messages' }">

</ul>

Neben der direkten Datenbindung an Eigenschaften des ViewModels kennt Knockout auch die Möglichkeit, über den sogenannten Kontext auf Eigenschaften zuzugreifen: So kann man beispielsweise mit der Eigenschaft $parent auf dem übergeordneten Kontext der Datenbindung operieren, was innerhalb von Schleifen gelegentlich durchaus nützlich ist.

Von Zeit zu Zeit genügen die in Knockout enthaltenen Features zur Datenbindung nicht. Alternativ lassen sich jedoch eigene Bindungen entwickeln, die man anschließend auf die selbe Art wie value oder click verwenden kann.

Um eine eigene Eigenschaft zu definieren, muss man dem ko.bindingHandlers-Objekt die entsprechende Funktion in Form zweier Funktionen hinzufügen:

ko.bindingHandlers.myBinding = {
init: function (element, valueAccessor, allBindingsAccessor,
viewModel, bindingContext) {
// ...
},
update: function (element, valueAccessor, allBindingsAccessor,
viewModel, bindingContext) {
// ...
}
};

Für den Einsatz genügt es, den Namen myBinding im data-bind-Attribut des gewünschten Elements anzugeben:

<div data-bind="myBinding: 23"></div>

Knockout ruft die init-Funktion für jedes Element, das die myBinding-Eigenschaft verwendet, einmal auf. Direkt im Anschluss, und zusätzlich nach jeder Änderung des zugrunde liegenden Werts, ruft die Bibliothek die update-Funktion auf. Mithilfe der an die Funktionen übergebenen Parameter gewährt Knockout unter anderem Zugriff auf das Element und den aktuellen Wert:

  • Der Parameter element verweist auf das zugehörige Element im DOM. Man kann ihn beispielsweise in Verbindung mit jQuery nutzen, um das Element auf einfachem Weg verändern zu können.
  • Hinter valueAccessor verbirgt sich eine Funktion, die den aktuellen Wert aus dem zugrunde liegenden observable ausliest oder ihn dorthin schreibt.
  • Prinzipiell verhält sich der allBindingsAccessor auf die selbe Art, allerdings ermöglicht dieser den Zugriff auf alle Eigenschaften des ViewModels, die an das Element gebunden sind.
  • Der Parameter viewModel ermöglicht den Zugriff auf das ViewModel. Je nach Kontext steht das ganze ViewModel oder ein untergeordneter Ausschnitt zur Verfügung. Letzteres gilt beispielsweise in Schleifen.
  • Dennoch ist es über den bindingContext stets möglich, auf die Umgebung zuzugreifen – beispielsweise mit den Eigenschaften $parent und $root.

Darüber hinaus bietet Knockout noch einige weitere Möglichkeiten, etwa das Registrieren eigenentwickelter ereignisbehandelnder Funktionen und ein einfaches Serialisieren von ViewModels für den Datenaustausch mit dem Webserver.

Anbinden von Infrastruktur

Für Letzteres kennt Knockout die beiden Funktionen toJS und toJSON, die sich in ihrer Funktionsweise allerdings nicht wesentlich voneinander unterscheiden. So nimmt toJS ein ViewModel entgegen, klont dessen Struktur, evaluiert die derzeitigen Werte und gibt ein reines JavaScript-Objekt zurück:

var json = ko.toJS(viewmodel);
console.log(json);
// => {
// message: 'foo',
// messages: []
// }

Ruft man stattdessen die Funktion toJSON auf, erhält man als Ergebnis ein bereits als Zeichenkette serialisiertes JSON-Objekt:

var json = ko.toJSON(viewmodel);
console.log(json);
// => '{"message":"foo","messages":[]}'

Unter der Haube macht die toJSON-Funktion nämlich nichts anderes, als zunächst die Funktion toJS aufzurufen und deren Ergebnis an die Webbrowser-eigene Funktion JSON.stringify zu übergeben.

Für den Weg in der umgekehrten Richtung, also das Deserialisieren eines JSON-Objekts in ein ViewModel, bietet Knockout von Haus aus keine Unterstützung. Stattdessen muss man jeder Eigenschaft des ViewModels den jeweiligen Wert aus dem JSON-Objekt von Hand zuweisen.

Alternativ kann man hierfür das mapping-Plug-in verwenden, das analog zu den beiden Funktionen toJS und toJSON die beiden Funktionen fromJS und fromJSON enthält, die jeweils versuchen, ein JSON-Objekt beziehungsweise ein als Zeichenkette serialisiertes JSON-Objekt auf Basis von vorgegebenen Konventionen zu deserialisieren.

Keinerlei Hilfsmittel sind für die eigentliche Kommunikation mit dem Webserver vorhanden. Stattdessen verweist die Dokumentation auf Bibliotheken und Funktionen von Drittanbietern, wie das ohnehin häufig eingesetzte jQuery und dessen getJSON- und ajax-Funktion.

Diese Zurückhaltung ist einerseits von Vorteil, erlaubt sie doch die flexible Kombination von Knockout mit der JavaScript-Bibliothek der Wahl. Zusätzlich besteht so die Möglichkeit, andere Kommunikationsmittel als AJAX zu verwenden, beispielsweise WebSockets.

Der Nachteil besteht andererseits darin, dass die meisten Projekte derartigen Infrastrukturcode in stets gleicher Form enthalten und Knockout Entwickler daher dazu verleitet, unnötig viel redundanten Code zu schreiben. Zudem lässt die Bibliothek hierdurch die Architektur und Struktur der Webanwendung komplett offen.