Select element not updating correctly with Knockout - javascript

Background
I have a situation where I want to have a few dropdown menus which change options based on what is available. I've managed to simplify this code and replicate the problem with the code below.
In this example I have 5 available colors and I want to choose four of them. If I select one, then I want it to not be available in the other menus.
Problem
The dropdown menus only sort of work. The options that are shown do seem to be valid based on what's available, however sometimes when selecting an entry it will not allow it until I choose a second time. Also, as seen in commented code below, a lodash _.sortBy seems to break functionality altogether.
HTML
<div data-bind="foreach:colorChoices">
<select data-bind="options: localAvailableOptions, optionsCaption: 'Select...', optionsText: function(currentValue) {return 'Color ID ' + currentValue;}, value: id"></select>
</div>
Javascript
function appModel() {
var self = this;
self.colorIds = [1, 2, 3, 4, 5];
self.colorChoices = ko.observableArray();
self.selectedColorIds = ko.computed(function() {
return _(self.colorChoices())
.filter(function(item) {
return item.id()
})
.map(function(item) {
return item.id()
})
.value();
});
self.availableColorIds = ko.computed(function() {
return _.difference(self.colorIds, self.selectedColorIds());
});
self.colorChoices.push(new colorChoice(self));
self.colorChoices.push(new colorChoice(self));
self.colorChoices.push(new colorChoice(self));
self.colorChoices.push(new colorChoice(self));
}
function colorChoice(parent) {
var self = this;
self.id = ko.observable();
self.localAvailableOptions = ko.computed(function() {
//clone as to not modify original array
var availableIds = _.clone(parent.availableColorIds());
//add own ID so dropdown menu contains matching entry
if (self.id()) {
availableIds.push(self.id());
}
//seems to break with _.sortBy
//return _.sortBy(availableIds);
return availableIds;
});
}
ko.applyBindings(new appModel());
CodePen (same code)
https://codepen.io/anon/pen/KEKPKV

I found the issue.
if (self.id()) {
availableIds.push(self.id());
}
This was missing a check to see if it already existed, and meant that the available options included duplicate values, which was presumably producing the undefined behavior.

Related

Filtering observable array with knockout

Could you please find what I'm doing wrong with this array filter. Fiddle Here
I've been working on it, and making very slow progress. I checked on a lot of samples but not able to find my issue.
Thanks
//This is the part I'm not able to fix
self.filteredPlaces = ko.computed(function() {
var filter = self.filter().toLowerCase();
if (!filter) {
ko.utils.arrayForEach(self.placeList(), function (item) {
});
return self.placeList();
} else {
return ko.utils.arrayFilter(self.placeList(), function(item) {
var result = (item.city().toLowerCase().search(filter) >= 0);
return result;
});
}
});
You did not data-bind filter to any input. You used query instead.
Change your filter value to use the query observable:
var filter = self.query().toLowerCase();
I think I know what you're trying to accomplish so I'll take a shot. There are a few things wrong with this code.
foreach in knockout accepts an array not a function.
http://knockoutjs.com/documentation/foreach-binding.html
I think you're trying to hide entries that don't contain the text in the search box. For that you need the visible binding. I re-factored your code to the sample below.
<div data-bind="foreach: placeList" class="alignTextCenter">
<p href="#" class="whiteFont" data-bind="text: city, visible: isVisible"></p>
</div>
I added isVisible as an item in your array, and an observable in your class.
var initialPlaces = [
{"city":"Real de Asientos","lat":22.2384759,"lng":-102.089015599999,isVisible:true},
{"city":"Todos Santos","lat":23.4463619,"lng":-110.226510099999,isVisible:true},
{"city":"Palizada","lat":18.2545777,"lng":-92.0914798999999,isVisible:true},
{"city":"Parras de la Fuente","lat":25.4492883,"lng":-102.1747077,isVisible:true},
{"city":"Comala","lat":19.3190634,"lng":-103.7549847,isVisible:true},
];
var Place = function(data) {
this.city = ko.observable(data.city);
this.lat = ko.observable(data.lat);
this.lng = ko.observable(data.lng);
this.isVisible = ko.observable(data.isVisible);
};
Lastly, you want to subscribe to the changes of "query" since it's on your text box so that the list updates when the text box changes. It's the self.query.subscribe line in my fiddle. I apologize about the formatting, I tried several times and could not get it to work.
Working fiddle here

Knockout JS using computed arrays outside of ViewModel

I would like to display a list of items on a page, and be able to dynamically reposition items by using a dropdown list of all positions. Selecting a position from the dropdown will change the current position of the item and re-shift the position any affected elements in the list.
I do have a working version of this concept, but it is not ideal. For some reason, when I reference my selectedItems computed array (I filter my items by setting the selectedItem observable), the position that is contained in the returned item is the original position value of the item, and not the one that has been set via the dropdowns/my reposition function. This is somewhat odd because the 'items' observableArray does contain the updated value, and the computedArray does return the right item, just not with the most up to date value.
A working JSfiddle is below. However, it does a lot of manual calculation and does not take advantage of the computed array as described above. The issue might be somehow related to setting Knockout observables from outside the ViewModel. To see the issue, uncomment the 2 lines in the 'document ready' block', where I attempt to find the current position of an item, and comment out the for loop where I look for the current item manually.
https://jsfiddle.net/tq1m873m/5/
I'm new to KnockoutJS & JS in general, be gentle :)
$(document).ready(function () {
$("select[id^='selectName_']").change(function () {
//Extract the item ID from the select html id attribute
var curItemIDNum = $(this).attr('id').substring(15);
var currentPos = 0;
// myViewModel.selectedItem("item" + curItemIDNum);
// currentPos = myViewModel.selectedItems()[0].position();
// START - really bad code, shield your eyes
// I can't seem to get the current position via the 2 commented lines above and have to resort to converting the observable array to a regular array and pulling the value that way. Not pretty!
var itemsJS = ko.toJS(self.items());
for (var x = 0; x < itemsJS.length; x++) {
if (("item" + curItemIDNum) == itemsJS[x].name) {
currentPos = itemsJS[x].position;
break;
}
}
// END - really bad code
reposition("item" + curItemIDNum, currentPos, $(this).val());
refreshDropDowns();
});
refreshDropDowns();
});
You were working on this before, and I didn't have a working solution for you. Today, I do. Your use of a jQuery trigger is not going to work out well. Let's do it all with Knockout.
I made items to be just an array of objects that do not have assigned positions. orderedItems is a computed that goes through items in order and creates an observable for position.
A subscription on that position observable calls moveItemTo, which rearranges items, and all the dependencies are updated by Knockout.
$(function() {
ko.applyBindings(viewModel());
});
function item(name) {
return {
name: name
};
}
var viewModel = function() {
var self = {};
self.items = ko.observableArray([
item('item1'),
item('item2'),
item('item4'),
item('item5'),
item('item3')
]);
function moveItemTo(item, pos) {
var oldPos = self.items.indexOf(item),
newPos = pos - 1,
items;
if (oldPos < newPos) {
items = self.items.slice(oldPos, newPos + 1);
items.push(items.shift());
self.items.splice.bind(self.items, oldPos, items.length).apply(self.items, items);
} else {
items = self.items.slice(newPos, oldPos + 1);
items.unshift(items.pop());
self.items.splice.bind(self.items, newPos, items.length).apply(self.items, items);
}
}
self.orderedItems = ko.computed(function() {
return ko.utils.arrayMap(self.items(), function(item, index) {
var pos = ko.observable(index + 1);
pos.subscribe(moveItemTo.bind(null, item));
return {
name: item.name,
position: pos
};
});
});
return self;
}; //end of viewmodel
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div>Set the order of the item by selecting a new position from the dropdown:
<ul data-bind="foreach: orderedItems">
<li>
<div> <span data-bind="text: name"></span>
<select data-bind="options: $root.orderedItems, optionsValue:'position', value: position"></select>
</div>
</li>
</ul>
</div>ITEMS CONTENTS:
<BR>
<span data-bind="text: JSON.stringify(ko.toJS(items), null, 4)"></span>
One way I can think of doing this would be to use a computed value to hold the current state of the position. This would allow you to do your reshuffling when a new position is set on an item like below :
ko.utils.arrayForEach(self.items(), function (x) {
x.newPosition = ko.computed({
read: function () {
return x.position();
},
write: function (val) {
//get the item in the prev position
var temp = ko.utils.arrayFirst(self.items(), function (y) {
return y.position() == val;
});
//swap positons here
if (temp) {
temp.position(x.position());
x.position(val);
}
}
});
});
in the Mark up it would be
<select data-bind="options:positions,value:newPosition"></select>
so on computed "write" ... the script swaps the position values. I left your original binding to the orderedItems. you can find a working sample here https://jsfiddle.net/p1yhmvcr/2/ ... the one thing worth noting here would be the sorting is not really physical. the observable array items are still on their original index in the array and the code is only changing the position values.

angularJs exclude already selected items from array

I have an array of objects in $scope.currentSChannels.scgsLink This array of objects have something like
$scope.currentSChannels.scgsLink = [{channelId:1, sCgsLinkId:1, groupNo:1, percentage: 50, expireHrs:4},{channelId:1, sCgsLinkId:2, groupNo:2, percentage:50, expireHrs:1}]
and I also have the following select list
<div class="col-md-4">
<select class="form-control" ng-model="newLink.groupNo"
name="groupNo" id="groupNo"
ng-options="t.value as t.text for t in metaData.spGroups"></select>
</div>
I need to filter that list to not show already selected items in the $scope.currentSChannels.scgsLink groupNo column. I looked at http://christian.fei.ninja/Angular-Filter-already-selected-items-from-ng-options/ and also at AngularJS ng-options to exclude specific object and both seem to be close but not enough as I need to filter against an array and a particular column in that array. How should I implement that filtering?
The template is getting a bit tricky. Assuming selectedLink is the variable that points to the selected groupNo
ng-options="t.value as t.text for t in metaData.spGroups | filter: {value: '!' + currentSChannels.scgsLink[selectedLink].groupNo}"
See this fiddle : the second select contains the same collection as the first one, excluded what is already selected.
Edit: Solution above is for excluding elements according to one value. So as to exclude the elements according to a collection of values, a custom filter would suit best:
Filter
app.filter('channelFilter', function () {
return function (metadata, exclusions) {
var filterFunction = function (metadata) {
// return the metadata object if exclusions array does NOT contain his groupNo
return !exclusions.some(function (exclusion) {
return exclusion.groupNo === metadata.value;
});
};
return metadatas.filter(filterFunction);
};
});
Usage
ng-options="metadata in metadatas | channelFilter: exclusions"
Template
ng-options="t.value as t.text for t in metaData.spGroups | channelFilter: currentSChannels.scgsLink"
Fiddle
That said, would be more efficient to group selected links by groupNo to avoid searches in the array, and filter in the controller.
I wanted to make it a bit more generic, so I've done the following
http://jsfiddle.net/96m4sfu8/
app.filter('excludeFrom', function () {
return function (inputArray, excludeArray, excludeColumnName, inputColumnName) {
if (inputColumnName==undefined)
inputColumnName = 'value';
var filterFunction = function (inputItem) {
return !excludeArray.some(function (excludeItem) {
return excludeItem[excludeColumnName] === inputItem[inputColumnName];
});
};
return inputArray.filter(filterFunction);
};
});

Filter one collection based on another with AngularJS custom filter

I need to filter a collection of objects based on another collection in angularjs. I need to create a custom filter I'm sure but have found no examples that are similar. So I have a list of "equipmentOptions" (basically they are product customizations, ex. premium stereo in your car). Some "equipmentOptions" are not available unless you have already selected other "equipmentOptions".
The equipmentOptions available are $scope.product.equipmentOptions.
The equipmentOptions that have been selected are $scope.product.selectedOptions.
equipmentOptions has a property requiredParents which is a collection of id's for other equipment options. If any of these requiredParents exist in $scope.product.selectedOptions, then the equipmentOption should be shown.
Here's what I've tried so far to no avail:
<div ng-repeat="option in product.equipmentOptions | optionsFilter:product.selectedOptions">
optionsFilter.js
myApp.filter('optionsFilter', function () {
return function (selectedOptions) {
// I'm just trying to get the list of selected options as well as the current option here for filtering, how do I get the current option?
};
});
Your first argument is the list of product.equipmentOptions. The second argument is product.selectedOptions. Write your filter like this:
myApp.filter('optionsFilter', function() {
return function(equipmentOptions, selectedOptions) {
var filteredOptions = [];
angular.forEach(equipmentOptions, function(option) {
if(/*option meets criteria*/) {
filteredOptions.push(option);
}
});
return filteredOptions;
}
});

How to call a javascript function using a javascript object from a different javascript file?

I am currently using a predefined javascript Select2.js.
Now my issue is that I have some set of values that are to be removed from the list which are again dynamic values as in say when I am editing a page and I need the already saved values again fetched from backend during page load to not appear when I am searching for the data.
For the above I need a way to access the select2 object being created on the page load and set add these values in some attribute but from a different js file.
Can anyone please help me with the same? I hope whatever I am making sense...
Thanks in advance.
Sample code --
var val = this.getVal(),
choices = this.results.find(".select2-result"),
compound = this.results.find(".select2-result-with-children"),
self = this;
choices.each2(function (i, choice) {
var id = self.id(choice.data("select2-data"));
if (indexOf(id, val) >= 0) {
choice.addClass("select2-selected");
// mark all children of the selected parent as selected
choice.find(".select2-result-selectable").addClass("select2-selected");
}
});
compound.each2(function(i, choice) {
// hide an optgroup if it doesnt have any selectable children
if (!choice.is('.select2-result-selectable')
&& choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
choice.addClass("select2-selected");
}
});
The above is a snippet from the select2.js..
Here this refers to the select2 object being created using the below code :
$(".select2Search").select2({
closeOnSelect: false,
width: '450px',
minimumInputLength: 3,
ajax: {},
allowClear: true,
tags: true
});
When alerted, val returns the comma separated ids of the elements already selected so I need to make sure that this.getVal() has the predefined data...
There is a function named setVal as below -
setVal: function (val) {
var unique;
if (this.select) {
this.select.val(val);
} else {
unique = [];
// filter out duplicates
$(val).each(function () {
if (indexOf(this, unique) < 0) unique.push(this);
});
this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
alert(this.opts.element.val());
}
}

Categories

Resources