Knockout.js Components — Reusable UI Widgets and Encapsulation
In this tutorial, you will learn about Knockout.js Components. We cover key concepts, practical examples, and best practices to help you master this topic.
Knockout.js components are self-contained units of UI that combine a ViewModel and a template into a reusable custom element, enabling modular application architecture.
What You'll Learn
- Registering components with
ko.components.register() - Defining component ViewModel and template
- Passing parameters to components
- Component lifecycle and disposal
- Loading components asynchronously
Why It Matters
As applications grow, repeating the same UI patterns (product cards, pagination controls, date pickers) in multiple places leads to duplication. Components encapsulate markup, logic, and styling in a single, reusable package.
Real-World Use
An e-commerce site with ProductCard, ReviewList, StarRating, AddToCartButton, and PriceDisplay components. Each component is developed and tested independently, then composed on category pages, search results, and product detail pages.
Component Architecture
flowchart TD
A[ko.components.register] --> B[Component Definition]
B --> C[ViewModel]
B --> D[Template]
C --> E[Observables & Computeds]
C --> F[Lifecycle Hooks]
D --> G[HTML Markup]
D --> H[Bindings]
E --> I[Custom Element]
F --> I
G --> I
H --> I
style B fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Registering a Basic Component
// Register a component with inline ViewModel and template
ko.components.register('star-rating', {
viewModel: function(params) {
var self = this;
self.rating = ko.observable(params.rating || 0);
self.maxRating = ko.observable(params.maxRating || 5);
self.stars = ko.pureComputed(function() {
var stars = [];
for (var i = 1; i <= self.maxRating(); i++) {
stars.push({
filled: i <= self.rating(),
index: i
});
}
return stars;
});
self.setRating = function(star) {
self.rating(star.index);
};
},
template: '<div class="star-rating">\
<span data-bind="foreach: stars">\
<span data-bind="text: filled ? \'★\' : \'☆\',\
click: $parent.setRating,\
css: { filled: filled }"></span>\
</span>\
<span data-bind="text: rating"></span> / <span data-bind="text: maxRating"></span>\
</div>'
});
Expected output: <star-rating params="rating: 3, maxRating: 5"></star-rating> renders 3 filled stars and 2 empty stars. Clicking a star updates the rating.
Using a Component in HTML
<!-- With static params -->
<star-rating params="rating: 4, maxRating: 5"></star-rating>
<!-- With observable params (two-way binding) -->
<star-rating params="rating: productRating, maxRating: 5"></star-rating>
<!-- Using component binding (alternative to custom element) -->
<div data-bind="component: { name: 'star-rating', params: { rating: 3, maxRating: 5 } }"></div>
Expected output: The component renders in place of the custom element tag. When productRating changes in the parent ViewModel, the component's rating parameter updates automatically.
Component with Separate Files
For larger components, use separate files:
// product-card.js
ko.components.register('product-card', {
viewModel: { require: 'components/product-card' },
template: { require: 'text!components/product-card.html' }
});
// components/product-card.js
define(['knockout'], function(ko) {
return function(params) {
this.product = params.product;
this.addToCart = function() {
params.cart.addItem(this.product);
};
};
});
// components/product-card.html
<div class="product-card">
<h3 data-bind="text: product.name"></h3>
<p data-bind="text: product.description"></p>
<span class="price" data-bind="text: '$' + product.price.toFixed(2)"></span>
<button data-bind="click: addToCart, enable: product.inStock">Add to Cart</button>
</div>
Component with AMD/RequireJS
ko.components.register('product-card', {
viewModel: { require: 'components/product-card' },
template: { require: 'text!components/product-card.html' }
});
For projects without a module loader, use string-based templates:
ko.components.register('simple-greeting', {
viewModel: function(params) {
this.name = ko.observable(params.name || 'World');
},
template: '<p>Hello, <span data-bind="text: name"></span>!</p>'
});
Passing Callbacks to Components
ko.components.register('confirm-button', {
viewModel: function(params) {
var self = this;
self.label = params.label || 'Confirm';
self.onConfirm = params.onConfirm; // Callback from parent
self.confirm = function() {
if (self.onConfirm && typeof self.onConfirm === 'function') {
self.onConfirm();
}
};
},
template: '<button data-bind="click: confirm, text: label"></button>'
});
// Parent usage
// <confirm-button params="label: 'Delete', onConfirm: $root.deleteItem"></confirm-button>
Expected output: The component calls the parent's deleteItem function when its button is clicked, effectively communicating upward without tight coupling.
Component Lifecycle and Disposal
Components have a lifecycle that Knockout manages automatically:
ko.components.register('timer-display', {
viewModel: function(params) {
var self = this;
self.elapsed = ko.observable(0);
self.interval = setInterval(function() {
self.elapsed(self.elapsed() + 1);
}, 1000);
// Cleanup: called when the component is removed from the DOM
self.dispose = function() {
clearInterval(self.interval);
console.log('Timer disposed');
};
},
template: '<span data-bind="text: elapsed"></span> seconds'
});
Expected output: The timer increments every second. When the component is removed (e.g., navigating away), dispose stops the interval, preventing memory leaks.
Async Component Loading
ko.components.register('heavy-component', {
viewModel: {
require: 'components/heavy-component',
// Knockout shows a placeholder while loading
createViewModel: function(params, componentInfo) {
componentInfo.element.innerHTML = 'Loading...';
}
},
template: { require: 'text!components/heavy-component.html' }
});
Component Communication Patterns
Components can communicate with parents through:
// 1. Observable parameters (reactive)
// Parent passes observable; component reads/writes it
// 2. Callback functions
// Parent passes a function; component calls it
// 3. Event aggregation
// Both subscribe to a shared event bus
var eventBus = new ko.subscribable();
ko.components.register('notification', {
viewModel: function() {
var self = this;
self.messages = ko.observableArray([]);
eventBus.subscribe(function(msg) {
self.messages.push(msg);
});
},
template: '<div data-bind="foreach: messages">\
<div class="notification" data-bind="text: $data"></div>\
</div>'
});
Common Mistakes
Not defining a dispose function - Components that create timers, subscriptions, or event listeners must clean up in
dispose(). Undisposed resources cause memory leaks.Overwriting params instead of reading them -
params.ratingis passed by reference for observables. Do not reassignparams— read its properties.Using component binding with custom elements incorrectly - Custom elements must have a hyphen in their name (per Web Components spec). Use
product-card, notproductCardorproductcard.Nesting components too deeply - Deep component trees are hard to debug. Limit nesting to 3-4 levels and use event aggregation for deep communication.
Not handling loading states for async components - Async component loading takes time. Show a loading indicator or placeholder to avoid flickering.
Practice Questions
- What is the naming convention for Knockout component custom elements?
- How do you pass an observable from a parent to a child component?
- What method should you define in a component's ViewModel for cleanup?
- How can a child component communicate an action back to its parent?
- What are the two parts of a Knockout component definition?
Challenge: Build a pagination component that accepts totalItems, pageSize, and currentPage parameters. It should render page numbers, Previous/Next buttons, and call a callback when the page changes. Use the component on a product list page.
FAQ
Mini Project
Build a contact-card component that displays name, phone, email, and avatar. Create a contact-list component that uses contact-card internally. Add a search input to filter contacts. Each component should have proper disposal.
What's Next
Components use templates internally. Learn how to create and manage templates directly for more advanced rendering scenarios.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro