Search code examples
knockout-3.0

knockout.js: how to make a dependent cascading dropdown unconditionally visible?


Getting started with knockout, I have been playing with the pattern found at http://knockoutjs.com/examples/cartEditor.html. I have cascading select menus where the second one's options depend on the state of the first -- no problem so far. But whatever I do, I haven't figured a way to change the out-of-the-box behavior whereby the second element is not visible -- not rendered, I would imagine -- until the first element has a true-ish value (except by taking out the optionsCaption and instead stuffing in an empty record at the top of my data -- more on that below.) The markup:

<div id="test" class="border">
    <div class="form-row form-group">
        <label class="col-form-label col-md-3  text-right pr-2">
            language
        </label>
        <div class="col-md-9">
            <select class="form-control" name="language"
            data-bind="options: roster,
                optionsText: 'language',
                optionsCaption: '',
                value: language">
            </select>
        </div>
    </div>
    <div class="form-row form-group">
        <label class="col-form-label col-md-3 text-right pr-2">
            interpreter
        </label>
        <div class="col-md-9" data-bind="with: language">
            <select class="form-control" name="interpreter"
            data-bind="options: interpreters,
                optionsText : 'name',
                optionsCaption: '',
                value: $parent.interpreter"
            </select>
        </div>
    </div>
</div>

Code:

function Thing() {
    var self = this;
    self.language = ko.observable();
    self.interpreter = ko.observable();
    self.language.subscribe(function() {
        self.interpreter(undefined);
   });
};
ko.applyBindings(new Thing());

my sample data:

roster = [
    {   "language": "Spanish",
        "interpreters": [
            {"name"  : "Paula"},
            {"name"  : "David"},
            {"name"  : "Humberto"},
            {"name"  : "Erika"},
            {"name"  : "Francisco"},
        ]
    },
    {"language":"Russian",
     "interpreters":[{"name":"Yana"},{"name":"Boris"}]
    },
    {"language":"Foochow",
     "interpreters":[{"name":"Lily"},{"name":"Patsy"}]
    },
    /* etc */

Now, I did figure out that I can hack around this and get the desired effect by putting

{ "language":"", "interpreters":[] }

at the front of my roster data structure, and that's what I guess I will do unless one of you cognoscenti can show me the more elegant way that I am overlooking.


Solution

  • After using both Knockout and Vuejs, I found Vuejs much easier to work with. Knockout is a bit out dated and no longer supported by any one or group.

    Having said that, here is how I addressed your issue. The comments here refer to the link you provided not your code so I could build my own test case.

    My working sample is at http://jsbin.com/gediwal/edit?js,console,output

    1. I removed the optionsCaption from both select boxes.
    2. Added the following item to the data (note that this has to be the first item in the arry): {"products":{"name":"Waiting", "price":0}, "name":"Select..."},
    3. I added the disable:isDisabled to the second selectbox cause I want it to be disabled when nothing is selected in the first selectbox.

    4. added self.isDisabled = ko.observable(true); to the cartline model

    5. altered the subscription to check the new value. If it is the select option the second one gets lock.

      function formatCurrency(value) {
          return "$" + value.toFixed(2);
      }
      
      var CartLine = function() {
          var self = this;
      
          // added this to enable/disable second select
          self.isDisabled = ko.observable(true);
      
          self.category = ko.observable();
          self.product = ko.observable();
          self.quantity = ko.observable(1);
          self.subtotal = ko.computed(function() {
              return self.product() ? self.product().price * parseInt("0" + self.quantity(), 10) : 0;
          });
      
          // Whenever the category changes, reset the product selection
          // added the val argument. Its the new value whenever category lchanges.
          self.category.subscribe(function(val) {
              self.product(undefined);
              // check to see if it should be disabled or not.
              self.isDisabled("Select..." == val.name);
          });
      };
      
      var Cart = function() {
          // Stores an array of lines, and from these, can work out the grandTotal
          var self = this;
          self.lines = ko.observableArray([new CartLine()]); // Put one line in by default
          self.grandTotal = ko.computed(function() {
              var total = 0;
              $.each(self.lines(), function() { total += this.subtotal() })
              return total;
          });
      
          // Operations
          self.addLine = function() { self.lines.push(new CartLine()) };
          self.removeLine = function(line) { self.lines.remove(line) };
          self.save = function() {
              var dataToSave = $.map(self.lines(), function(line) {
                  return line.product() ? {
                      productName: line.product().name,
                      quantity: line.quantity()
                  } : undefined
              });
              alert("Could now send this to server: " + JSON.stringify(dataToSave));
          };
      };