Advanced Knockout.js
Beyond Simple Observables
Beyond Simple Observables
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:
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:
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:
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 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')]);
}
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')]);
}
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');
}
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…' }
]);
}
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'));
};
For more information, samples, tutorials, and for great documentation, check out Knockout's site.
You can also download the source to this presentation.