Knockout.js – in Action – OK, what the hell is



Knockout.js – in Action – OK, what the hell is

0 0


knockout-in-action

Knockoutjs in Action slides

On Github dra1n / knockout-in-action

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

  <p>Oh no! This is <span data-bind="text: name"></span></p>
  <p>And he is <span data-bind="text: currentActivity"></span></p>
            
  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

  <div class="product">
    <img src="./images/consoles/xbox.png" alt="xbox">
    <div>
      <h3>Xbox One</h3>
      <p>
        Be first to experience Xbox One.
        The Day One Edition features a commemorative
        controller and an exclusive achievement.
      </p>
    </div>

    <div class="price">$499</div>
    <input name="product[xbox]" type="text" value="1">
    <span class="delete">
  </span></div>
  ...
  <div>
    <div class="total">
      Total: <span class="total-amount">$1198</span>
    </div>
    <div>
      <div class="checkout">
        <a href="#">Checkout</a>
      </div>
    </div>
  </div>
  

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);
    }
  }
          
  <a href="#" data-bind="click: $parent.delete" class="delete"></a>
          

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');
        });
      }
    }
            
    <a data-bind="click: checkout" href="#" class="btn">Checkout</a>

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