Csaba Toth
Csaba Toth

Reputation: 10729

Knockout options binding: how to remove items dynamically from popup if they are selected

Here is a sample for the problem, in this case someone selects toppings for a pizza (my real world problem is analogue to that): http://jsfiddle.net/csabatoth/aUH2C/4/

HTML:

<h2>Pizza toppings (<span data-bind="text: toppings().length"></span>)</h2>
<table class="table table-bordered">
    <thead><tr>
        <th>Topping</th><th>Number of units</th>
    </tr></thead>
    <tbody data-bind="foreach: toppings">
        <tr>
            <td><select data-bind="options: $root.availableToppings(), value: name, optionsText: 'name'"></select></td>
            <td><input data-bind="value: num" /></td>
            <td><a href="#" data-bind="click: $root.removeTopping">Remove</a></td>
        </tr>
    </tbody>
</table>

<button type="button" class="btn btn-primary" data-bind="click: addTopping, enable: toppings().length < 5">Add another topping</button>

JS:

// Class to represent a possible topping
function Topping(name) {
    var self = this;
    self.name = name;
    // This will have other properties
}

// Class to represent a row in the grid
function ToppingRow(topping, num) {
    var self = this;
    self.topping = ko.observable(topping);
    self.num = ko.observable(num);

    self.toppingName = ko.computed(function() {
        return self.topping().name;
    });
}

function ToppingsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableToppings = ko.observableArray([
        new Topping("Mushroom"),
        new Topping("Pepperoni"),
        new Topping("Cheese"),
        new Topping("Olives"),
        new Topping("Chicken")
    ]);    

    // Editable data
    self.toppings = ko.observableArray([
        new ToppingRow(self.availableToppings()[0], 1)
    ]);

    // Operations
    self.addTopping = function() {
        self.toppings.push(new ToppingRow(self.availableToppings()[0], 1));
    }
    self.removeTopping = function(topp) { self.toppings.remove(topp) }    
}

ko.applyBindings(new ToppingsViewModel());

What I would like is: when the user selects a topping, that option should disappear from the popup list. Once the user removes the topping, it should reappear in the popup. In other words: I don't want the user to add the same topping more than once. How to do that?

(Or now I think if I should approach this in a totally different way and would have a list with the toppings on the left, and the user could drag&drop to the right destination list from there...). In the real world example the number of "toppings" would be maybe some dozen I think.

Upvotes: 3

Views: 2324

Answers (2)

4imble
4imble

Reputation: 14426

You could simplify this slightly by having a topping selector with 1 drop down. Then when you click add it inserts the currently selected item to a selected toppings section and then removes the option from the available list.

If you are feeling clever you could also bind the drop-down to a computed collection of items that does not include already selected items. (underscore.js will also help with this immensely).

(Fiddle)

JS

// Class to represent a possible topping
function Topping(name) {
    var self = this;
    self.name = name;
    // This will have other properties
}

// Class to represent a row in the grid
function ToppingRow(topping, num) {
    var self = this;
    self.topping = topping;
    self.num = ko.observable(num);

    self.toppingName = ko.computed(function() {
        return self.topping.name;
    });
}

function ToppingsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.allToppings = ko.observableArray([
        new Topping("Mushroom"),
        new Topping("Pepperoni"),
        new Topping("Cheese"),
        new Topping("Olives"),
        new Topping("Chicken")
    ]);   

    self.selectedToppings = ko.observableArray([]);

    self.availableToppings = ko.computed(function(){
        return _.reject(self.allToppings(), function(topping) {
            return _.contains(_.pluck(self.selectedToppings(), 'topping'), topping);
        })
    });

    self.currentlySelectedTopping = ko.observable();
    self.currentlySelectedToppingNumber = ko.observable(1);

    // Operations
    self.addTopping = function() {
        self.selectedToppings.push(new ToppingRow(self.currentlySelectedTopping(), self.currentlySelectedToppingNumber()));
    }
    //self.removeTopping = function(topp) { self.toppings.remove(topp) }    
}

ko.applyBindings(new ToppingsViewModel());

HTML

<h2>Pizza toppings</h2>

<table class="table table-bordered">
    <thead><tr>
        <th>Topping</th><th>Number of units</th>
    </tr></thead>
    <tbody>
        <tr>
            <td><select data-bind="options: availableToppings, value: currentlySelectedTopping, optionsText: 'name'"></select></td>
            <td><input data-bind="value: currentlySelectedToppingNumber" /></td>
            <td><a href="#" data-bind="click: function(){alert('hello');}">Remove</a></td>
        </tr>
        <!-- ko foreach: selectedToppings -->
        <tr><td data-bind="text: toppingName"></td><td data-bind="text: num"></td></tr>
        <!-- /ko -->
    </tbody>
</table>

<button type="button" class="btn btn-primary" data-bind="click: addTopping, enable: availableToppings().length">Add another topping</button>

Upvotes: 2

Muhammad Raheel
Muhammad Raheel

Reputation: 19882

I dont why you are making simple things difficult. Here is some modified version.

function Topping(name) {
    var self = this;
    self.name = name;
    self.active = ko.observable(false)

    self.toggle = function () {
        self.active(!self.active())
    }
    // This will have other properties
}

// Class to represent a row in the grid
function ToppingRow(name, num) {
    var self = this;
    self.name = name;
    self.num = num;
}

function ToppingsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableToppings = ko.observableArray([
        new Topping("Mushroom"),
        new Topping("Pepperoni"),
        new Topping("Cheese"),
        new Topping("Olives"),
        new Topping("Chicken")
    ]); 

    self.list = ko.observableArray()
    self.num = ko.observable(1) 
    self.selected = ko.observable()  


    // Operations
    self.addTopping = function() {
        self.list.push(new ToppingRow(self.selected(),self.num()));
        self.setAvailableToppings(self.selected())
    }
    self.removeTopping = function(item) { 
        self.list.remove(item) 
        self.setAvailableToppings(item.name)
    } 

    self.setAvailableToppings   = function (name) {
        var items = []
        ko.utils.arrayForEach(self.availableToppings(),function (item) {
            if(item.name == name){
                item.toggle()
            }
            items.push(item)
        })
        self.availableToppings([])
        self.availableToppings(items)       
        var selected = ko.utils.arrayFirst(self.availableToppings(),function (item) {
            return item.active() == false
        })
        if(selected){
            self.selected(selected.name)
        }
    }

    self.setOptionDisable = function(option, item) {
        ko.applyBindingsToNode(option, {disable: item.active()}, item);
    }
}


$(document).ready(function(){
    ko.applyBindings(new ToppingsViewModel());      
})

And view

<h2>Pizza toppings (<span data-bind="text: list().length"></span>)</h2>

<table class="table table-bordered">
    <thead><tr>
        <th>Topping</th><th>Number of units</th>
    </tr></thead>
    <tbody data-bind="foreach: list">
        <tr>
            <td data-bind="text:name"></td>
            <td data-bind="text: num"></td>
            <td><a href="#" data-bind="click: $root.removeTopping">Remove</a></td>
        </tr>
    </tbody>
</table>
<br clear="all"/>
<select data-bind="
    options: $root.availableToppings(), 
    value: $root.selected, 
    optionsText: 'name',
    optionsValue : 'name',
    optionsAfterRender: $root.setOptionDisable,
    enable: list().length < 5
">
</select>
    <input data-bind="value: $root.num,enable: list().length < 5" />
    <br clear="all"/>
<button type="button" class="btn btn-primary" 
    data-bind="
        click: addTopping, 
        enable: list().length < 5
">Add another topping</button>

Fiddle Demo

Upvotes: 2

Related Questions