NexusCS

KnockoutJS

JavaScript
KnockoutJS is a standalone JavaScript MVVM library with declarative bindings, automatic UI refresh, dependency tracking, and templating.
mvvm
data-binding
observables

Getting started

Introduction

KnockoutJS simplifies dynamic UIs with automatic data binding and dependency tracking.

Latest version: 3.5.1

Installation

<!-- CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js"></script>
# npm
npm install knockout

# Bower
bower install knockout

Quick Example

<div>
  <p>First name: <input data-bind="value: firstName" /></p>
  <p>Last name: <input data-bind="value: lastName" /></p>
  <h2>Hello, <span data-bind="text: fullName"></span>!</h2>
</div>
function ViewModel() {
  var self = this;
  self.firstName = ko.observable("Bob");
  self.lastName = ko.observable("Smith");

  self.fullName = ko.pureComputed(function () {
    return self.firstName() + " " + self.lastName();
  });
}

ko.applyBindings(new ViewModel());

Apply Bindings

// Bind to entire document
ko.applyBindings(viewModel);

// Bind to specific element
ko.applyBindings(viewModel, document.getElementById("myElement"));

Observables

Creating Observables

var name = ko.observable("Bob");
var age = ko.observable(25);
var items = ko.observableArray();

Reading & Writing

// Read value (call as function)
var currentName = name();

// Write value (call with argument)
name("Mary");

// Chain writes
viewModel.firstName("Mary").lastName("Jones");

Subscribing to Changes

var subscription = name.subscribe(function (newValue) {
  console.log("New value: " + newValue);
});

// Subscribe to old value (before change)
name.subscribe(
  function (oldValue) {
    console.log("Old value: " + oldValue);
  },
  null,
  "beforeChange",
);

// Unsubscribe
subscription.dispose();

ko.when (3.5+)

// Callback-based
ko.when(
  function () {
    return myObservable() !== undefined;
  },
  function (result) {
    console.log("Predicate is now true");
  },
);

// Promise-based
ko.when(function () {
  return myObservable() !== undefined;
}).then(function (result) {
  console.log("Predicate is now true");
});

Observable Arrays

Creating Arrays

var arr = ko.observableArray();
var arr = ko.observableArray(["a", "b", "c"]);

Reading Arrays

arr().length; // Get length
arr()[0]; // Get item by index
arr.indexOf("a"); // Returns 0 or -1
arr.slice(1, 3); // Get slice copy

Mutating Methods

arr.push("d"); // Add to end
arr.pop(); // Remove from end
arr.unshift("z"); // Add to beginning
arr.shift(); // Remove from beginning

arr.reverse(); // Reverse in place
arr.sort(); // Alphabetical sort
arr.sort(function (a, b) {
  return a.price - b.price;
});

arr.splice(1, 2); // Remove 2 items from index 1

KO-Specific Methods

arr.replace(oldItem, newItem);

// Remove items
arr.remove(item);
arr.remove(function (item) {
  return item.age < 18;
});

// Remove all
arr.removeAll();
arr.removeAll(["a", "b"]);

// Destroy (sets _destroy: true)
arr.destroy(item);
arr.destroyAll();

Array Utilities

// Get sorted/reversed copies
arr.sorted();
arr.reversed();

// Track individual changes
arr.subscribe(
  function (changes) {
    // changes = [{ index, status, value, moved }]
    // status: 'added' or 'deleted'
  },
  null,
  "arrayChange",
);

// Type checking
ko.isObservableArray(arr); // true

Computed Observables

Basic Computed

function ViewModel() {
  var self = this;
  self.firstName = ko.observable("Bob");
  self.lastName = ko.observable("Smith");

  self.fullName = ko.computed(function () {
    return self.firstName() + " " + self.lastName();
  });
}

Pure Computed (Preferred)

// Prevents memory leaks, reduces overhead
this.fullName = ko.pureComputed(function () {
  return this.firstName() + " " + this.lastName();
}, this);

// Alternative syntax
this.fullName = ko.computed(
  function () {
    return this.firstName() + " " + this.lastName();
  },
  this,
  { pure: true },
);

Pure computeds auto-dispose when no subscribers, don't recalculate when sleeping, and should NOT have side effects.

Writable Computed

this.fullName = ko.pureComputed({
  read: function () {
    return this.firstName() + " " + this.lastName();
  },
  write: function (value) {
    var parts = value.split(" ");
    this.firstName(parts[0]);
    this.lastName(parts[1]);
  },
  owner: this,
});

Value Converter Example

this.formattedPrice = ko.pureComputed({
  read: function () {
    return "$" + this.price().toFixed(2);
  },
  write: function (value) {
    var num = parseFloat(value.replace(/[^\.\d]/g, ""));
    this.price(isNaN(num) ? 0 : num);
  },
  owner: this,
});

Type Checking Utilities

Observable Type Checks

ko.isObservable(obj); // Any observable
ko.isWritableObservable(obj); // Writable
ko.isComputed(obj); // Computed
ko.isPureComputed(obj); // Pure computed
ko.isObservableArray(obj); // Observable array

Extenders

Built-in Extenders

// Always notify (even if unchanged)
myObservable.extend({ notify: "always" });

// Rate limit notifications
myObservable.extend({ rateLimit: 50 });

// Rate limit with options
myObservable.extend({
  rateLimit: {
    timeout: 500,
    method: "notifyWhenChangesStop",
  },
});

Custom Extender — Log Changes

ko.extenders.logChange = function (target, option) {
  target.subscribe(function (newValue) {
    console.log(option + ": " + newValue);
  });
  return target;
};

// Use it
this.name = ko.observable("Bob").extend({ logChange: "name" });

Numeric Extender

ko.extenders.numeric = function (target, precision) {
  var result = ko
    .pureComputed({
      read: target,
      write: function (newValue) {
        var pow = Math.pow(10, precision);
        var rounded = Math.round(+newValue * pow) / pow;
        target(isNaN(rounded) ? 0 : rounded);
      },
    })
    .extend({ notify: "always" });

  result(target());
  return result;
};

// Usage
this.price = ko.observable(25.99).extend({ numeric: 2 });

Validation Extender

ko.extenders.required = function (target, message) {
  target.hasError = ko.observable();
  target.validationMessage = ko.observable();

  function validate(newValue) {
    target.hasError(newValue ? false : true);
    target.validationMessage(newValue ? "" : message || "Required");
  }

  validate(target());
  target.subscribe(validate);
  return target;
};

// Usage
this.name = ko.observable().extend({ required: "Enter name" });

Multiple Extenders

this.name = ko.observable("Bob").extend({
  required: "Enter name",
  logChange: "name",
});

Binding Syntax

Basic Syntax

<!-- Single binding -->
<span data-bind="text: myMessage"></span>

<!-- Multiple bindings -->
<input data-bind="value: cellphone, enable: hasCellphone" />

<!-- Expressions -->
<span data-bind="text: price() > 50 ? 'expensive' : 'cheap'"></span>

Function Calls

<!-- Simple function -->
<button data-bind="click: handleClick">Click</button>

<!-- With parameters -->
<button
  data-bind="click: function(data) {
  myFunction('param', data)
}"
>
  Click
</button>

Multi-line Bindings

<select
  data-bind="
  options: countries,      /* Available items */
  optionsText: 'name',
  value: selectedCountry,
  optionsCaption: 'Choose...'
"
></select>

Text & Appearance Bindings

Text Content

<!-- text: Set text content -->
<span data-bind="text: myValue"></span>

<!-- html: Set innerHTML (XSS risk!) -->
<div data-bind="html: rawHtml"></div>

The html binding is an XSS risk. Never use with untrusted content.

Visibility

<!-- visible -->
<div data-bind="visible: shouldShow"></div>

<!-- hidden -->
<div data-bind="hidden: shouldHide"></div>

CSS Classes

<!-- css: Toggle classes -->
<div
  data-bind="css: {
  active: isActive,
  disabled: isDisabled
}"
></div>

<!-- Dynamic class name -->
<div data-bind="css: dynamicClassName"></div>

Inline Styles

<div
  data-bind="style: {
  color: currentColor,
  fontWeight: isBold() ? 'bold' : 'normal'
}"
></div>

Attributes

<!-- attr: Set attributes -->
<a
  data-bind="attr: {
  href: url,
  title: details
}"
  >Link</a
>

<img data-bind="attr: { src: imageUrl }" />

Form Field Bindings

Click Binding

<!-- Simple click -->
<button data-bind="click: handleClick">Click</button>

<!-- With data and event -->
<button
  data-bind="click: function(data, event) {
  handleClick(data, event)
}"
>
  Click
</button>

<!-- Prevent bubbling -->
<button data-bind="click: handler, clickBubble: false">Click</button>

Event Binding

<div
  data-bind="event: {
  mouseover: showDetails,
  mouseout: hideDetails
}"
>
  Hover me
</div>

Submit Binding

<form data-bind="submit: doSubmit">
  <input data-bind="value: userName" />
  <button type="submit">Submit</button>
</form>

Enable / Disable

<input data-bind="enable: isEditable" />
<input data-bind="disable: !isEditable()" />

Value Binding

<!-- Basic value (updates on blur) -->
<input data-bind="value: userName" />

<!-- Update on keydown -->
<input data-bind="value: userName, valueUpdate: 'afterkeydown'" />

Text Input (Preferred)

<!-- Immediate updates (better than value) -->
<input data-bind="textInput: userName" />

textInput gives immediate updates; value waits for blur.

Focus Binding

<input data-bind="hasFocus: isEditing" />

Checked Binding

<!-- Checkbox -->
<input type="checkbox" data-bind="checked: isSelected" />

<!-- Radio buttons -->
<input
  type="radio"
  data-bind="checked: selectedOption, checkedValue: 'option1'"
/>
<input
  type="radio"
  data-bind="checked: selectedOption, checkedValue: 'option2'"
/>

Options Binding

<select
  data-bind="
  options: countries,
  optionsText: 'name',
  optionsValue: 'id',
  value: selectedCountry,
  optionsCaption: 'Choose...'
"
></select>

Selected Options (Multi-select)

<select
  data-bind="
  options: items,
  selectedOptions: chosenItems
"
  size="5"
  multiple="true"
></select>

Unique Name

<input data-bind="uniqueName: true" />

Control Flow Bindings

If / Ifnot

<!-- if: Conditionally render -->
<div data-bind="if: showSection">Content shown when true</div>

<!-- ifnot: Inverse condition -->
<div data-bind="ifnot: hideSection">Content shown when false</div>

With Binding

<!-- with: Change binding context -->
<div data-bind="with: selectedPerson">
  <span data-bind="text: name"></span>
  <span data-bind="text: $parent.title"></span>
</div>

with re-renders descendants on change. Use using for better performance.

Using Binding (3.5+)

<!-- using: Re-evaluate, don't re-render -->
<div data-bind="using: coords">
  <span data-bind="text: latitude"></span>
  <span data-bind="text: longitude"></span>
</div>

Let Binding

<!-- let: Add properties to context -->
<div
  data-bind="let: {
  fullName: firstName() + ' ' + lastName()
}"
>
  <span data-bind="text: fullName"></span>
</div>

Containerless Syntax

<ul>
  <li>Always visible</li>

  <!-- ko if: someCondition -->
  <li>Conditional item</li>
  <!-- /ko -->

  <!-- ko foreach: items -->
  <li data-bind="text: $data"></li>
  <!-- /ko -->
</ul>

Foreach Binding

Basic Foreach

<ul data-bind="foreach: items">
  <li data-bind="text: name"></li>
</ul>

Primitive Arrays

<ul data-bind="foreach: months">
  <li data-bind="text: $data"></li>
</ul>

With Index

<ul data-bind="foreach: items">
  <li>
    Item <span data-bind="text: $index"></span>:
    <span data-bind="text: name"></span>
  </li>
</ul>

Using Alias

<ul data-bind="foreach: { data: people, as: 'person' }">
  <li data-bind="text: person.name"></li>
</ul>

Nested Foreach

<ul data-bind="foreach: { data: categories, as: 'category' }">
  <li>
    <span data-bind="text: category.name"></span>
    <ul data-bind="foreach: { data: items, as: 'item' }">
      <li>
        <span data-bind="text: category.name"></span>:
        <span data-bind="text: item"></span>
      </li>
    </ul>
  </li>
</ul>

Animation Callbacks

<ul
  data-bind="foreach: {
  data: items,
  afterAdd: fadeIn,
  beforeRemove: fadeOut
}"
>
  <li data-bind="text: $data"></li>
</ul>
function fadeIn(element) {
  $(element).hide().fadeIn();
}

function fadeOut(element) {
  $(element).fadeOut(function () {
    $(element).remove();
  });
}

Foreach Callbacks

Callback When Called
afterRender(elements, data) After each render
afterAdd(node, index, data) When items added
beforeRemove(node, index, data) Before items removed*
beforeMove(node, index, data) Before items moved
afterMove(node, index, data) After items moved

*You must remove DOM in beforeRemove

Other Options

<!-- noChildContext: Keep parent context -->
<ul
  data-bind="foreach: {
  data: items,
  as: 'item',
  noChildContext: true
}"
>
  <li data-bind="text: item.name"></li>
</ul>

<!-- includeDestroyed: Hide destroyed items -->
<div
  data-bind="foreach: {
  data: items,
  includeDestroyed: false
}"
>
  ...
</div>

Binding Context

Context Properties

Property Description
$data Current item data
$parent Parent context data
$parents Array of all parent data
$parents[0] Same as $parent
$root Top-level view model
$component Closest component VM
$index Observable index (foreach)
$parentContext Parent binding context
$rawData Raw observable
$context Current binding context
$element Current DOM element
$componentTemplateNodes Nodes passed to component

Usage Examples

<!-- Access parent data -->
<div data-bind="with: selectedItem">
  <span data-bind="text: name"></span>
  <span data-bind="text: $parent.title"></span>
  <span data-bind="text: $root.appName"></span>
</div>

<!-- Access index in foreach -->
<ul data-bind="foreach: items">
  <li>
    <span data-bind="text: $index"></span>:
    <span data-bind="text: $data"></span>
  </li>
</ul>

Template Binding

Named Template

<div
  data-bind="template: {
  name: 'person-tmpl',
  data: buyer
}"
></div>

<script type="text/html" id="person-tmpl">
  <h3 data-bind="text: name"></h3>
  <p data-bind="text: email"></p>
</script>

Template with Foreach

<div
  data-bind="template: {
  name: 'item-tmpl',
  foreach: items
}"
></div>

<script type="text/html" id="item-tmpl">
  <div data-bind="text: name"></div>
</script>

Template with Alias

<ul
  data-bind="template: {
  name: 'month-tmpl',
  foreach: months,
  as: 'month'
}"
></ul>

Dynamic Template Selection

<div
  data-bind="template: {
  name: displayMode,
  foreach: items
}"
></div>
// displayMode can be observable or function
this.displayMode = ko.observable("active-tmpl");

// Or function
this.displayMode = function (item) {
  return item.active() ? "active-tmpl" : "inactive-tmpl";
};

Components

Register Component

ko.components.register("my-widget", {
  viewModel: function (params) {
    this.name = ko.observable(params.name || "World");
  },
  template: '<div>Hello, <span data-bind="text: name"></span>!</div>',
});

Using Components

<!-- component binding -->
<div data-bind="component: 'my-widget'"></div>

<div
  data-bind="component: {
  name: 'my-widget',
  params: { name: 'Ko' }
}"
></div>

<!-- Custom elements -->
<my-widget></my-widget>
<my-widget params="name: userName"></my-widget>

<!-- Containerless -->
<!-- ko component: "my-widget" --><!-- /ko -->

ViewModel Patterns

// Constructor function
ko.components.register("my-component", {
  viewModel: MyViewModel,
  template: { element: "my-template-id" },
});

// Shared instance
ko.components.register("my-component", {
  viewModel: { instance: sharedInstance },
  template: "...",
});

// Factory function
ko.components.register("my-component", {
  viewModel: {
    createViewModel: function (params, componentInfo) {
      return new MyViewModel(params);
    },
  },
  template: "...",
});

AMD Module Loading

// Separate modules
ko.components.register("my-component", {
  viewModel: { require: "path/to/viewmodel" },
  template: { require: "text!path/to/template.html" },
});

// Single module (viewModel + template)
ko.components.register("my-component", {
  require: "path/to/component",
});

Template-Only Component

ko.components.register("special-offer", {
  template: '<div class="offer" data-bind="text: productName"></div>',
});

Component Disposal

function MyViewModel() {
  this.subscription = someObs.subscribe(...);
  this.interval = setInterval(..., 1000);
}

MyViewModel.prototype.dispose = function() {
  this.subscription.dispose();
  clearInterval(this.interval);
};

Component dispose() is NOT called on page navigation — only on KO-managed removal.

Synchronous Loading

ko.components.register('my-component', {
  viewModel: ...,
  template: ...,
  synchronous: true
});

Custom Bindings

Binding Handler Structure

ko.bindingHandlers.myBinding = {
  init: function (
    element,
    valueAccessor,
    allBindings,
    viewModel,
    bindingContext,
  ) {
    // Called once when binding first applied
    var value = ko.unwrap(valueAccessor());

    // Get other binding values
    var otherVal = allBindings.get("otherBinding");
    var hasOther = allBindings.has("otherBinding");
  },

  update: function (
    element,
    valueAccessor,
    allBindings,
    viewModel,
    bindingContext,
  ) {
    // Called on first apply AND when dependencies change
    var value = ko.unwrap(valueAccessor());
  },
};

Example — slideVisible

ko.bindingHandlers.slideVisible = {
  init: function (element, valueAccessor) {
    var shouldShow = ko.unwrap(valueAccessor());
    $(element).toggle(shouldShow);
  },

  update: function (element, valueAccessor, allBindings) {
    var shouldShow = ko.unwrap(valueAccessor());
    var duration = allBindings.get("slideDuration") || 400;

    if (shouldShow) {
      $(element).slideDown(duration);
    } else {
      $(element).slideUp(duration);
    }
  },
};
<div data-bind="slideVisible: isVisible, slideDuration: 600">Content</div>

Virtual Element Support

ko.virtualElements.allowedBindings.myBinding = true;
<!-- ko myBinding: value -->
Content
<!-- /ko -->

Utilities

Convert to Plain JS

var plain = ko.toJS(viewModel); // Unwrap all observables
var json = ko.toJSON(viewModel); // JSON string
var json = ko.toJSON(viewModel, null, 2); // Pretty-printed

Unwrap Values

// Works for observables and plain values
var val = ko.unwrap(possiblyObservable);

// Equivalent to:
var val = ko.isObservable(x) ? x() : x;

Clean & Reapply Bindings

// Remove KO data from node
ko.cleanNode(element);

// Remove and reapply
ko.cleanNode(element);
ko.applyBindings(newViewModel, element);

Array Utilities

ko.utils.arrayForEach(array, function (item) {
  console.log(item);
});

var names = ko.utils.arrayMap(array, function (item) {
  return item.name;
});

var adults = ko.utils.arrayFilter(array, function (item) {
  return item.age > 18;
});

var result = ko.utils.arrayFirst(array, function (item) {
  return item.id === 5;
});

var index = ko.utils.arrayIndexOf(array, item);

Object Utilities

ko.utils.objectForEach(obj, function (key, value) {
  console.log(key + ": " + value);
});

Debugging

Show View Model as JSON

<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>

Debug Specific Value

<pre data-bind="text: JSON.stringify(ko.toJS(myObservable), null, 2)"></pre>

Console Logging

myObservable.subscribe(function (value) {
  console.log("New value:", value);
});

this.myComputed = ko.computed(function () {
  console.log("Evaluating...");
  return this.firstName() + " " + this.lastName();
}, this);

Gotchas

Observable Array Items

Observable arrays track membership, NOT item properties. Putting an object in an observableArray doesn't make its properties observable.

// Wrong: item properties not observable
var items = ko.observableArray([{ name: "Bob" }]);

// Right: make item properties observable
function Item(name) {
  this.name = ko.observable(name);
}
var items = ko.observableArray([new Item("Bob")]);

Array Method Syntax

Use KO methods directly, not on unwrapped array.

// Correct: notifies subscribers
arr.push("item");

// Wrong: does NOT notify subscribers
arr().push("item");

Pure Computed Side Effects

Pure computeds should NOT have side effects. The evaluator won't run when sleeping (no subscribers).

// Wrong: side effect in pure computed
this.fullName = ko.pureComputed(function () {
  console.log("Evaluated!"); // Won't run when sleeping
  return this.firstName() + " " + this.lastName();
}, this);

HTML Binding XSS

The html binding is an XSS risk. Never use with untrusted content.

<!-- Dangerous with user input -->
<div data-bind="html: userContent"></div>

<!-- Safe alternative -->
<div data-bind="text: userContent"></div>

With vs Using

with re-renders descendants; using only re-evaluates bindings.

<!-- Re-renders DOM on change -->
<div data-bind="with: selectedPerson">...</div>

<!-- Better performance: re-evaluates bindings only -->
<div data-bind="using: selectedPerson">...</div>

Value vs TextInput

textInput gives immediate updates; value waits for blur.

<!-- Updates on blur -->
<input data-bind="value: userName" />

<!-- Updates immediately (preferred) -->
<input data-bind="textInput: userName" />

Component Disposal

Component dispose() is NOT called on page navigation. Only on KO-managed removal.

// Only called when KO removes component
MyViewModel.prototype.dispose = function () {
  this.subscription.dispose();
};

beforeRemove Responsibility

beforeRemove makes YOU responsible for DOM removal. KO won't remove nodes.

function fadeOut(element) {
  $(element).fadeOut(function () {
    $(element).remove(); // YOU must remove it
  });
}

Also see