SlideShare une entreprise Scribd logo
1  sur  30
Télécharger pour lire hors ligne
Wartbare Web-Anwendungen
mit Knockout.js und
Model-View-ViewModel (MVVM)



Malte Clasen
Problem: Rezepteditor
Problem: Rezepteditor
Acceptance Tests
    Feature: RecipeEditor
      In order to edit a recipe
      As a registered user
      I want to be able to modify all properties of a recipe


    Scenario: Add Category
      Given I am on the "Streuselkuchen" recipe page
      And I am in edit mode
      When I click on the empty category field
      And I type "Blechkuchen"
      Then there should be 3 categories
      And the title of the third category should be "Blechkuchen"

4
HTML-Ansicht   <h1>Streuselkuchen</h1>
               <img class="photo" alt="recipe"                 <h3>Kategorien</h3>
               src="/Content/Images/streuselkuchen.jpg">       <ul>
               <div>Foto von Malte</div>                            <li><a href="/KuchenUndTorten">
                                                                    Kuchen und Torten</a></li>
               <h2>Zubereitung</h2>                                 <li>Kleingebäck</li>
                                                               </ul>
               <h3>Teig</h3>
                                                               <h2>Zutaten</h2>
               <div>Mehl in eine Schüssel geben und eine
               Kuhle hineindrücken. Zucker, Hefe und           <h3>Teig</h3>
               <emph>lauwarmen</emph>                          <table>
               Soja-Reis-Drink hineingeben. Mit einem          <tbody>
               Küchentuch zudecken und 15 Minuten lang         <tr class="ingredient">
               gehen lassen. Margarine zerlassen und                 <td class="amount">400</td>
               ebenfalls in die Kuhle geben. Von innenheraus         <td class="unit">g</td>
               mit dem restlichen Mehl verkneten. Nochmals           <td class="name"><a href="/Mehl">
               15 Minuten lang zugedeckt gehen lassen. Auf           Mehl</a></td>
               einem mit Backpapier ausgelegten Blech          </tr>
               ausrollen und wieder 15 Minuten lang gehen      …
               lassen.</div>                                   </tbody>
                                                               </table>
               <h3>Streusel</h3>
                                                               <h3>Streusel</h3>
               <div>Mehl, Zucker und Zimt vermischen,          <table>…</table>
               Margarine zerlassen und alles verkneten.
               Streusel auf dem ausgerollten, gegangenen
               Teig <strong>gleichmäßig</strong> verteilen
               und ca. 20 Minuten bei 180°C Umluft
               backen.</div>
ASP.NET-Editor
                                     zwei Code-
     veränderbare        „neue
                                       Pfade,
    Anzahl in Listen   Kategorie“
                                    Postback, …




6
Knockout: Prinzip
      JSON

                        ViewModel




             Template



                                    HTML




7
JSON
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Get()
    {
      return Json(_model, JsonRequestBehavior.AllowGet);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(RecipeModel model)
    {
      return Json("gespeichert um " +
             DateTime.Now.ToShortTimeString());
    }

8
ViewModel
    function RecipeViewModel(initialData) {
            var self = this;
            self.data = ko.observable();
            …
            this.onSave = function () { … };
            this.onRemoveComponent = function (model, event) {
                     self.data().Components.remove(model);
            }
            …
             var mapping = { … };
            self.data(ko.mapping.fromJS(initialData, mapping));
    };

9
Observable
     function QuantityModel(data) {
             this.Amount = ko.observable(data.Amount);
             this.Unit = ko.observable(data.Unit);
     }




10
ObservableArray
     function ComponentModel(data) {
       var self = this;
       this.Title = ko.observable(data.Title);
       this.Preparation = ko.observable(data.Preparation);

         this.Ingredients = ko.observableArray();
         this.Ingredients($.map(data.Ingredients, function (item)
               { return new RecipeIngredientModel(item); }));
         …
     }




11
Binding: Text
     <h1 data-bind="text: Title"></h1>

     ergibt zur Laufzeit

     <h1 data-bind="text: Title">Streuselkuchen</h1>




12
Binding: With
     function RecipeViewModel(initialData) {
             var self = this;
             self.data = ko.observable();
             …
     };
     …
     ko.applyBindings(recipeViewModel);

     <form action="#" id="RecipeEditor" data-bind="with: data">
            <h1 data-bind="text: Title"></h1>
            …
     </form>

13
Binding: Attr
     <ul data-bind="foreach: Categories">
            <li><a data-bind="text:Name, attr:
     {href:Url}"></a></li>
     </ul>




14
Virtual Elements
     <img class="photo" alt="recipe" data-bind="attr: {src:
     Image.Url}" />
     <div>
       Foto von
       <!--ko text: Image.Author-->
       <!--/ko-->
     </div>




15
Binding: Foreach
     <ul data-bind="foreach: Categories">
       <li><a data-bind="text:Name, attr: {href:Url}"></a></li>
     </ul>




16
Binding: If
     <ul data-bind="foreach: Categories">
       <!-- ko if:!IsEmpty() -->
       <li><a data-bind="text:Name, attr: {href:Url}"></a></li>
       <!-- /ko -->
     </ul>
     function CategoryModel(data) {
       var self = this;
       this.Name = ko.observable(data.Name);
       this.Url = ko.observable(data.Url);
       this.IsEmpty = ko.computed(function () {
             return (!self.Name()) && (!self.Url());
       }, this);
     }
17
Mapping, manuell
     function ComponentModel(data) {
             var self = this;
             this.Title = ko.observable(data.Title);
             this.Preparation = ko.observable(data.Preparation);
             this.Ingredients = ko.observableArray();
             this.Ingredients($.map(data.Ingredients, function (item)
                      { return new RecipeIngredientModel(item); }));
     }




18
Mapping, automatisch
     function RecipeViewModel(initialData) {
     var self = this;
     self.data = ko.observable();
     var mapping = {
              'Components': {
                      create: function (options) {
                              return new
     ComponentModel(options.data);
                      }
              }
     };
     self.data(ko.mapping.fromJS(initialData, mapping));
     }
19
Mapping, zu JSON
     this.onSave = function () {
             $.ajax("/Recipe/Edit", {
                      data: ko.mapping.toJSON(self.data),
                      type: "post",
                      contentType: "application/json",
                      dataType: "json",
                      success: function (result) {
     ShowLogMessage(result); }
             })
             .error(function (e, jqxhr, settings, exception) {
                      console.log(exception);
             });
     };
20
HTML-Editor   <h1 contenteditable="true">Streuselkuchen</h1>
              <img class="photo" alt="recipe"                 <h3>Kategorien</h3>
              src="/Content/Images/streuselkuchen.jpg">       <ul>
              <div>Foto von Malte</div>                            <li><input type="text"></li>
                                                                   <li><input type="text"></li>
              <h2>Zubereitung</h2>                            </ul>

              <h3 contenteditable="true">Teig</h3>            <h2>Zutaten</h2>

              <div contenteditable="true">Mehl in eine        <div class="toolbar edit">
              Schüssel geben und eine Kuhle                         <button type="button">
              hineindrücken. Zucker, Hefe und                             <img src="/list-remove.png">
              <emph>lauwarmen</emph>                                </button>
              Soja-Reis-Drink hineingeben. Mit einem          </div>
              Küchentuch zudecken und 15 Minuten lang         <h3 contenteditable="true">Teig</h3>
              gehen lassen. Margarine zerlassen und           <table>
              ebenfalls in die Kuhle geben. Von innenheraus   <tbody>
              mit dem restlichen Mehl verkneten. Nochmals     <tr class="ingredient">
              15 Minuten lang zugedeckt gehen lassen. Auf           <td class="amount"><input
              einem mit Backpapier ausgelegten Blech                type="number"></td>
              ausrollen und wieder 15 Minuten lang gehen            <td class="unit"><input
              lassen.</div>                                         type=“text"></td></td>
                                                                    <td class="name"><input
              <h3 contenteditable="true">Streusel</h3>              type=“text"></td>
                                                              </tr>
              <div contenteditable="true">Mehl, Zucker        …
              und Zimt vermischen, Margarine zerlassen        </tbody>
              und alles verkneten. Streusel auf dem           </table>
              ausgerollten, gegangenen Teig
              <strong>gleichmäßig</strong> verteilen und      <h3 contenteditable="true">Streusel</h3>
              ca. 20 Minuten bei 180°C Umluft                 <table>…</table>
              backen.</div>
HTML-Editor
     <div contenteditable="true" data-bind="html: Preparation,
     event: {change: onUpdatePreparation}">Mehl in eine Schüssel
     geben …

     function ComponentModel(data) {
             var self = this;
             this.Preparation = ko.observable(data.Preparation);
             this.onUpdatePreparation = function(model, event) {
                      self.Preparation($(event.target).html());
             };
     }



22
Event: Click
     <button type="button" data-bind="event: { click:
     $root.onRemoveComponent }">
             <img alt="list-remove" src="list-remove.png" />
     </button>

     function RecipeViewModel(initialData) {
             var self = this;
             self.data = ko.observable();
             this.onRemoveComponent = function (model, event) {
                      self.data().Components.remove(model);
             }
     }

23
Binding: Value
     <ul data-bind="foreach: Categories">
            <li><input type="text" data-bind="value:Name"/></li>
     </ul>

     function CategoryModel(data) {
             var self = this;
             this.Name = ko.observable(data.Name);
     }




24
Event: Change
     <ul data-bind="foreach: Categories">
            <li><input type="text" data-bind="value:Name,
     event:{change: $root.onChangeCategory}" /></li>
     </ul>

     this.onChangeCategory = function (model, event) {
             if (model.Name()) {
                      …
             } else {
                      self.data().Categories.remove(model);
             }
     }

25
Computed
     function ComponentModel(data) {
              var self = this;
              this.Ingredients = ko.observableArray();
              this.LastIngredient = ko.computed(function () {
                       if (self.Ingredients().length === 0)
                                 return null;
                       return
     self.Ingredients()[self.Ingredients().length-1];
              }, this);
     }




26
Subscribe
     this.LastIngredientSubscription = null;
     this.LastIngredient.subscribe(function (newValue) {
               if (self.LastIngredientSubscription !== null) {
                          self.LastIngredientSubscription.dispose();
               }
               var currentLastIngredient = newValue;
               if (currentLastIngredient === null)
                          currentLastIngredient = self.AddEmptyIngredient();
               if (!currentLastIngredient.IsEmpty())
                          currentLastIngredient = self.AddEmptyIngredient();
               self.LastIngredientSubscription =
     currentLastIngredient.IsEmpty.subscribe(
                          function (newValue) { if (!newValue) {
     self.AddEmptyIngredient(); } });
     });
27
Rahmenwerk
     @Scripts.Render("~/Scripts/recipe")
     <script type="text/javascript">
             InitRecipeViewModel(@Html.Raw(Model.Json));
     </script>

     var recipeViewModel;
     function InitRecipeViewModel(initialData) {
             recipeViewModel = new RecipeViewModel(initialData);
     }
     function InitRecipeEditor() {
             ko.applyBindings(recipeViewModel);
             ShowRecipeEditor();
     }
28
Übersicht
     • Grundlage: HTML-Ansicht
     • Knockout:
        – Model: Service
        – View: Template
        – ViewModel: Script
     • Observable, ObservableArray, Compute, Subscribe
     • Binding
        – Inhalte: Text, Attr
        – Struktur: With, Foreach, If
        – Events: Click

29
Vielen Dank für Ihre
       Aufmerksamkeit.


info@adesso.de
www.adesso.de | malteclasen.de/blog

Contenu connexe

En vedette

En vedette (11)

CloudConf2011 Introduction to Google App Engine
CloudConf2011 Introduction to Google App EngineCloudConf2011 Introduction to Google App Engine
CloudConf2011 Introduction to Google App Engine
 
Agilität, Snapshots und Continuous Delivery
Agilität, Snapshots und Continuous DeliveryAgilität, Snapshots und Continuous Delivery
Agilität, Snapshots und Continuous Delivery
 
Continuous Delivery praktisch
Continuous Delivery praktischContinuous Delivery praktisch
Continuous Delivery praktisch
 
Was Sie über NoSQL Datenbanken wissen sollten!
Was Sie über NoSQL Datenbanken wissen sollten!Was Sie über NoSQL Datenbanken wissen sollten!
Was Sie über NoSQL Datenbanken wissen sollten!
 
A Business-Critical SharePoint Solution From adesso AG
A Business-CriticalSharePoint SolutionFrom adesso AGA Business-CriticalSharePoint SolutionFrom adesso AG
A Business-Critical SharePoint Solution From adesso AG
 
Wozu Portlets – reichen HTML5 und Rest nicht aus für moderne Portale?
Wozu Portlets – reichen HTML5 und Rest nicht aus für moderne Portale?Wozu Portlets – reichen HTML5 und Rest nicht aus für moderne Portale?
Wozu Portlets – reichen HTML5 und Rest nicht aus für moderne Portale?
 
Scala 4 Enterprise
Scala 4 EnterpriseScala 4 Enterprise
Scala 4 Enterprise
 
OOP 2013 NoSQL Suche
OOP 2013 NoSQL SucheOOP 2013 NoSQL Suche
OOP 2013 NoSQL Suche
 
SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP (Kurzversion)
SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP (Kurzversion)SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP (Kurzversion)
SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP (Kurzversion)
 
Getriebene Anwendungslandschaften
Getriebene AnwendungslandschaftenGetriebene Anwendungslandschaften
Getriebene Anwendungslandschaften
 
SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP
SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMPSNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP
SNMP Applied - Sicheres Anwendungs-Monitoring mit SNMP
 

Plus de adesso AG

Bedarfsorientiertes Qualitätsmanagement im Projektalltag
Bedarfsorientiertes Qualitätsmanagement im ProjektalltagBedarfsorientiertes Qualitätsmanagement im Projektalltag
Bedarfsorientiertes Qualitätsmanagement im Projektalltag
adesso AG
 
Cloud Computing - Technologie und Missbrauchspotentiale
Cloud Computing - Technologie und MissbrauchspotentialeCloud Computing - Technologie und Missbrauchspotentiale
Cloud Computing - Technologie und Missbrauchspotentiale
adesso AG
 
QS von IT-Consulting bis Software Development
QS von IT-Consulting bis Software DevelopmentQS von IT-Consulting bis Software Development
QS von IT-Consulting bis Software Development
adesso AG
 
Das Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssen
Das Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssenDas Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssen
Das Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssen
adesso AG
 
JSF Testing - Tools und Technics
JSF Testing - Tools und TechnicsJSF Testing - Tools und Technics
JSF Testing - Tools und Technics
adesso AG
 

Plus de adesso AG (17)

Mythos High Performance Teams
Mythos High Performance TeamsMythos High Performance Teams
Mythos High Performance Teams
 
Google App Engine JAX PaaS Parade 2013
Google App Engine JAX PaaS Parade 2013Google App Engine JAX PaaS Parade 2013
Google App Engine JAX PaaS Parade 2013
 
NoSQL in der Cloud - Why?
NoSQL in der Cloud -  Why?NoSQL in der Cloud -  Why?
NoSQL in der Cloud - Why?
 
Lean web architecture mit jsf 2.0, cdi & co.
Lean web architecture mit jsf 2.0, cdi & co.Lean web architecture mit jsf 2.0, cdi & co.
Lean web architecture mit jsf 2.0, cdi & co.
 
Schlanke Webarchitekturen nicht nur mit JSF 2 und CDI
Schlanke Webarchitekturen nicht nur mit JSF 2 und CDISchlanke Webarchitekturen nicht nur mit JSF 2 und CDI
Schlanke Webarchitekturen nicht nur mit JSF 2 und CDI
 
Agile Praktiken
Agile PraktikenAgile Praktiken
Agile Praktiken
 
Java und Cloud - nicht nur mit PaaS
Java und Cloud - nicht nur mit PaaS Java und Cloud - nicht nur mit PaaS
Java und Cloud - nicht nur mit PaaS
 
10 Hinweise für Architekten
10 Hinweise für Architekten10 Hinweise für Architekten
10 Hinweise für Architekten
 
HTML5 Storage
HTML5 StorageHTML5 Storage
HTML5 Storage
 
Google Dart
Google DartGoogle Dart
Google Dart
 
Java in the Cloud : PaaS Platforms in Comparison
Java in the Cloud : PaaS Platforms in ComparisonJava in the Cloud : PaaS Platforms in Comparison
Java in the Cloud : PaaS Platforms in Comparison
 
Bedarfsorientiertes Qualitätsmanagement im Projektalltag
Bedarfsorientiertes Qualitätsmanagement im ProjektalltagBedarfsorientiertes Qualitätsmanagement im Projektalltag
Bedarfsorientiertes Qualitätsmanagement im Projektalltag
 
Cloud Computing - Technologie und Missbrauchspotentiale
Cloud Computing - Technologie und MissbrauchspotentialeCloud Computing - Technologie und Missbrauchspotentiale
Cloud Computing - Technologie und Missbrauchspotentiale
 
QS von IT-Consulting bis Software Development
QS von IT-Consulting bis Software DevelopmentQS von IT-Consulting bis Software Development
QS von IT-Consulting bis Software Development
 
Vergleich des Scala Web-Frameworks Lift mit dem Java EE Programmiermodell
Vergleich des Scala Web-Frameworks Lift mit dem Java EE ProgrammiermodellVergleich des Scala Web-Frameworks Lift mit dem Java EE Programmiermodell
Vergleich des Scala Web-Frameworks Lift mit dem Java EE Programmiermodell
 
Das Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssen
Das Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssenDas Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssen
Das Stockholm-Syndrom ...oder warum wir alle glauben, Maven lieben zu müssen
 
JSF Testing - Tools und Technics
JSF Testing - Tools und TechnicsJSF Testing - Tools und Technics
JSF Testing - Tools und Technics
 

Wartbare Web-Anwendungen mit Knockout.js und Model-View-ViewModel (MVVM)

  • 1. Wartbare Web-Anwendungen mit Knockout.js und Model-View-ViewModel (MVVM) Malte Clasen
  • 4. Acceptance Tests Feature: RecipeEditor In order to edit a recipe As a registered user I want to be able to modify all properties of a recipe Scenario: Add Category Given I am on the "Streuselkuchen" recipe page And I am in edit mode When I click on the empty category field And I type "Blechkuchen" Then there should be 3 categories And the title of the third category should be "Blechkuchen" 4
  • 5. HTML-Ansicht <h1>Streuselkuchen</h1> <img class="photo" alt="recipe" <h3>Kategorien</h3> src="/Content/Images/streuselkuchen.jpg"> <ul> <div>Foto von Malte</div> <li><a href="/KuchenUndTorten"> Kuchen und Torten</a></li> <h2>Zubereitung</h2> <li>Kleingebäck</li> </ul> <h3>Teig</h3> <h2>Zutaten</h2> <div>Mehl in eine Schüssel geben und eine Kuhle hineindrücken. Zucker, Hefe und <h3>Teig</h3> <emph>lauwarmen</emph> <table> Soja-Reis-Drink hineingeben. Mit einem <tbody> Küchentuch zudecken und 15 Minuten lang <tr class="ingredient"> gehen lassen. Margarine zerlassen und <td class="amount">400</td> ebenfalls in die Kuhle geben. Von innenheraus <td class="unit">g</td> mit dem restlichen Mehl verkneten. Nochmals <td class="name"><a href="/Mehl"> 15 Minuten lang zugedeckt gehen lassen. Auf Mehl</a></td> einem mit Backpapier ausgelegten Blech </tr> ausrollen und wieder 15 Minuten lang gehen … lassen.</div> </tbody> </table> <h3>Streusel</h3> <h3>Streusel</h3> <div>Mehl, Zucker und Zimt vermischen, <table>…</table> Margarine zerlassen und alles verkneten. Streusel auf dem ausgerollten, gegangenen Teig <strong>gleichmäßig</strong> verteilen und ca. 20 Minuten bei 180°C Umluft backen.</div>
  • 6. ASP.NET-Editor zwei Code- veränderbare „neue Pfade, Anzahl in Listen Kategorie“ Postback, … 6
  • 7. Knockout: Prinzip JSON ViewModel Template HTML 7
  • 8. JSON [AcceptVerbs(HttpVerbs.Get)] public ActionResult Get() { return Json(_model, JsonRequestBehavior.AllowGet); } [AcceptVerbs(HttpVerbs.Post)] public ActionResult Edit(RecipeModel model) { return Json("gespeichert um " + DateTime.Now.ToShortTimeString()); } 8
  • 9. ViewModel function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); … this.onSave = function () { … }; this.onRemoveComponent = function (model, event) { self.data().Components.remove(model); } … var mapping = { … }; self.data(ko.mapping.fromJS(initialData, mapping)); }; 9
  • 10. Observable function QuantityModel(data) { this.Amount = ko.observable(data.Amount); this.Unit = ko.observable(data.Unit); } 10
  • 11. ObservableArray function ComponentModel(data) { var self = this; this.Title = ko.observable(data.Title); this.Preparation = ko.observable(data.Preparation); this.Ingredients = ko.observableArray(); this.Ingredients($.map(data.Ingredients, function (item) { return new RecipeIngredientModel(item); })); … } 11
  • 12. Binding: Text <h1 data-bind="text: Title"></h1> ergibt zur Laufzeit <h1 data-bind="text: Title">Streuselkuchen</h1> 12
  • 13. Binding: With function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); … }; … ko.applyBindings(recipeViewModel); <form action="#" id="RecipeEditor" data-bind="with: data"> <h1 data-bind="text: Title"></h1> … </form> 13
  • 14. Binding: Attr <ul data-bind="foreach: Categories"> <li><a data-bind="text:Name, attr: {href:Url}"></a></li> </ul> 14
  • 15. Virtual Elements <img class="photo" alt="recipe" data-bind="attr: {src: Image.Url}" /> <div> Foto von <!--ko text: Image.Author--> <!--/ko--> </div> 15
  • 16. Binding: Foreach <ul data-bind="foreach: Categories"> <li><a data-bind="text:Name, attr: {href:Url}"></a></li> </ul> 16
  • 17. Binding: If <ul data-bind="foreach: Categories"> <!-- ko if:!IsEmpty() --> <li><a data-bind="text:Name, attr: {href:Url}"></a></li> <!-- /ko --> </ul> function CategoryModel(data) { var self = this; this.Name = ko.observable(data.Name); this.Url = ko.observable(data.Url); this.IsEmpty = ko.computed(function () { return (!self.Name()) && (!self.Url()); }, this); } 17
  • 18. Mapping, manuell function ComponentModel(data) { var self = this; this.Title = ko.observable(data.Title); this.Preparation = ko.observable(data.Preparation); this.Ingredients = ko.observableArray(); this.Ingredients($.map(data.Ingredients, function (item) { return new RecipeIngredientModel(item); })); } 18
  • 19. Mapping, automatisch function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); var mapping = { 'Components': { create: function (options) { return new ComponentModel(options.data); } } }; self.data(ko.mapping.fromJS(initialData, mapping)); } 19
  • 20. Mapping, zu JSON this.onSave = function () { $.ajax("/Recipe/Edit", { data: ko.mapping.toJSON(self.data), type: "post", contentType: "application/json", dataType: "json", success: function (result) { ShowLogMessage(result); } }) .error(function (e, jqxhr, settings, exception) { console.log(exception); }); }; 20
  • 21. HTML-Editor <h1 contenteditable="true">Streuselkuchen</h1> <img class="photo" alt="recipe" <h3>Kategorien</h3> src="/Content/Images/streuselkuchen.jpg"> <ul> <div>Foto von Malte</div> <li><input type="text"></li> <li><input type="text"></li> <h2>Zubereitung</h2> </ul> <h3 contenteditable="true">Teig</h3> <h2>Zutaten</h2> <div contenteditable="true">Mehl in eine <div class="toolbar edit"> Schüssel geben und eine Kuhle <button type="button"> hineindrücken. Zucker, Hefe und <img src="/list-remove.png"> <emph>lauwarmen</emph> </button> Soja-Reis-Drink hineingeben. Mit einem </div> Küchentuch zudecken und 15 Minuten lang <h3 contenteditable="true">Teig</h3> gehen lassen. Margarine zerlassen und <table> ebenfalls in die Kuhle geben. Von innenheraus <tbody> mit dem restlichen Mehl verkneten. Nochmals <tr class="ingredient"> 15 Minuten lang zugedeckt gehen lassen. Auf <td class="amount"><input einem mit Backpapier ausgelegten Blech type="number"></td> ausrollen und wieder 15 Minuten lang gehen <td class="unit"><input lassen.</div> type=“text"></td></td> <td class="name"><input <h3 contenteditable="true">Streusel</h3> type=“text"></td> </tr> <div contenteditable="true">Mehl, Zucker … und Zimt vermischen, Margarine zerlassen </tbody> und alles verkneten. Streusel auf dem </table> ausgerollten, gegangenen Teig <strong>gleichmäßig</strong> verteilen und <h3 contenteditable="true">Streusel</h3> ca. 20 Minuten bei 180°C Umluft <table>…</table> backen.</div>
  • 22. HTML-Editor <div contenteditable="true" data-bind="html: Preparation, event: {change: onUpdatePreparation}">Mehl in eine Schüssel geben … function ComponentModel(data) { var self = this; this.Preparation = ko.observable(data.Preparation); this.onUpdatePreparation = function(model, event) { self.Preparation($(event.target).html()); }; } 22
  • 23. Event: Click <button type="button" data-bind="event: { click: $root.onRemoveComponent }"> <img alt="list-remove" src="list-remove.png" /> </button> function RecipeViewModel(initialData) { var self = this; self.data = ko.observable(); this.onRemoveComponent = function (model, event) { self.data().Components.remove(model); } } 23
  • 24. Binding: Value <ul data-bind="foreach: Categories"> <li><input type="text" data-bind="value:Name"/></li> </ul> function CategoryModel(data) { var self = this; this.Name = ko.observable(data.Name); } 24
  • 25. Event: Change <ul data-bind="foreach: Categories"> <li><input type="text" data-bind="value:Name, event:{change: $root.onChangeCategory}" /></li> </ul> this.onChangeCategory = function (model, event) { if (model.Name()) { … } else { self.data().Categories.remove(model); } } 25
  • 26. Computed function ComponentModel(data) { var self = this; this.Ingredients = ko.observableArray(); this.LastIngredient = ko.computed(function () { if (self.Ingredients().length === 0) return null; return self.Ingredients()[self.Ingredients().length-1]; }, this); } 26
  • 27. Subscribe this.LastIngredientSubscription = null; this.LastIngredient.subscribe(function (newValue) { if (self.LastIngredientSubscription !== null) { self.LastIngredientSubscription.dispose(); } var currentLastIngredient = newValue; if (currentLastIngredient === null) currentLastIngredient = self.AddEmptyIngredient(); if (!currentLastIngredient.IsEmpty()) currentLastIngredient = self.AddEmptyIngredient(); self.LastIngredientSubscription = currentLastIngredient.IsEmpty.subscribe( function (newValue) { if (!newValue) { self.AddEmptyIngredient(); } }); }); 27
  • 28. Rahmenwerk @Scripts.Render("~/Scripts/recipe") <script type="text/javascript"> InitRecipeViewModel(@Html.Raw(Model.Json)); </script> var recipeViewModel; function InitRecipeViewModel(initialData) { recipeViewModel = new RecipeViewModel(initialData); } function InitRecipeEditor() { ko.applyBindings(recipeViewModel); ShowRecipeEditor(); } 28
  • 29. Übersicht • Grundlage: HTML-Ansicht • Knockout: – Model: Service – View: Template – ViewModel: Script • Observable, ObservableArray, Compute, Subscribe • Binding – Inhalte: Text, Attr – Struktur: With, Foreach, If – Events: Click 29
  • 30. Vielen Dank für Ihre Aufmerksamkeit. info@adesso.de www.adesso.de | malteclasen.de/blog