Knockout – Custom Bindings & jQuery Datepicker

written by devangelist on März 4, 2013 in HTML and Javascript and jQuery and jQuery UI and Knockout and MVVM with 2 comments

Die Custom Bindings werden oft und gerne von Anfängern übersehen, doch dafür gibt es eigentlich keinen Grund – denn hat man sie erst einmal verstanden, verfügt man über ein mächtiges Werkzeug, besonders wenn es darum geht, komplexe DOM Manipulationen vorzunehmen.

Standard Bindings wie ein text oder value reichen in vielen Fällen nicht aus und der Reiz, dafür wieder ein Element via jQuery-Selektor zu ändern, ist besonders groß. Doch genau damit wird das dahinterstehende Konzept gebrochen, denn es geht unter anderem auch darum, die Steuerung der UI so gut wie Möglich auszulagern.

Ein Custom Binding bietet für dieses Problem genau die richtige Lösung – nämlich die DOM Manipulation zu abstrahieren. Ein Binding besteht aus zwei Teilen, einer init und einer update Funktion:

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

Die Magie bieten die ersten zwei Parameter, zum Beispiel um via jQuery auf das Element zuzugreifen oder um den Wert aus dem Observable zu bekommen:

ko.bindingHandlers.customBinding = {
  init: function(element, valueAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    $(element).fadeIn();
    $(element).text(value);
  }
};

Die fünf Parameter

  • element: Wie erwartet das DOM-Element, welches an dieser Stelle nicht erst selektiert werden muss.
  • valueAccessor: Diese Funktion bietet Zugriff auf den übergebenen Wert im Binding. Wenn ein Observable verwendet wurde, dann ist das Ergebnis der Funktion das Observable.
  • allBindingsAccessor: Wie der Name schon sagt, bietet diese Funktion Zugriff auf alle anderen Bindings. Diese Möglichkeit wird im Normalfall für zusätzliche options-Parameter verwendet.
  • viewModel: Angefangen im root-Level, wenn sich das Binding also in keinem Template oder ähnlichem befindet, ist das viewModel das Hauptmodel, welches am Anfang bei ko.ApplyBindings mitgegeben wurde. In einer foreach hingegen bietet es Zugriff auf das aktuelle Element im Array und wenn das Binding innerhalb eines with: person verwendet wird, ist das viewModel person.
  • bindingContext: Dieses Objekt beinhaltet die Daten, welche aus den Bindings referenziert werden können.  Interessant in diesem Zusammenhang ist die Möglichkeit mit $parent bzw. $parents auf das Elternelement zugreifen zu können, sowie einige weitere spezielle Eigenschaften wie $root, $index (im foreach-Binding) und $parentContext.

Init/Update

  • Die init Funktion wird einmalig beim binden der Daten aufgerufen.
  • Die update Funktion wird einmalig direkt nach der init aufgerufen und bei jedem Ändern eines Wertes des Observable.

Praxis: jQuery UI Datepicker mit Knockout

jQuery Plugins können wunderbar mit einem Custom Binding abgebildet werden, denn die darin enthaltenen Parameter beinhalten das aktuelle DOM-Element und eine Möglichkeit, options-Parameter mitzugeben.

ko.bindingHandlers.datepicker = {
  init: function (element, valueAccessor, allBindingsAccessor) {
    var options = allBindingsAccessor().datepickerOptions || {};
    $(element).datepicker(options);
  }
}

Das Binding dazu sieht wie folgt aus:

<input type="text" data-bind="datepicker: startDate, datepickerOptions: { numberOfMonths: 3 }" />

Ein Datepicker erscheint an dieser Stelle, allerdings fehlt noch ein wichtiger Punkt: Das lesen und schreiben des Observables. Der Datepicker bietet in seiner API ein getDate und setDate um anstelle der String mit Date-Werten arbeiten zu können. Als erster Schritt muss sich beim ändern des Datums, sprich beim change-Event des Datepickers der Wert des Observables gesetzt werden:

$(element).bind("change", function () {
  var observable = valueAccessor(),
      dateValue = $(element).datepicker("getDate");

  observable(dateValue);
});

In der update Funktion hingegen muss genau das Gegenteil passieren: Wenn sich der Wert des Observables ändert, muss auch der Datepicker aktualisiert werden:

update: function (element, valueAccessor, allBindingsAccessor) {
  var value = ko.utils.unwrapObservable(valueAccessor());

  // If the date is coming from a Microsoft webservice.
  if (typeof value === "string" && value.indexOf('/Date(') === 0) {
    value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
  }
  var currentDate = $(element).datepicker("getDate");

  // Check if the date has changed.
  if (value && value - currentDate !== 0) {
    $(element).datepicker("setDate", value);
  }
}

Hier werden gleich zwei Fliegen mit einer Klatsche geschlagen: Wird für das Datum direkt ein .NET Webservice konsumiert, dann liefert dieser das Datum in folgendem Format zurück: Date(1308787200000). Dies bringt auf der clientseitge nur wenig und muss mit der oben stehenden Funktion manuell geparst werden. Außerdem wird das Datum auf Änderungen überprüft und nur dann neu gesetzt.

One/two-way-Bindings

Die gezeigte Möglichkeit funktioniert wenn es sich um ein two-way-Binding handelt, sprich wenn das gewünschte Datum ein observable und damit writeable ist. Wenn hingegen das Datum in einem Objekt gekapselt ist, zum Beispiel mit var range = {start: new Date(2013, 0, 1), end: new Date(2015, 11, 31)} dann reicht ein observable(dateValue) nicht mehr aus. Für diese one-way-Bindings muss ein kleiner Trick angewendet werden:

$(element).bind("change", function () {
  var observable = valueAccessor(),
      dateValue = $(element).datepicker("getDate");

  // Two-way-binding means a writeable observable.
  if (ko.isWriteableObservable(observable)) {
    observable(dateValue);
    return;
  }
  if (allBindingsAccessor()._ko_property_writers) {
    allBindingsAccessor()._ko_property_writers.datepicker(dateValue);
  }
});

Mit _ko_property_writers zu arbeiten ist sicherlich nicht die eleganteste Lösung, aber dafür erfordert diese keine unnötige Umstrukturierung der Daten.

Das vollständige Binding sieht wie folgt aus:

ko.bindingHandlers.datepicker = {
  init: function (element, valueAccessor, allBindingsAccessor) {
    // Get the options from the binding.
    var options = allBindingsAccessor().datepickerOptions || {};

    $(element)
      .datepicker(options)
      .bind("change", function () {
        ko.bindingHandlers.datepicker.updateValue(element, valueAccessor, allBindingsAccessor);
      });

    ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
      $(element).datepicker("destroy");
    });
  },
  update: function (element, valueAccessor, allBindingsAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());

    // If the date is coming from a Microsoft webservice.
    if (typeof value === "string" && value.indexOf('/Date(') === 0) {
      value = new Date(parseInt(value.replace(/\/Date\((.*?)\)\//gi, "$1")));
    }
    var currentDate = $(element).datepicker("getDate");

    // Check if the date has changed.
    if (value && value - currentDate !== 0) {
      $(element).datepicker("setDate", value);
    }
  },
  updateValue: function (element, valueAccessor, allBindingsAccessor) {
    var observable = valueAccessor(),
        dateValue = $(element).datepicker("getDate");

    // Two-way-binding means a writeable observable.
    if (ko.isWriteableObservable(observable)) {
      observable(dateValue);
      return;
    }
    if (allBindingsAccessor()._ko_property_writers) {
      allBindingsAccessor()._ko_property_writers.datepicker(dateValue);
    }
  }
};

Das Projekt sowie eine Demo dazu finden sich auf Github

Knockout – Custom Bindings & jQuery Datepicker: 1 Stern2 Sterne3 Sterne4 Sterne5 Sterne 4,43 von 5 Punkte, 7 abgegebene Stimmen.
Loading ... Loading ...

About the Author

Roberto Bez ist passionierter Webentwickler und TechLead bei der HolidayCheck AG. Für Roberto bedeutet das Entwickeln nicht nur Arbeit, sondern auch Freude, Motivation und täglich neue, aufregende Herausforderungen. Besonders gerne setzt er sich mit neuen Webtechnologien sowie Datenbanken aller Art auseinander und versucht diese in die tägliche Anwendungsentwicklung miteinzubringen. Neben dem Entwickeln trifft man ihn gerne Abends beim Laufen oder im Sommer bei Mountainbike-Touren durch die schönen Berge Südtirols.