Knockout.js

in Action

Bakuta Andrey

OK, what the hell is

Model-View-View Model (MVVM)

?

Model-shmodel


  {
    characters: [
      {
        name: "Kratos",
        description: "Total badass"
      },

      {
        name: "Nathan Drake",
        description: "Treasure hunter and fortune seeker"
      }
    ]
  }
            

View Model


  var ViewModel = {
    name: "Kratos",
    currentActivity: ko.observable("ripping off sombody's head")
  }
            

View


  

Oh no! This is

And he is


  ko.applyBindings(ViewModel);
              

Oh no! This is Kratos

And he is ripping off sombody's head

Let's solve some problem


TODO list

boring ...


Shopping Cart


  
xbox

Xbox One

Be first to experience Xbox One. The Day One Edition features a commemorative controller and an exclusive achievement.

$499
...
Total: $1198

Do it

Knockout

way

javascript:


  (function () {
    var data = [
      {
        title: 'Xbox One',
        description: 'Be first to experience Xbox One...',
        price: '$499',
        quantity: 1,
        img: './images/consoles/xbox.png'
      },
      ...
    ]

    function CheckoutViewModel(data) {
      ko.mapping.fromJS({ products: data }, {}, this);
    }

    $(function() {
      ko.applyBindings(new CheckoutViewModel(data));
    })
  })();
            

html:


  <!-- ko foreach: products -->
    <div class="product">
      <img data-bind="attr: { src: img, alt: title }" />
      <div>
        <h3 data-bind="text: title"></h3>
        <p data-bind="text: description"></p>
      </div>

      <div class="price" data-bind="text: price"></div>
      <input data-bind="value: quantity" type="text" />
      <span class="delete" />
    </div>
  <!-- /ko -->

  <div>
    <div class="total">
      Total: <span class="total-amount">$1198</span>
    </div>
    <div>
      <div class="checkout">
        <a href="#" class="btn">Checkout</a>
      </div>
    </div>
  </div>
  

Can we do better?


Sure

The problem


  var data = [
    {
      title: 'Xbox One',
      description: 'Be first to experience Xbox One...',
      price: '$499',
      quantity: 1,
      img: './images/consoles/xbox.png'
    },
    ...
  ]
            

Product ViewModel


  function ProductViewModel(data) {
    ko.mapping.fromJS(data, {}, this);

    this.formattedPrice = ko.computed(function() {
      return '$' + this.price();
    }, this);
  }
            

Mapping FTW!


  function CheckoutViewModel(data) {
    var mapping = {
      products: {
        create: function(options) { return new ProductViewModel(options.data); }
      }
    }

    ko.mapping.fromJS({ products: data }, mapping, this);
  }
            

What's in the view?


  <!-- ko foreach: products -->
    <div class="product">
     <img data-bind="attr: { src: img, alt: title }" />
      <div>
        <h3 data-bind="text: title"></h3>
        <p data-bind="text: description"></p>
      </div>

      <div class="price" data-bind="text: formattedPrice"></div>
      <input data-bind="value: quantity" type="text" />
      <span class="delete" />
    </div>
  <!-- /ko -->
  

The problem

Total is still static

Let's fix that

Little helper


  var formatMoney = function(value) {
    return '$' + value;
  }
            

Meet and Greet Computed, again


  function CheckoutViewModel(data) {
    ...

    this.total = ko.computed(function() {
      var total = 0;

      ko.utils.arrayForEach(this.products(), function(product) {
        total += product.price * product.quantity();
      });

      return formatMoney(total);
    }, this);
  }
            

What's in the view?


  <div>
    <div class="total">
      Total: <span data-bind="text: total" class="total-amount"></span>
    </div>
    <div>
      <div class="checkout">
        <a href="#" class="btn">Checkout</a>
      </div>
    </div>
  </div>
            

Product ViewModel Revised


   function ProductViewModel(data) {
     var mapping = {
       observe: ['quantity']
     }

     ko.mapping.fromJS(data, mapping, this);

     this.formattedPrice = ko.computed(function() {
       return formatMoney(this.price)
     }, this);
   }
            

OMG! OMG! OMG!

Specs changed!!!

Do it fast with jQuery

A place for my stuff


Have you noticed that their stuff is shit and your shit is stuff?
George Carlin

Put my stuff in data attributes


  var clearFormat = function(price) {
    return parseFloat(price.replace(/[^0-9-.]/g, ''));
  },

  formatMoney = function(price) {
    return '$' + price;
  },

  storePrices = function() {
    $('.price').each(function() {
      $(this).next('input').data('price', clearFormat($(this).text()));
    });
  };
            

Use stored price to calculate subtotal


  var calculateSubtotal = function(e) {
    var product = e.originalEvent.currentTarget,
        price = $('.price', product),
        subtotal = formatMoney($(this).data('price') * parseInt($(this).val()));

    price.html(subtotal);
  },

  calculateTotal = function() {
    var total = 0;
    $('input').each(function() {
      var $this = $(this);
      total += $this.data('price') * parseInt($this.val());
    });
    $('.total-amount').text(formatMoney(total));
  },

  $('.product').on('change', 'input', function(e) {
    calculateSubtotal.call(this, e);
    calculateTotal.call(this, e);
  });
            
Add method to Product ViewModel

    this.subtotal = ko.computed(function() {
      return formatMoney(this.price * this.quantity());
    }, this);
            
Fix view

    <div class="subtotal" data-bind="text: subtotal"></div>
            
And use it when calculating total

  this.total = ko.computed(function() {
    var total = 0;
    ko.utils.arrayForEach(this.products(), function(product) {
      total += product.subtotal();
    });
    return total;
  }, this)
            

Easy, right?

But we always can do better

Introducing Extenders


  ko.extenders.formatMoney = function(target) {
    target.formatMoney = ko.computed(function() {
      return '$' + ko.utils.unwrapObservable(this);
    }, target);

    return target;
  };
            

Seems cool

But how to use it?

Inside ViewModels


  function ProductViewModel(data) {
    ...
    this.subtotal = ko.computed(function() {
      return this.price * this.quantity();
    }, this).extend({ formatMoney: true });
  }

  function CheckoutViewModel(data) {
    ...
    this.total = ko.computed(function() {
      var total = 0;
      ko.utils.arrayForEach(this.products(), function(product) {
        total += product.subtotal();
      });
      return total;
    }, this).extend({ formatMoney: true });
  }
            

Inside Views


  <div class="total">
    Total: <span data-bind="text: total.formatMoney" class="total-amount"></span>
  </div>

  ...

  <div data-bind="text: subtotal.formatMoney" class="subtotal"></div>
            

Everyone likes destroying things

Let us allow one to do this

Easy peasy ...


  $('.product').on('click', '.delete', function(e) {
    e.preventDefault();
    $(e.originalEvent.currentTarget).remove();
    calculateTotal();
  });
          

... Lemon Squeezy


  function CheckoutViewModel(data) {
    ...
    this.delete = function(product) {
      this.products.remove(product);
    }
  }
          

  
          

Synchronize with server

Good ol' jQuery

Wait! What?


    function CheckoutViewModel(data) {
      ...
      this.checkout = function() {
        var mapping = { ignore: ['description', 'img', 'price'] };

        $.ajax({
          dataType: 'json',
          data: ko.mapping.toJSON(this, mapping),
          type: 'post'
        })
        .success(function() {
          alert('Thank you for your order');
        });
      }
    }
            
    Checkout

It is everywhere


    $('.checkout a').on('click', function(e) {
      e.preventDefault();

      $.ajax({
        dataType: 'json',
        data: JSON.stringify($('form').serializeArray()),
        type: 'post'
      })
      .success(function() {
        alert('Thank you for your order');
      });
    });
            

What else?

  • Control flow data bindings (foreach, if, ifnot)
  • Template binding
  • Custom bindings
  • Writable computed fields
  • Manual subscriptions

Sources of inspiration

Questions, anyone?