JackOfTrades
JackOfTrades

Reputation: 11

Show/Hide div within a single row in a foreach loop - KnockoutJS

I've tried several ways of doing this with no success. Would love some advice!

Goal: I have a table where each row is an order, but where within that row, if changes need to be made, a div appears underneath (in red). This needs to show/hide when a button on that row is clicked/toggled (Button is: Make Changes)

Issue: I have all the buttons working apart from the make changes toggle. Tried the visible observable, but the closest I could get was toggling the div's visibility for the whole table, not per row.

//Class to represent a row in the table
         function orderDetail(order, orderChange) {
             var self = this;
             self.order = ko.observable(order);
         self.orderChange = ko.observable(orderChange);
         }
         
         //Overall viewmodel, plus initial state
         function FoodViewModel() {
             var self = this;
         
             self.foodTypes = [
                { foodType: "Please Select"},
            { foodType: "Veg"},
            { foodType: "Meat"}
             ];    
         
             self.orders = ko.observableArray([
                 new orderDetail(self.foodTypes[0], self.foodTypes[0])
             ]);
         
             // Add and remove rows
             self.addOrder = function() {
                 self.orders.push(new orderDetail(self.foodTypes[0], self.foodTypes[0]));
             }
             self.removeOrder = function(order) { self.orders.remove(order) }
        
         
         }

         ko.applyBindings(new FoodViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
         <thead>
            <tr>
               <th>Orders</th>
               <th></th>
           <th></th>
            </tr>
         </thead>
         <tbody data-bind="foreach: orders">
            <tr>
               <td>
        <div><select data-bind="options: $root.foodTypes, value: order, optionsText: 'foodType'" id="foodList"></select></div>
            <div><select data-bind="options: $root.foodTypes, optionsText: 'foodType', value: orderChange" id="foodListChange" style="color: red;"></select></div>
           </td>
               <td>
        <button class="button button2" >Make Changes</button>
           </td>
               <td>
        <button class="button button1" href="#" data-bind="click: $root.removeOrder">Remove</button>
           </td>
            </tr>
         </tbody>
      </table>
      <button data-bind="click: addOrder" class="button">Add Order</button>

Thanks in advance!

Upvotes: 1

Views: 423

Answers (1)

Tomalak
Tomalak

Reputation: 338208

If you want that the user interface reacts to something in Knockout, make an observable.

In this case you want to display part of the UI conditionally (apparently to toggle an edit mode), so let's create:

  • an observable editMode that is either true or false, to store the UI state
  • a function toggleEditMode that toggles between the two states, to bind it to the button
  • an if: editMode and an ifnot: editMode binding, to show different parts of the UI accordingly

function OrderDetail(params) {
  var self = this;
  params = params || {};
  self.order = ko.observable(params.order);
  self.orderChange = ko.observable(params.orderChange);
  self.editMode = ko.observable(true);
  self.buttonCaption = ko.pureComputed(function () {
    return self.editMode() ? "Done" : "Edit";
  });
  self.toggleEditMode = function () {
    self.editMode(!self.editMode());
  }
}

function OrderList(params) {
  var self = this;
  params = params || {};

  self.foodTypes = ko.observableArray(params.foodTypes);
  self.orders = ko.observableArray();

  self.addOrder = function(foodType) {
    self.orders.push(new OrderDetail());
  }
  self.removeOrder = function(order) {
    self.orders.remove(order);
  }
}

var vm = new OrderList({
  foodTypes: [
    {foodType: "Veg"},
    {foodType: "Meat"}
  ]
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
  <thead>
    <tr>
      <th style="width: 150px;">Orders</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody data-bind="foreach: orders">
    <tr>
      <td>
        <div data-bind="ifnot: editMode">
          <!-- ko with: order -->
          <span data-bind="text: foodType"></span>
          <!-- /ko -->
        </div>
        <div data-bind="if: editMode">
          <select data-bind="
            options: $root.foodTypes,
            value: order,
            optionsText: 'foodType',
            optionsCaption: 'Please select&hellip;'
           "></select>
        </div>
      </td>
      <td>
        <button class="button button2" data-bind="
          click: toggleEditMode,
          text: buttonCaption,
          enable: order
        "></button>
        <button class="button button1" href="#" data-bind="
          click: $root.removeOrder
        ">Remove</button>
      </td>
    </tr>
  </tbody>
</table>
<button data-bind="click: addOrder" class="button">Add Order</button>
<hr>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>

Notes

  • Don't make "Please Select" part of your food types. That's what the optionsCaption binding is for.
  • I've parameterized the viewmodels (see the params object). This will work better than hard-coding values or using long argument lists, especially if you want to use a mapping plugin later.
  • The "Done" button is disabled as long as no order is selected, via the enable: order binding, i.e. if the order property is empty, the enable binding will keep the button disabled.
  • The with: order binding serves a similar purpose. It will only display its contents when there actually is an order value to display. This will prevent rendering errors with incomplete OrderDetail instances.

Upvotes: 1

Related Questions