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
- KnockoutJS Documentation (knockoutjs.com)
- Observables (knockoutjs.com)
- Computed Observables (knockoutjs.com)
- Bindings (knockoutjs.com)
- Components (knockoutjs.com)
- Custom Bindings (knockoutjs.com)