Mike Mike - 6 months ago 40
HTML Question

binding multi dropdown list in knockout.js

I have a multi dropdown list and I need to do the following:

1. Make sure that when selecting value in one dropdown list it won't appear in the others (couldn't find a proper solution here).

2. When selecting the value "Text" a text field (

<input>
) will apear instead of the Yes/no dropdown.

3. "Choose option" will appear only for the first row (still working on it).

4. Make sure that if "Text" is selected, it always will be on the top (still working on it).


JSFiddle

HTML:

<div class='liveExample'>
<table width='100%'>
<tbody data-bind='foreach: lines'>
<tr>
<td>
Choose option:
</td>
<td>
<select data-bind='options: filters, optionsText: "name", value: filterValue'> </select>
</td>
<td data-bind="with: filterValue">
<select data-bind='options: filterValues, optionsText: "name", value: "name"'> </select>
</td>
<td>
<button href='#' data-bind='click: $parent.removeFilter'>Remove</button>
</td>
</tr>
</tbody>
</table>
<button data-bind='click: addFilter'>Add Choice</button>


JAVASCRIPT:

var CartLine = function() {
var self = this;
self.filter = ko.observable();
self.filterValue = ko.observable();

// Whenever the filter changes, reset the value selection
self.filter.subscribe(function() {
self.filterValue(undefined);
});
};

var Cart = function() {
// Stores an array of filters
var self = this;
self.lines = ko.observableArray([new CartLine()]); // Put one line in by default

// Operations
self.addFilter = function() { self.lines.push(new CartLine()) };
self.removeFilter = function(line) { self.lines.remove(line) };
};


ko.applyBindings(new Cart());


I will appeaciate your assist here! Mainly for the first problem.

Thanks!

Mike

Answer

If you want to limit the options based on the options that are already selected in the UI, you'll need to make sure every cartLine gets its own array of filters. Let's pass it in the constructor like so:

var CartLine = function(availableFilters) {
  var self = this;
  self.availableFilters = availableFilters;

  // Other code
  // ...
};

You'll have to use this new viewmodel property instead of your global filters array:

<td>
  <select data-bind='options: availableFilters, 
    optionsText: "name", 
    value: filterValue'> </select>
</td>

Now, we'll have to find out which filters are still available when creating a new cartLine instance. Cart manages all the lines, and has an addFilter function.

self.addFilter = function() {
  var availableFilters = filters.filter(function(filter) {
    return !self.lines().some(function(cartLine) {
      var currentFilterValue = cartLine.filterValue();
      return currentFilterValue &&
        currentFilterValue.name === filter.name;
    });
  });

  self.lines.push(new CartLine(availableFilters))
};

The new CartLine instance gets only the filter that aren't yet used in any other line. (Note: if you want to use Array.prototype.some in older browsers, you might need a polyfill)

The only thing that remains is more of an UX decision than a "coding decision": do you want users to be able to change previous "Choices" after having added a new one? If this is the case, you'll need to create computed availableFilters arrays rather than ordinary ones.

Here's a forked fiddle that contains the code I posted above: http://jsfiddle.net/ztwcqL69/ Note that you can create doubled choices, because choices remain editable after adding new ones. If you comment what the desired behavior would be, I can help you figure out how to do so. This might require some more drastic changes... The solution I provided is more of a pointer in the right direction.

Edit: I felt bad for not offering a final solution, so here's another approach:

If you want to update the availableFilters retrospectively, you can do so like this:

CartLines get a reference to their siblings (the other cart lines) and create a subscription to any changes via a ko.computed that uses siblings and their filterValue:

var CartLine = function(siblings) {
  var self = this;

  self.availableFilters = ko.computed(function() {
    return filters.filter(function(filter) {
      return !siblings()
        .filter(function(cartLine) { return cartLine !== self })
        .some(function(cartLine) {
        var currentFilterValue = cartLine.filterValue();
        return currentFilterValue &&
          currentFilterValue.name === filter.name;
      });
    });
  });

  // Other code...
};

Create new cart lines like so: self.lines.push(new CartLine(self.lines)). Initiate with an empty array and push the first CartLine afterwards by using addFilter.

Updated fiddle that contains this code: http://jsfiddle.net/2L6kq1ad/

Comments