0251 / 590 837 15
info@a-coding-project.de

Knockout! Template-Engine für JavaScript

Knockout! Template-Engine für JavaScript

Ich bin vor kurzer Zeit auf KnockoutJS aufmerksam gemacht worden und finde das Ding mittlerweile echt cool. Aus dem Grund möchte ich heute mal zeigen, was man mit der Template Engine für Sachen anstellen kann.

Einsatzgebiete für Knockout

Eine Template Engine für JavaScript? Viele Leute finden ja Smarty schon absolut unnötig, dann noch etwas, was im Browser läuft? Hmm… SEO-Technisch dürfte das wohl die volle Katastrophe sein…

Nun, Knockout ist vor allem für Webanwendungen, wie zum Beispiel Browsergames gedacht. Durch das MVC-Modell kann man dadurch die Performance stark optimieren, da man nicht mehr die komplette Seite neu lädt, sondern nur noch die Daten über AJAX.

Los geht’s: Interaktives Tutorial

Ich könnte jetzt ein schönes Beispiel beschreiben, wie man Knockout benutzen könnte. Aber damit hättet ihr keinen großen Mehrwert, denn das interaktive Tutorial auf der Website des JavaScript Frameworks ist einfach genial. Die ersten 3 Tutorials habe ich schon durch, man lernt echt viel.

Aus diesem Grund möchte ich auf dessen Basis aufzeigen, was man da alles machen kann.

Tutorial 1: Einführung

Die Beispiele basieren immer aus drei interaktiven Abschnitten. In einem schreibt man das HTML, in einem das JavaScript und ein Bereich zeigt an, was man gerade verbrochen hat.

<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<p>Full name: <strong data-bind="text: fullName"></strong></p>

Das ist im Endeffekt unser Template. Ist ganz normales HTML, was auch im Browser ausgeliefert wird. Nun kann das über JavaScript mit Daten befüllt werden:

function AppViewModel() {
    this.firstName = ko.observable("Stefan");
    this.lastName =  ko.observable("Wienströer");
    this.fullName = ko.computed(function(){
        return this.firstName() + " " + this.lastName();
    }, this);
}

// Activates knockout.js
ko.applyBindings(new AppViewModel());

Beim Laden der Seite wird mit dem Code mein Name angezeigt. Ihr könnt ihn bearbeiten und das untere Feld wird automatisch aktualisiert. Durch das data-bind sagt man, auf welche Eigenschaft nun zugegriffen werden muss. Das observable gibt an, dass beim ändern der Daten im Forumlar auch die Daten im JavaScript entsprechend angepasst werden.

Die fullName Eigenschaft ist mit einer Funktion berechnet. So wird beim Ändern des Formulars automatisch der volle Name neu angezeigt. Man muss gar keine JavaScript-Events zuweisen oder ähnliches.

Tutorial 2: Listen und Kollektionen

Im ersten Tutorial wurde die Bearbeitung eines einzelnen Datensatzes eingegangen. Das geht natürlich auch ein Stück größer: Mit Listen und Kollektionen.

<h2>Ihre Reservierungen (<span data-bind="text: seats().length"></span>)</h2>

<table>
    <thead>
        <tr>
            <th>Name des Passagiers</th>
            <th>Gericht</th>
            <th>Aufpreis</th>
            <th></th>
        </tr>
    </thead>
    <tbody data-bind="foreach: seats">
        <tr>
            <td><input data-bind="value: name" /></td>
            <td>
            <select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select>
            </td>
            <td data-bind="text: formattedPrice"></td>
            <td><a href="#" data-bind="click: $root.removeSeat">Entfernen</a></td>
        </tr>  
    </tbody>
</table>
<button data-bind="click: addSeat, enable: seats().length < 5">Neue Reservierung</button>
<h3 data-bind="visible: totalSurcharge() > 0">
    Totaler Aufpreis: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
</h3>

Im HTML lässt sich schon erkennen, wo die Reise hingeht. Durch die Bindings auf Container-Elemente können auch Arrays durchlaufen werden. Das kann man zum Beispiel für eine Tabelle machen, aber auch für einzelne Select-Felder.

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
    self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealName: "Standard (sandwich)", price: 0 },
        { mealName: "Premium (Hummer)", price: 34.95 },
        { mealName: "Ultimate (Zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);
    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    };
    
    self.removeSeat = function(seat) { self.seats.remove(seat) }
    
    self.totalSurcharge = ko.computed(function() {
   var total = 0;
   for (var i = 0; i < self.seats().length; i++)
       total += self.seats()[i].meal().price;
   return total;
});
}

ko.applyBindings(new ReservationsViewModel());

Das ist dann der entsprechende JavaScript-Part dazu. In der ReservationsViewModel werden hierbei die einzelnen Daten ausgelesen. In der Regel wird man hier auf AJAX zurückgreifen. Die Variablen werden wie eben wieder zugewiesen. Mit observableArray wird dabei eine ganze Liste zugewiesen. Wird da ein neuer Eintrag hinzugefügt, geschieht gleiches automatisch auch in der View.

Aus der Klasse SeatReservation werden quasi die einzelnen Instanzen für die Datensätze gebildet. Auch hier kann man wieder berechnete Eigenschaften einbinden, die man dann zum Beispiel in der Tabelle anzeigt (wie zum Beispiel formattedPrice ).

Knockout Liste

Knockout Liste

Tutorial 3: Single Page Anwendungen

Das dritte Beispiel kommt aus dem echten Leben: Es wird eine Mailbox gebaut, inklusive Ajax-Anfragen an den Server. Dadurch bekommt man den Flow vermittelt, den man im Live-Betrieb später einsetzen kann.

<ul class="folders" data-bind="foreach: folders">
    <li data-bind="text: $data,css: { selected: $data == $root.chosenFolderId() },
               click: $root.goToFolder"></li>
</ul>
<table class="mails" data-bind="with: chosenFolderData">
    <thead>
        <tr>
            <th>Von</th>
            <th>An</th>
            <th>Betreff</th>
            <th>Datum</th>
           </tr>
    </thead>
    <tbody data-bind="foreach: mails">
        <tr data-bind="click: $root.goToMail">
            <td data-bind="text: from"></td>
            <td data-bind="text: to"></td>
            <td data-bind="text: subject"></td>
            <td data-bind="text: date"></td>
        </tr>     
    </tbody>
</table>
<div class="viewMail" data-bind="with: chosenMailData">
    <div class="mailInfo">
        <h1 data-bind="text: subject"></h1>
        <p>
            <label>From</label>: 
            <span data-bind="text: from"></span>
        </p>
        <p>
            <label>To</label>:
            <span data-bind="text: to"></span>
        </p>
        <p>
            <label>Date</label>: 
            <span data-bind="text: date"></span>
        </p>
    </div>
    <p class="message" data-bind="html: messageContent" />
</div>

Mit dem bisschen HTML-Code werden bereits E-Mails in einer Liste und beim Klick auch schon im Detail angezeigt:

Inbox mit Knockout

Mail im Detail

Mail im Detail

Wie ihr vielleicht gesehen habt, wird die Detail-Ansicht im Default ausgeblendet. Dies geschieht über das data-bind mit with. Interessant ist jetzt natürlich das JavaScript dazu. Ist auch gar nicht so lang:

function WebmailViewModel() {
  var self = this;
  self.folders = ['Inbox', 'Archive', 'Sent', 'Spam'];
  self.chosenFolderId = ko.observable();
  self.chosenFolderData = ko.observable();
  self.chosenMailData = ko.observable();
  self.goToFolder = function(folder) { 
    self.chosenFolderId(folder); 
    self.chosenMailData(null); // Stop showing a mail
    $.get('/mail', { folder: folder }, self.chosenFolderData);
  };
  self.goToFolder('Inbox');
  self.goToMail = function(mail) { 
    self.chosenFolderId(mail.folder);
    self.chosenFolderData(null); // Stop showing a folder
    $.get("/mail", { mailId: mail.id }, self.chosenMailData);
  };
};

ko.applyBindings(new WebmailViewModel());

Bei einem Klick werden die goTo aufgerufen. Dabei werden dann chosenFolderId und chosenMailData gesetzt und somit die jeweiligen Views ein- und ausgeblendet. Mit jQuery(?) werden dann die Daten per GET geholt und direkt zugewiesen. Sie werden dann automatisch aktualisiert. Hier könnt ihr euch einfach mal den Netzwerk-Verlauf eures Browsers anschauen.

Tutorial 4: Eigene Bindings

Bei den Bindings wurden oben zum Beispiel mit „text:“ der Text von bestimmten Sachen zugewiesen. Diese Bindings kann man auch selbst erweitern. So kann man komplett andere Sachen an Elementen verändern lassen.

Ein solches Binding könnte so aussehen:

ko.bindingHandlers.fadeVisible = {
    init: function(element, valueAccessor) {
        // Start visible/invisible according to initial value
        var shouldDisplay = valueAccessor();
        $(element).toggle(shouldDisplay);
    },
    update: function(element, valueAccessor) {
        // On update, fade in/out
        var shouldDisplay = valueAccessor();
        shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
    } 
};

Wichtig ist hier vor allem die Update-Funktion. Sie wird aufgerufen, wenn sich ein Wert verändert hat. Dabei kann man dann entsprechend das element anpassen.

Tutorial 5: Laden und Speichern

Man kann einem Formular den submit-Binder zuweisen. In einer Funktion bekommt man dann eine neue Instanz, die dann zum Beispiel über Ajax an den Server geschickt werden kann. Ich denke das Beispiel zeigt ungefähr worum es geht:

<h3>Tasks</h3>

<form data-bind="submit: addTask">
    Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?" />
    <button type="submit">Add</button>
</form>

<ul data-bind="foreach: tasks, visible: tasks().length > 0">
    <li>
        <input type="checkbox" data-bind="checked: isDone" />
        <input data-bind="value: title, disable: isDone" />
        <a href="#" data-bind="click: $parent.removeTask">Delete</a>
    </li> 
</ul>

You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
<span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>

Und dazu das JavaScript:

function Task(data) {
    this.title = ko.observable(data.title);
    this.isDone = ko.observable(data.isDone);
}

function TaskListViewModel() {
    // Data
    var self = this;
    self.tasks = ko.observableArray([]);
    self.newTaskText = ko.observable();
    self.incompleteTasks = ko.computed(function() {
        return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
    });

    // Operations
    self.addTask = function() {
        self.tasks.push(new Task({ title: this.newTaskText() }));
        self.newTaskText("");
    };
    self.removeTask = function(task) { self.tasks.remove(task) };
}

ko.applyBindings(new TaskListViewModel());

Fazit: Das ist unsere Zukunft!

Knockout hat mich auf jeden Fall beeindruckt. Ich habe mich bisher noch nicht so sehr mit JavaScript MVCs befasst, denke aber dass es gerade in Webanwendungen zu einer sehr guten Performance führen kann. Man kann es zum Beispiel gut mit jQuery oder ähnlichem kombinieren. Es sieht danach aus, als würde ich das System in Kürze auch produktiv einsetzen. Schauen wir mal, ob es meinen Anforderungen auch in der Praxis Stand hält :)

Kommentare

?Herbrich? schrieb am 27.01.2014:

Wie sieht es denn mit ASP.NET aus? Kann ich es mit einen Webservice Kombinieren. Dass wäre dann ja auch für mich wiederum sehr Produktiv Interesannt. LG, Herbrich

Stefan Wienströer schrieb am 27.01.2014:

Das ist ja ein Client-Seitiges Framework. Von daher dürftest du auch mit ASP.net keine Probleme bekommen.