Knockout three cascading dropdowns

2019-08-02 01:00发布

问题:

I really liked the live example in cartEditor. This is what I need. Well, almost what if I want to add another dropdown. For example if I want to add to an existing example another field country. The scenario is the following: I select the country, vehicle category and according to this choose the vehicle itself. How to bind two dropdowns to a third?

Tables in the database about such:

Table vehicle

id name countryId categoryId

Table category

id name

Table country

id name

File for a live example is located at knockoutjs.com

回答1:

You'd continue with the same principle:

  • Define your data in an array or plain object
  • Construct your viewmodels that make data renderable and selectable
  • Create ko.pureComputed properties that select subsets of your data based on user input.

For example, let's say your products have both a category and a country:

var products = [
  { 
    name: "Cheese",
    country: "The Netherlands",
    category: "Dairy"
  }
]

Now, if you have two observables, bound to your UI:

this.selectedCountry = ko.observable();
this.selectedCategory = ko.observable();

You can create a ko.pureComputed that makes a selection of products that meet the requirements:

this.selectedProducts = ko.pureComputed(function() {
  return products.filter(function(product) {
    return product.category === this.selectedCategory() &&
           product.country === this.selectedCountry();
  });
}, this);

Example:

var products = [
  {
    name: "Gouda Cheese",
    country: "The Netherlands",
    category: "Dairy"
  },
  {
    name: "Camambert",
    country: "France",
    category: "Dairy"
  },
  {
    name: "Red Wine",
    country: "France",
    category: "Alcoholic beverages"
  }
  
];

var ViewModel = function() {
  // These will be bound to your dropdowns' selections
  this.selectedCountry = ko.observable();
  this.selectedCategory = ko.observable();

  // This computed calculates which products from your data
  // meet the requirements whenever one of the selections changes
  this.selectedProducts = ko.pureComputed(function() {
    return products.filter(function(product) {
      return product.category === this.selectedCategory() &&
        product.country === this.selectedCountry();
    }.bind(this));
  }, this);
  
  // Here, we create a list of countries that appear in your data,
  // this list is used to fill the dropdown's options
  this.countries = getKeyUniques(products, "country");

  // Do the same for categories
  this.categories = getKeyUniques(products, "category");

};

ko.applyBindings(new ViewModel());
  
// Utils
function getKeyUniques(arr, key) {
    return Object.keys(arr.reduce(function(map, item) {
      map[item[key]] = true;
      return map
    }, {}));
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<select data-bind="options: countries, value: selectedCountry"></select>
<select data-bind="options: categories, value: selectedCategory"></select>

<ul data-bind="foreach: selectedProducts">
  <li data-bind="text: name"></li>
</ul>



回答2:

My solution is as follows. I wanted to filter based on ID. Plan to get the lists of countries and categories from the server and then receive from the server a filtered list of products. A huge thank you to the user. With Your help and with the help of the example on the website http://knockoutjs.com/examples/cartEditor.html I solved my problem. Post here my solution:

var countries = [
    {
        "id": 1,
        "name": "Russia"
    },
    {
        "id": 2,
        "name": "USA"
    },
    {
        "id": 3,
        "name": "Great Britain"
    }
];

var categories = [
    {
        "id": 1,
        "name": "Classic Cars"
    },
    {
        "id": 2,
        "name": "Motorcycles"
    },
    {
        "id": 3,
        "name": "Planes"
    }
];

var products = [
    {
        "name": "P-51-D Mustang",
        "countryId": 2,
        "categoryId": 3,
        "price": 12.42
    },
    {
        "name": "1997 BMW R 1100 S",
        "countryId": 2,
        "categoryId": 2,
        "price": 60.87
    },
    {
        "name": "2002 Chevy Corvette",
        "countryId": 2,
        "categoryId": 1,
        "price": 100.87
    },
    {
        "name": "1998 Chrysler Plymouth Prowler",
        "countryId": 1,
        "categoryId": 1,
        "price": 150.87
    }
];

var CartLine = function () {
    var self = this;
    self.countries = ko.observableArray(countries);
    self.categories = ko.observableArray(categories);
    self.quantity = ko.observable(1);
    self.selectedCountry = ko.observable();
    self.selectedCategory = ko.observable();
    self.selectedProduct = ko.observable();
    self.subtotal = ko.pureComputed(function () {
        return self.selectedProduct() ? (self.selectedProduct().price * self.quantity()) : "yet";
    });
    self.products = ko.pureComputed(function () {
        return $.grep(products, function (item) {
            return (self.selectedCountry() && self.selectedCategory() && item.countryId === self.selectedCountry().id && item.categoryId === self.selectedCategory().id);
        });
    });
};

var Cart = function () {
    var self = this;
    self.lines = ko.observableArray([new CartLine()]);
    self.lines2 = ko.observableArray([new CartLine()]);
    self.grandTotal = ko.pureComputed(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);
    };
};

ko.applyBindings(new Cart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="stl.css" />
    </head>
    <body>
            <div class='liveExample'>
                <table width='50%'>
                    <thead>
                        <tr>
                            <th width='25%'>Country</th>
                            <th width='25%'>Category</th>
                            <th class='price' width='15%'>Product</th>
                            <th class='price' width='15%'>Price</th>
                            <th class='quantity' width='10%'>Quantity</th>
                            <th class='price' width='15%'>Subtotal</th>
                            <th width='10%'></th>
                        </tr>
                    </thead>
                    <tbody data-bind='foreach: lines'>
                        <tr>
                            <td>
                                <select data-bind='options: countries, optionsText: "name", optionsCaption: "Select...", value: selectedCountry'> </select>
                            </td>
                            <td>
                                <select data-bind='options: categories, optionsText: "name", optionsCaption: "Select...", value: selectedCategory'> </select>
                            </td>
                            <td>
                                <select data-bind='visible: (selectedCountry() && selectedCategory()), options: products, optionsText: "name", optionsCaption: "Select...", value: selectedProduct'> </select>    
                            </td>
                            <td>
                                <span data-bind='text: selectedProduct() ? selectedProduct().price : 0'> </span>
                            </td>
                            <td class='quantity'>
                                <input data-bind='value: quantity, valueUpdate: "afterkeydown"' />
                            </td>
                            <td class='price'>
                                <span data-bind='text: subtotal()'> </span>
                            </td>
                            <td>
                                <a href='#' data-bind='click: $root.removeLine'>Remove</a>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <p class='grandTotal'>
                    Total value: <span data-bind='text: grandTotal()'> </span>
                </p>
                <button data-bind='click: addLine'>Add product</button>
            </div>
        <script src="knockout-3.4.1.js" type="text/javascript"></script>
        <script src="jquery-3.1.1.min.js" type="text/javascript"></script>
        <script src="app.js" type="text/javascript"></script>
    </body>
</html>



回答3:

I did what you asked for based on the approach below -

  • Keeping the track of the values selected in the dropdowns for category and country and filtering out the vehicles array on the basis of countryId and categoryId

For this, I have subscribed to the selected values of country and category, so when either of them changes, I call the filters to update the selected vehicle so that it is updated in the third dropdown.

It also includes some test results at the end of the view model definition.

P.S. - Spare me for the bad combination of data, but I guess my efforts would be enough for you to get the gist of it ;-)

Here is the fiddle