Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

How to Work with Lists and Collections of Knockout ?

4.81/5 (6 votes)
29 Jun 2013CPOL5 min read 30.3K  
How to Work with Lists and Collections of Knockout

Introduction

  • Very often, you'll want to generate repeating blocks of UI elements, especially when displaying lists where the user can add and remove elements
  • Knockout lets you do that easily, using observable Arrays and the foreach binding

What is Observable Arrays ?

  • If you want to detect and respond to changes on one object, you’d use observables
  • If you want to detect and respond to changes of a collection of things, use an observableArray
  • This is useful in many scenarios where you’re displaying or editing multiple values and need repeated sections of UI to appear and disappear as items are added and removed
e.g.
C#
var myObservableArray = ko.observableArray();// Initially an empty array
myObservableArray.push('Some value');// Adds the value and notifies observers

Important Note

  • An observableArray tracks which objects are in the array, not the state of those objects
  • Simply putting an object into an observableArray doesn’t make all of that object’s properties themselves observable
  • Of course, you can make those properties observable if you wish, but that’s an independent choice
  • An observableArray just tracks which objects it holds, and notifies listeners when objects are added or removed

How to Apply observableArray with Real world Application ? 

  • Here I have used Visual Studio 2012 and ASP.NET MVC 4 Web Application
  • Please Follow the inline comments for better understanding

CASE 1 : Displaying Reservation Data  

View's Code - Index.cshtml

HTML
<h2>Your Seat Reservations</h2>

<table>
    <thead>
        <tr>
            <th>Passenger Name</th>
            <th>Meal</th>
            <th>Amount ($)</th>
            <th></th>
        </tr>
    </thead>

    @*render a copy of seats child elements for each entry in the seats array*@
    <tbody data-bind="foreach: seats">
        <tr>
          <td data-bind="text: name"></td>
          <td data-bind="text: meal().mealName"></td>
          <td data-bind="text: meal().price"></td>
        </tr>
    </tbody>
</table> 

ViewModel (Javascript) Code - ko.list.js

C#
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this; 
    self.name = name;
    self.meal = ko.observable(initialMeal);
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;
    // Non-editable Meals data - would come from the server
    self.availableMeals = [
        { mealName: "Vegetarian Raw Meal", price: 10.52 },
        { mealName: "Vegetarian Vegan Meal", price: 34.95 },
        { mealName: "Fruit Platter Meal", price: 45.50 }
    ];
    // Editable data - seats Array
    self.seats = ko.observableArray([
        new SeatReservation("Sampath", self.availableMeals[0]),
        new SeatReservation("Lokuge", self.availableMeals[1])
    ]);
}
ko.applyBindings(new ReservationsViewModel());

Output

Important Points about above Code 

SeatReservation

  • a simple JavaScript class constructor that stores a passenger name with a meal selection

ReservationsViewModel, a ViewModel class

  • availableMeals - a JavaScript object providing meal data
  • seats - an array holding an initial collection of SeatReservation instances.Note that it's a ko.observableArray , which means it can automatically trigger UI updates whenever items are added or removed
meal Property
  • is an observable
  • It's important to invoke meal() as a function (to obtain its current value) before attempting to read sub-properties
  • In other words, write meal().price, not meal.price

CASE 2 : Adding items 

View's Code - Index.cshtml

@*... leave all the rest unchanged ...*@
<button data-bind="click: addSeat">Reserve Another Seat</button>

ViewModel (Javascript) Code - ko.list.js

C#
function ReservationsViewModel() {
     var self = this;
    // ... leave all the rest unchanged ...
    // add seats
    self.addSeat = function () {
        self.seats.push(new SeatReservation("Chaminda", self.availableMeals[2]));
    };
}

Output

Explanation about above Scenario 

  • Now when you click "Reserve Another Seat", the UI updates to match
  • This is because seats is an observable Array, so adding or removing items will trigger UI updates automatically
  • Note that adding a row does not involve regenerating the entire UI
  • For efficiency, Knockout tracks what has changed in your ViewModel, and performs a minimal set of DOM updates to match

CASE 3 : Edit items

View's Code - Index.cshtml

HTML
@*... leave all the rest unchanged ...*@
<tbody data-bind="foreach: seats">
  <tr>
    <td data-bind="text: name"></td>
    <td><select data-bind="options: $root.availableMeals, value: meal,
               optionsText: 'mealName'"></select></td>
    <td data-bind="text: meal().price"></td>
  </tr>
</tbody>

Output

Important Points about above Code

  • This code uses two new bindings, options and optionsText 
  • Which together control both the set of available items in a dropdown list, and which object property (in this case, mealName) is used to represent each item on screen
  • You can now select from the available meals, and doing so causes the corresponding row (only) to be refreshed to display that meal's price

CASE 4 : Formatting Prices 

View's Code - Index.cshtml

HTML
@*... leave all the rest unchanged ...*@ 
<tbody data-bind="foreach: seats">
  <tr>
   <td data-bind="text: name"></td>
   @*update the view to make use of the formattedPrice*@
   <td><select data-bind="options: $root.availableMeals, value: meal,
             optionsText: 'mealName'"></select></td>
   <td data-bind="text: formattedPrice"></td>
  </tr>
</tbody>

ViewModel (Javascript) Code - ko.list.js

C#
function SeatReservation(name, initialMeal) {
   var self = this;
  
 // ... leave all the rest unchanged ...
   
 self.formattedPrice = ko.computed(function () {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";
    });
 }

Output 

Important Points about above Code

  • We've got a nice object-oriented representation of our data
  • So we can trivially add extra properties and functions anywhere in the object graph
  • The SeatReservation class the ability to format its own price data using some custom logic
  • Since the formatted price will be computed based on the selected meal, we can represent it using ko.computed (so it will update automatically whenever the meal selection changes)

CASE 5 : Removing Items

View's Code - Index.cshtml

<tbody data-bind="foreach: seats">
   <tr>
       @*... leave all the rest unchanged ...*@
       <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
   </tr>
</tbody>

ViewModel (Javascript) Code - ko.list.js

C#
function ReservationsViewModel() {
    var self = this;
   // ... leave all the rest unchanged ...
   // remove seats
   self.removeSeat = function (seat) { self.seats.remove(seat); };
}

Output

Important Points about above Code

  • Note that the $root. prefix causes Knockout to look for a removeSeat handler on your top-level ViewModel, instead of on the SeatReservation instance being bound
  • That's a more convenient place to put removeSeat in this example
  • So,I have added a corresponding removeSeat function on root ViewModel class, That is ReservationsViewModel

CASE 6 : Displaying a Total Amount

View's Code - Index.cshtml

HTML
@*... leave all the rest unchanged ...*@
<h3 data-bind="visible: totalAmount() > 0">Total Amount: $
<span data-bind="text: totalAmount().toFixed(2)"></span>
</h3>

ViewModel (Javascript) Code - ko.list.js

JavaScript
function ReservationsViewModel() {
     var self = this;
    // ... leave all the rest unchanged ...
    // Computed Total amount
    self.totalAmount = ko.computed(function () {
        var total = 0;
        for (var i = 0; i < self.seats().length; i++)
            total += self.seats()[i].meal().price;
        return total;
    });
 }

Output

Important Points about above Code

  • I have defined the total as a computed property
  • It lets the framework, take care of knowing when to recalculate and refresh the display
  • The visible binding makes an element visible or invisible as your data changes (internally, it modifies the element's CSS display style)
  • In this case, we choose to show the "Total Amount" information only if it's greater than zero
  • You can use arbitrary JavaScript expressions inside declarative bindings
  • Here, we used totalAmount() > 0 and totalAmount().toFixed(2)
  • Internally, this actually defines a computed property to represent the output from that expression
  • It's just a very lightweight and convenient syntactical alternative
  • Again, notice that since seats and meal are both observables, we're invoking them as functions to read their current values (e.g. self.seats().length)
  • When you run the code, you'll see "Total Amount" appear and disappear as appropriate, and thanks to dependency tracking, it knows when to recalculate its own value
  • You don't need to put any code in your "add" or "remove" functions to force dependencies to update manually

CASE 7 : Display the Total Number of Seats being Reserved

View's Code - Index.cshtml

HTML
<h2>Your seat reservations (<span data-bind="text: seats().length"></span>)</h2>
@*... leave all the rest unchanged ...*@
<button data-bind="click: addSeat, enable: seats().length < 3">Reserve Another Seat</button>

Output

Important Points about above Code

  • For display the Total number of seats being reserved, you can implement that in just a single place
  • You don't have to write any extra code to make the seat count update when you add or remove items
  • Just update the <h2> as above on top of your View
  • Similarly, For a Limit on the number of seats you can reserve
  • You can make the UI represent that by using the enable binding
  • The button becomes disabled when the seat limit is reached
  • You don't have to write any code to re-enable it, when the user removes some seats
  • Because the expression will automatically be re-evaluated by Knockout when the associated data changes

Final Full Code

Index.cshtml

C#
<h2>Your seat reservations (<span data-bind="text: seats().length"></span>)</h2>
<table>
    <thead>
        <tr>
            <th>Passenger Name</th>
            <th>Meal</th>
            <th>Amount ($)</th>
            <th></th>
        </tr>
    </thead>
    @*render a copy of seats child elements for each entry in the seats array*@
    <tbody data-bind="foreach: seats">
        <tr>
          <td data-bind="text: name"></td>
    @*update the view to make use of the formatted Price*@
          <td>
           <select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>
          <td data-bind="text: formattedPrice"></td>
          <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
        </tr>
    </tbody>
</table>
<button data-bind="click: addSeat, enable: seats().length < 3">Reserve Another Seat</button>
<h3 data-bind="visible: totalAmount() > 0">Total Amount: $<span data-bind="text: totalAmount().toFixed(2)"></span></h3>

ko.list.js 

JavaScript
// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);
    self.formattedPrice = ko.computed(function () {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";
    });
}
// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;
    // Non-editable Meals data - would come from the server
    self.availableMeals = [
        { mealName: "Vegetarian Raw Meal", price: 10.52 },
        { mealName: "Vegetarian Vegan Meal", price: 34.95 },
        { mealName: "Fruit Platter Meal", price: 45.50 }
    ];
    // Editable data - seats Array
    self.seats = ko.observableArray([
        new SeatReservation("Sampath", self.availableMeals[0]),
        new SeatReservation("Lokuge", self.availableMeals[1])
    ]);
    // Computed Total amount
    self.totalAmount = ko.computed(function () {
        var total = 0;
        for (var i = 0; i < self.seats().length; i++)
            total += self.seats()[i].meal().price;
        return total;
    });
    // add seats
    self.addSeat = function () {
        self.seats.push(new SeatReservation("Chaminda", self.availableMeals[2]));
    };
    // remove seats
    self.removeSeat = function (seat) { self.seats.remove(seat); };
}
ko.applyBindings(new ReservationsViewModel());

That's It.You're Done. 

 Conclusion   

  • You saw that following the MVVM pattern makes it very simple to work withchangeable object graphs such as Arrays and Hierarchies
  • You update the underlying data, and the UI automatically updates in sync
  • So enjoy with this Great Framework 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)