Knockout.js Control Flow — Conditional Rendering and List Iteration
In this tutorial, you will learn about Knockout.js Control Flow. We cover key concepts, practical examples, and best practices to help you master this topic.
Knockout.js control flow bindings manage DOM creation and destruction based on your ViewModel state, using foreach for lists, if/ifnot for conditions, and with for context switching.
What You'll Learn
- Using foreach to render lists with automatic updates
- Conditional rendering with if, ifnot, and visible
- Context switching with the with binding
- Understanding the let binding for local variables
- Performance characteristics of each approach
Why It Matters
Manual DOM manipulation for lists and conditionals is tedious and error-prone. Control flow bindings let you declare what should be rendered and let Knockout handle creating, updating, and removing DOM elements when data changes.
Real-World Use
An email client where the inbox list uses foreach, a loading spinner uses if, the selected email detail uses with, and empty-state messages use ifnot. Each binding handles a specific rendering concern declaratively.
Control Flow Decision Tree
flowchart TD
A[Need to render?] --> B{Type}
B -->|List| C[foreach binding]
B -->|Conditional| D{When?}
B -->|Single Object| E[with binding]
D -->|Show/Hide| F[visible binding]
D -->|Create/Remove DOM| G[if / ifnot binding]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Foreach: Rendering Lists
The foreach binding duplicates the element's content for each item in an array:
<ul data-bind="foreach: products">
<li>
<strong data-bind="text: name"></strong>
<span data-bind="text: price"></span>
<button data-bind="click: $parent.removeProduct">Remove</button>
</li>
</ul>
function Product(name, price) {
this.name = ko.observable(name);
this.price = ko.observable(price);
}
function ViewModel() {
var self = this;
self.products = ko.observableArray([
new Product('Laptop', 999),
new Product('Mouse', 25),
new Product('Keyboard', 75)
]);
self.removeProduct = function(product) {
self.products.remove(product);
};
}
Expected output: Three list items render. Clicking Remove calls removeProduct with the correct product, removing that item from the array and the DOM.
Foreach with $index and Context
Inside foreach, special context variables are available:
<ol data-bind="foreach: items">
<li>
<span data-bind="text: $index()"></span>. <!-- Zero-based index -->
<span data-bind="text: $data"></span> <!-- Current item value -->
<span data-bind="text: $parent.title"></span> <!-- Parent context -->
</li>
</ol>
For nested foreach loops, context variables stack:
<div data-bind="foreach: categories">
<h3 data-bind="text: name"></h3>
<ul data-bind="foreach: items">
<li>
<!-- $parents[1] = root, $parents[0] = parent (category) -->
<span data-bind="text: $parents[1].title"></span> -
<span data-bind="text: $data"></span>
</li>
</ul>
</div>
If Binding: Create or Destroy DOM
The if binding removes child elements from the DOM when the condition is falsy and recreates them when truthy:
<!-- If: Completely removes/creates the element -->
<div data-bind="if: isLoggedIn">
<p>Welcome back, <span data-bind="text: userName"></span>!</p>
<button data-bind="click: logout">Logout</button>
</div>
<div data-bind="ifnot: isLoggedIn">
<p>Please <a href="#/login">login</a> to continue.</p>
</div>
<!-- Visible: Only hides/shows (element stays in DOM) -->
<div data-bind="visible: showWarning" class="warning">
Your session will expire soon.
</div>
Expected output: When isLoggedIn is false, the welcome message and logout button do not exist in the DOM at all (not just hidden). When it becomes true, Knockout renders them.
If vs Visible: When to Use Which
| Scenario | Use | Reason |
|---|---|---|
| Tabs/accordions | visible | Keep content in DOM for instant switching |
| Login/logout UI | if | Sensitive content should not exist in DOM at all |
| Loading spinner | if | No need to keep hidden spinner element |
| Form validation errors | visible | Show/hide messages without losing input state |
| Large hidden sections | if | Save memory by not rendering hidden content |
With Binding: Context Switching
The with binding changes the binding context to a specific object:
<div data-bind="with: selectedProduct">
<h2 data-bind="text: name"></h2>
<p data-bind="text: description"></p>
<p>Price: <span data-bind="text: price"></span></p>
<button data-bind="click: $parent.addToCart">Add to Cart</button>
</div>
function ViewModel() {
var self = this;
self.products = ko.observableArray([...]);
self.selectedProduct = ko.observable(null);
self.selectProduct = function(product) {
self.selectedProduct(product);
};
self.addToCart = function() {
// 'this' is selectedProduct, not the ViewModel
var product = self.selectedProduct();
// Add to cart logic...
};
}
Expected output: When selectedProduct is null, the with block renders nothing. When selectProduct is called, the block appears with the product's properties bound directly.
Containerless Control Flow
Sometimes you need control flow without a wrapping element. Use comment-based syntax:
<ul>
<li>Static item</li>
<!-- ko foreach: dynamicItems -->
<li data-bind="text: $data"></li>
<!-- /ko -->
</ul>
<!-- ko if: showExtra -->
<p>Extra content that appears conditionally.</p>
<!-- /ko -->
<!-- ko with: getCurrentUser() -->
<span data-bind="text: fullName"></span>
<!-- /ko -->
Expected output: The <!-- ko --> and <!-- /ko --> comments act as virtual elements. The bindings work exactly the same way but without adding wrapper divs to the DOM.
Let Binding (Local Variables)
The let binding creates named local variables within a scope:
<!-- ko let: { total: cartTotal(), discount: calculateDiscount() } -->
<p>Subtotal: $<span data-bind="text: total.toFixed(2)"></span></p>
<p>Discount: -$<span data-bind="text: discount.toFixed(2)"></span></p>
<p>Total: $<span data-bind="text: (total - discount).toFixed(2)"></span></p>
<!-- /ko -->
Common Mistakes
Using visible when if is appropriate - Keeping logged-out content in the DOM with
visiblecan expose secure content via DevTools. Useiffor sensitive sections.Creating too many nested foreach loops - Deeply nested foreach blocks with large arrays hurt performance. Flatten data or use containerless syntax to reduce nesting.
Forgetting context changes with
with- Insidewith, the binding context changes. Access parent properties via$parentor$root.Not using containerless syntax - Adding wrapper divs just to use foreach or if clutters the DOM. Use
<!-- ko -->comments instead.Mutating arrays during foreach rendering - Removing items while iterating can cause unexpected behavior. Defer mutations or use techniques that work with observableArray.
Practice Questions
- What is the difference between
ifandvisiblebindings? - How do you access the outer ViewModel from inside a foreach loop?
- What are containerless control flow bindings and when would you use them?
- What happens to the DOM when the
withbinding's value changes from null to an object? - How do you define local variables in a Knockout template?
Challenge: Build a tabbed interface with three tabs (Details, Specs, Reviews). Use the if binding to show only the active tab's content and the foreach binding to render review items in the Reviews tab.
FAQ
Mini Project
Build a multi-step checkout form with four steps (Cart, Shipping, Payment, Confirmation). Use if bindings to show only the active step, foreach to render cart items, and with to bind the current step's data context.
What's Next
Bindings connect ViewModel to View. Now learn how to handle user interactions with event and form bindings for clicks, submissions, and input changes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro