Advanced Knockout.js

Beyond Simple Observables

Simple Observable Review

Knockout.js (KO) makes it very easy to create a view model in JavaScript and apply it to your HTML

HTML:

<p>First Name: <span data-bind="text: FirstName"></span></p>
<p>Last Name: <span data-bind="text: LastName"></span></p>

JavaScript:

function Slide001ViewModel() {
    this.FirstName = ko.observable("Bert");
    this.LastName = ko.observable("Bertington");
}

ko.applyBindings(new Slide001ViewModel());

Example:

First Name:

Last Name:

Simple Computed Observables

A computed observable is an observable based on other observables.

HTML:

<p>Full Name: <span data-bind="text: FullName"></span></p>

JavaScript:

function Slide002ViewModel() {
    this.FirstName = ko.observable("Bert");
    this.LastName = ko.observable("Bertington");
    this.FullName = ko.computed(function(){return this.FirstName() + " " + this.LastName();}, this);
}

ko.applyBindings(new Slide002ViewModel());

Example:

Full Name:

Advanced Computed Bindings

Computed bindings are also great for validation, since any dependent bindings cause it to be recalculated:

HTML:

<label for="slide003FirstName">First Name</label>
<input type="text" id="slide003FirstName" data-bind="value: FirstName, valueUpdate: 'afterkeydown', css: {errored: !FirstName()}" />
<div data-bind="text: FirstNameMessage"></div>
<label for="slide003LastName">Last Name</label>
<input type="text" id="slide003LastName" data-bind="value: LastName, valueUpdate: 'afterkeydown', css: {errored: !LastName()}" />
<div data-bind="text: LastNameMessage"></div>

JavaScript

function Slide003ViewModel() {
	var vm = this,
		firstNameDefaultMessage = 'First Name is required.',
		lastNameDefaultMessage = 'Last Name is required.';

    vm.FirstName = ko.observable();
    vm.FirstNameMessage = ko.observable(firstNameDefaultMessage);
    vm.LastName = ko.observable();
    vm.LastNameMessage = ko.observable(lastNameDefaultMessage);
    
	vm.IsValid = ko.computed(function(){
		var valid = true;
		
		if(!vm.FirstName()){
			vm.FirstNameMessage(firstNameDefaultMessage);
			valid = false;
		}
		else {
			vm.FirstNameMessage('Thanks!');
		}
		
		if(!vm.LastName()){
			vm.LastNameMessage(lastNameDefaultMessage);
			valid = false;
		}
		else {
			vm.LastNameMessage('Thanks!');
		}
		
		return valid;
	}, vm);
}

Example:

Sample Form

Custom Subscriptions

A custom subscription allows you to capture an observable as it's changing. Paired with a setTimeout pattern, it can be used to "slow down" the observable and process after a larger amount of input.

HTML:

<form>
	<div>
		<label for="SearchText">Search:</label>
		<input type="text" id="Slide004SearchText" data-bind="value: Search, valueUpdate: 'afterkeydown', css: {errored: !SearchResults()}" />
	</div>
	<div>
		<label for="Results">Results:</label>
		<textarea id="Slide004SearchResults" data-bind="text: SearchResults"></textarea>
	</div>
</form>

JavaScript:

function Slide004ViewModel() {
	var vm = this, searchTimeout;
	
	vm.SearchResults = ko.observable();
	
	vm.Search = ko.observable();
	vm.Search.subscribe(function(newValue){
		clearTimeout(searchTimeout);
		
		searchTimeout = setTimeout(function () {
			
		    var req = new XMLHttpRequest();
		    
		    req.onreadystatechange = function() {
		    	if (req.readyState === 4 && req.status === 200) {
		    		vm.SearchResults(req.responseText);
		    	}
		    }
		    req.open('GET', 'https://www.googleapis.com/customsearch/v1?key=AIzaSyCoVUVyHJF3sYDokH-MbsezwmGCu23J6-s&cx=017576662512468239146:omuauf_lfve&q=' + newValue, true);
		    req.send();
		    
		}, 800);
	}, vm);
}

Please use conservatively; using the Google API with a limited number of queries.

Named Templates

Named templates allow you to reuse a section of KO-enhanced HTML with multiple observables.

HTML:

<script type="text/html" id="product-template">
	<li><strong><span data-bind="text: ProductName"></span>:</strong>
	<span data-bind="text: Description"></span></li>
</script>
<div class="productList">
	<h3>Computers</h3>
	<ul data-bind="template: {name: 'product-template', foreach: Computers}"></ul>
</div>
<div class="productList">
	<h3>Phones</h3>
	<ul data-bind="template: {name: 'product-template', foreach: Phones}"></ul>
</div>

JavaScript:

function Slide005And006ViewModel() {
	var vm = this;
	
	function ProductViewModel(productName, description) {
		var pvm = this;
		
		pvm.ProductName = ko.observable(productName);
		pvm.Description = ko.observable(description);
	}
	
	vm.Computers = ko.observableArray([new ProductViewModel('iMac', 'All-in-One Computer'), new ProductViewModel('Latitude', 'Dell Laptop'), new ProductViewModel('Surface', 'Microsoft Tablet')]);
	vm.Phones = ko.observableArray([new ProductViewModel('iPhone', 'Awesome'), new ProductViewModel('Lumia', 'Not Bad'), new ProductViewModel('Anything Android', 'Crap')]);
}

Computers

    Phones

      Virtual Templates

      With KO Templates, you're able to bind a container to an observableArray and a named template. However, you can also bind a "virtual" container to an observableArray to rid of any extra markup.

      HTML:

      <ul>
      	<!-- ko template: {name: 'product-template', foreach: Computers} --><!-- /ko -->
      	<!-- ko template: {name: 'product-template', foreach: Phones} --><!-- /ko -->
      </ul>

      JavaScript:

      function Slide005And006ViewModel() {
      	var vm = this;
      	
      	function ProductViewModel(productName, description) {
      		var pvm = this;
      		
      		pvm.ProductName = ko.observable(productName);
      		pvm.Description = ko.observable(description);
      	}
      	
      	vm.Computers = ko.observableArray([new ProductViewModel('iMac', 'All-in-One Computer'), new ProductViewModel('Latitude', 'Dell Laptop'), new ProductViewModel('Surface', 'Microsoft Tablet')]);
      	vm.Phones = ko.observableArray([new ProductViewModel('iPhone', 'Awesome'), new ProductViewModel('Lumia', 'Not Bad'), new ProductViewModel('Anything Android', 'Crap')]);
      }

      Nested Templates

      KO templates can be nested within each other (named and non-named). Very helpful when your view model becomes more complex and has child elements.

      HTML:

      <script type="text/html" id="pets-template">
      	<li data-bind="text: Pet"></li>
      </script>
      
      <script type="text/html" id="phonenumbers-template">
      	<a data-bind="text: PhoneNumber.FormattedNumber, attr: {href: PhoneNumber.UrlPhoneNumber }"></a><br />
      </script>
      
      <h3 data-bind="text: (FirstName() + ' ' + LastName())"></h3>
      <div class="details">
      	<h4>Contacts</h4>
      	<div data-bind="template: {name: 'phonenumbers-template', foreach: PhoneNumbers, as: 'PhoneNumber'}"></div>
      </div>
      <div class="details">
      	<h4>Pets</h4>
      	<ul data-bind="template: {name: 'pets-template', foreach: Pets, as: 'Pet'}"></ul>
      </div>

      JavaScript:

      function Slide007ViewModel() {
      	var vm = this;
      	
      	function PhoneNumberViewModel(phoneNumber, urlPhoneNumber) {
      		var pnvm = this;
      		
      		pnvm.FormattedNumber = ko.observable(phoneNumber);
      		pnvm.UrlPhoneNumber = ko.observable(urlPhoneNumber);
      	}
      	
      	vm.PhoneNumbers = ko.observableArray([new PhoneNumberViewModel('(123) 456-7654','11234567654'),new PhoneNumberViewModel('(321) 987-4568','13219874568'),new PhoneNumberViewModel('(231) 965-5321','12319655321')]);
      	vm.Pets = ko.observableArray([ko.observable('Rex'), ko.observable('Spot'), ko.observable('Bubbles')]);
      	
      	vm.FirstName = ko.observable('Test');
      	vm.LastName = ko.observable('User');
      }
      

      Contacts

      Pets

        Dynamically-Populated Observables

        KO gives you access to the raw View Model through its $data binding context. This allows you to pull in values that might not always exist on the view model.

        HTML:

        <script type="text/html" id="searchresults-template">
        	<div class="searchResult">
        		<h3 data-bind="text: $data.FirstName + ' ' + $data.LastName"></h3>
        		<p data-bind="if: $data.State"><strong>State:</strong> <span data-bind="text: $data.State"></span></p>
        		<p data-bind="if: $data.City"><strong>City:</strong> <span data-bind="text: $data.City"></span></p>
        		<p data-bind="if: $data.Description"><strong>Description:</strong> <span data-bind="text: $data.Description"></span></p>
        	</div>
        </script>
        <div class="searchResults" data-bind="template: {name: 'searchresults-template', foreach: SearchResults }"></div>

        JavaScript:

        function Slide008ViewModel() {
        	this.SearchResults = ko.observableArray(
        		[
        			{ FirstName: 'Brandon', LastName: 'Martinez', State: 'Michigan', City: 'Ludington' },
        			{ FirstName: 'Shawn', LastName: 'Riesterer', State: 'Wisconsin' },
        			{ FirstName: 'Jason', LastName: 'Young', Description: 'Must be around here somewhere…' }
        		]);
        }

        Multiple Binding Areas

        If you need to apply more than one View Model binding to a page, KO has support for this by targeting a parent container. For example, this entire presentation is done using multiple bindings.

        JavaScript:

        self.init = function()
        {
        	ko.applyBindings(new Slide001ViewModel(), document.getElementById('slide001'));
        	ko.applyBindings(new Slide002ViewModel(), document.getElementById('slide002'));
        	ko.applyBindings(new Slide003ViewModel(), document.getElementById('slide003'));
        	ko.applyBindings(new Slide004ViewModel(), document.getElementById('slide004'));
        	ko.applyBindings(new Slide005And006ViewModel(), document.getElementById('slide005'));
        	ko.applyBindings(new Slide005And006ViewModel(), document.getElementById('slide006'));
        	ko.applyBindings(new Slide007ViewModel(), document.getElementById('slide007'));
        	ko.applyBindings(new Slide008ViewModel(), document.getElementById('slide008'));
        };

        Thank You

        For more information, samples, tutorials, and for great documentation, check out Knockout's site.

        You can also download the source to this presentation.