knockout: remove one element from multiselect control when other is selected - javascript

I'm using knockout and I have a list of item, let say:
Tomato,
Potato,
Broccoli,
Bean
all those item are allowed to user to select from multiselect form-control.
<select class="form-control" multiple
data-bind="selectPicker: Types,
optionsText: 'Name',
optionsValue: 'VegetableTypeId',
selectPickerOptions: { optionsArray: AvailableVegetableTypes }">
</select>
Except one scenario - when the user selects tomato, potato should unselect.
I was trying to use subscription on selected items array:
this.Types.subscribe(changes => {
var VegetableTypes = this.AvailableVegetablesTypes();
var company = VegetableTypes.First(element => element.VegetableTypeId == changes[0].value);
if (changes[0].status == "added") {
if (Vegetable.IsTomato) {
this.Types.remove(element =>
VegetableTypes.First(baseElement =>
baseElement.VegetableTypesTypeId == element && baseElement.IsPotato));
} else if (Vegetable.IsPotato) {
this.Types.remove(element =>
VegetableTypes.First(baseElement =>
baseElement.VegetableTypesTypeId == element && baseElement.IsTomato));
}
}
}, null, "arrayChange");
Problem is that I'm using ObservableArray.Remove, so it's again call my function before current run is finish. This should not be a problem, because after remove first change is "deletion" type, so whole logic should not be executed.
But after this, when I select tomato/potato again, nothing is fired. In the end I actually have both tomato and potato selected.
Then, when I deselect one of these two and select it again, everything works fine, and then the whole situation repeats.
Do you have any ideas?

I didn't understand why you are using selectPicker bindings instead of the normal options and selectedOptions bindings available in Knockout.
However, I built a simple demo which implements the desired behaviour. You can find it here:
http://jsbin.com/fofojaqohi/1/edit?html,js,console,output
Note that, whenever you select Tomato after Potato, Potato will become unselected.
You were on the right track: you need to subscribe to the array of selected items and check if there are any invalid selections. I hope this helps.
For reference, here is the code:
HTML:
<select class="form-control" multiple="true"
data-bind="options: availableVegetableTypes, selectedOptions: selected">
</select>
JS:
var availableVegetableTypes = ['Tomato',
'Potato',
'Broccoli',
'Bean'];
var selected = ko.observableArray();
var unselectMap = {
'Tomato': 'Potato'
};
selected.subscribe(function(selectedOptions){
var keysToUnselect = [];
console.log("Selected", selectedOptions);
selectedOptions.forEach(function(selectedOption){
if (unselectMap[selectedOption] != null) {
// This key exists in the unselect map
// Let's check if the value is in the array
if (_.contains(selectedOptions, unselectMap[selectedOption])) {
// The invalid key exists. Let's mark it for removal.
keysToUnselect.push(unselectMap[selectedOption]);
}
}
});
if (keysToUnselect.length > 0) {
console.log("Unselect", keysToUnselect);
var reject = function(v){
return _.contains(keysToUnselect, v);
};
filteredSelectedOptions = _.reject(selectedOptions, reject);
console.log("Filtered", filteredSelectedOptions);
selected(filteredSelectedOptions);
}
});
ko.applyBindings({
availableVegetableTypes:availableVegetableTypes,
selected: selected
});

Related

Vue watch and display deep changes

I'm trying to display a message for each category the user is selected
<v-select multiple style="position: relative; top: 20px;"
color="white" v-if="count == 3 && question" solo placeholder="Please Choose"
v-model="selected.category" :items="categories" item-text="name" return-object></v-select>
The user can select multiple categories, in each category there is a specific message i want to display.
I'm using watcher to watch the changes.
watch: {
'selected.category': {
handler(e) {
console.log(e)
this.icon = "mdi-emoticon"
this.text = this.selected.category.chat //This is an array now because multiple categories are selected
this.selection = true
}
},
}
The above code is wrong, it was only used when the user was able to select only one category, now i want them to be able to select multiple. I just can't figure out how to watch these deep changes and display the category message when multiple categories are selected. Is there any way i can send the latest selected category index in the handler or maybe only the selected object?
This made it work.
'selected.category': {
handler(e) {
console.log(e)
this.icon = "mdi-emoticon"
this.text = this.selected.category[this.selected.category.length - 1].chat
this.selection = true
}
},

How to make ng-options to run after it is enabled?

So, I have these two dropdown element and the output of second will depend on input of first. The second drop down will take input of first drop down as ( ng-options="reason for reason in getReason($ctrl.selectedItem))
However I realized the second dropdown will not wait for first drop down to be selected and run the ng-options but since it takes input from first. It causes null error.
How do I solve this issue?
class Items{
constructor(){
this.items = ["item1", "item2", "item3"];
this.selectedItem = null;
this.stuff = {
"item1" : ["asdd"],
"item2" : ["asdasdsd", "asdsddd],
"item3" : ["asdasdasd]
};
}
getReason(item){
return this.stuff[item];
}
}
<select ng-model=“$ctrl.selectedItem” ng-options=“item for item in items”>
</select>
<select ng-disabled=“!$ctrl.selectedItem” ng-options=“reason for reason in getReason($ctrl.selectedItem)”>
Change your getReason in order to return an empty array if item is not a "valid" argument.
getReason(item){
item ? return this.stuff[item] : [];
}
Note: This will keep the second dropdown empty until the item argument is valid.

Populate dropdownlist in mvc using JQuery onChange

Have two dropdown lists created in MVC. I want to repopulate the second dropdown list when a user selects something in the first dropdown list.
On page load I am populating the second dropdown list like this:
// Finds selected value
var selectedItem = departureRouteSelectList.First().Value.Substring(2, 2);
// Create new selectList for returnRoute
var returnRouteSelectList = Model.RoutesListConversely
.Where(x => x.Value.StartsWith(selectedItem))
.Select(x => new SelectListItem() { Text = x.Text, Value = x.Value });
The first line finds the last two letters which I use to compare on the Model.RoutesListConversely. The second line creates the list returnRouteSelectList based on what it finds using the two letters.
Can this be done using an onChange() method in JavaScript? So when you select something, this method runs and repopulate the second dropdown list?
Code:
<p>
#Html.Label("Departure route:")
#Html.DropDownListFor(m => m.Departureroute, departureRouteSelectList,
new { #class = "dropdownlist", #id = "departureroute" })
</p>
<p>
#Html.Label("Return route:")
#Html.DropDownListFor(m => m.Returnroute, returnRouteSelectList,
new { #class = "dropdownlist", #id = "returnroute" })
</p>
UPDATE:
Have tried solve this using this code.. .but the problem is that it does not append data, but the clear function is working.
// Getting value from departure dropdown
$("#departureroute").change(function () {
}).change(populateList);
function populateList() {
// Clear dropdown for return route
$("#returnroute").empty();
// Gets last two letters.
var word = (this.value).substring(2);
// Gets the value from the departure route
var selectedId = $("#departureroute").selectedItem.value;
var returnlist = $("#returnroute");
$("#departureroute").each()
{
if (word == selectedId.substring(2)) {
returnlist.append("#departureroute")
}
}
}
I managed to solve my problem using jquery each() function and append.
The following code did the trick:
// On change departure route dropdown list
$("#departureroute").change(function () {
}).change(populateReturnlist);
function populateReturnlist() {
// Clear dropdown for return route
$("#returnroute").empty();
// Gets last two letters.
var word = (this.value).substring(2);
var returnlist = $("#returnroute");
$("#departureroute option").each(function () {
// Gets the value from the departure route
var selectedId = this.value.substring(0, 2);
// Checks so the right routes gets added to list
if (selectedId === word) {
returnlist.append("<option>" + this.text + "</option>")
}
});
};

Knockout.js options binding - deselecting option when option has been removed

I have a dropdown (select control) bound to an observable array using the options, optionsValue, optionsText, and optionsCaption bindings.
If I have an option selected and then later remove it, the dropdown selects the first item. I would like it to set the selected value to undefined without having to add an empty item to the observable array.
Here is a simple example:
<select data-bind="value: selectedItemValue, options: items, optionsValue: 'value', optionsText: 'text', optionsCaption: ''"></select>
<button type="button" data-bind="click: selectLast">Select Last</button>
<button type="button" data-bind="click: removeLastItem">Remove Last</button>
var viewmodel = function () {
var self = this;
this.items = ko.observableArray();
this.selectedItemValue = ko.observable(null);
this.selectLast = function () {
self.selectedItemValue(
self.items()[self.items().length - 1].value);
};
this.removeLastItem = function () {
self.items.pop();
};
this.items.push({
value: "item1",
text: "First item"
});
this.items.push({
value: "item2",
text: "Second item"
});
this.items.push({
value: "item3",
text: "Third item"
});
};
var vm = new viewmodel();
ko.applyBindings(vm);
jsfiddle
Click the Select Last button or manually select the last item in the drop down
Click the Remove Last button
Result: First item gets selected
What is the best approach to achieve the behavior I want?
With the introduction of the valueAllowUnset in the pull request:
#647 - add option that allows the value binding to accept a selected value that is not currently in the list of options
This old bug/feature has been fixed. So if you upgrade your fiddle to use at least KO version 3.1 it is working as you described:
Demo JSFiddle
In addition to what nemesv mentions you can simply remove the value if the last one is matching the current selected value -
this.removeLastItem = function () {
var lastItem = self.items()[self.items().length - 1];
if (self.selectedItemValue() === lastItem.value) {
self.selectedItemValue(null);
}
self.items.pop();
};
http://jsfiddle.net/guvbxor2/4/
Basically just check if you are about to remove the option that is selected and if so null it

Prevent reactivity from removing data from template

I've got a template to manage important property in my collection. It's a simple list filtered by this property with a toggle that allows changing its value:
<template name="list">
{{#each items}}
<div class="box" data-id="{{_id}}">
{{name}}
<span class="toggle">Toggle</span>
</div>
{{/each}}
</template>
Template.list.items = function() {
return Items.find({property: true}, {sort: {name: 1}});
};
Template.list.events({
'click .toggle': function(e) {
var item = Items.findOne( $(e.target).closest('.box').data('id') );
Items.update(item._id, {$set: {
property: !item.property;
}});
},
});
Quite simple. Now, obviously, when I click the toggle for an item, this item property turns to false and it's removed from the list. However, to enable easy undo, I'd like the item to stay in the list. Ideally, until user leaves the page, but a postponed fadeout is acceptable.
I don't want to block the reactivity completely, new items should appear on the list, and in case of name change it should be updated. All I want is for the removed items to stay for a while.
Is there an easy way to achieve this?
I would store the removed items in a new array, and do something like this:
var removed = []; // Contains removed items.
Template.list.created = function() {
// Make it empty in the beginning (if the template is used many times sequentially).
removed = []
};
Template.list.items = function() {
// Get items from the collection.
var items = Items.find({property: true}).fetch();
// Add the removed items to it.
items = items.concat(removed)
// Do the sorting.
items.sort(function(a, b){ return a.name < b.name}) // May be wrong sorter, but any way.
return items
};
Template.list.events({
'click .toggle': function(e) {
var item = Items.findOne( $(e.target).closest('.box').data('id') );
Items.update(item._id, {$set: {
property: !item.property;
}});
// Save the removed item in the removed list.
item.property = !item.property
item.deleted = true // To distinguish them from the none deleted ones.
removed.push(item)
},
});
That should work for you, wouldn't it? But there may be a better solution.

Categories