Filtering observable array with knockout - javascript

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

Related

Defining Kendo UI Grid Column Templates with a Function that returns a string

I need to create a Kendo UI for jQuery column which uses a function to determine the template. Essentially the data I am receiving from the backend gives me a number, and I need to match up that number to the corresponding entry in another database table. Once the match is found, I need to set the column template to show that entry.
template: function (e) {
countryData.forEach(function (country) {
let countryDesc;
if (country.countryCode == e.countryCode) {
countryDesc = country.description;
return countryDesc;
}
})
}
This is the function that I have written. countryData is an array of JSON objects containing a list of countries with codes, I am matching that code up with e.countryCode to get the correct country. This is then assigned to countryDesc, and returned.
When ran, the columns are just displayed as 'undefined'. I'm confused as to why this isn't working, as if I do this for example: template: "foo", the column would display foo. Surely all i'm doing is returning a string, so this should work?
So after trying loads of things, replacing the .forEach with a plain for fixed this issue.
template: function (e) {
let country;
for (i = 0; i < countryData.length; i++) {
if (countryData[i].countryCode == e.countryCode) {
country = countryData[i].description;
}
return country;
}

Select element not updating correctly with Knockout

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.

How to pass ng-repeat item property to filter?

I created a filter that I want to pass an item property, and not the item itself. Is that possible?
The following does not work (item.param1 fails):
ng-reapeat="item in items | filter : fnFilter(item.param1)"
$scope.fnFilter = function(value) {
return value == "test";
}
Your question is quite unclear sdince you don't tell really what is your goal.
So if i just restrict to what i see there and following the link Angular filter exactly on object key already provided by #CeylanMumumKocabas you would have
ng-repeat="item in items | filter:{'param1': 'test'}"
Now let's consider you want something more complex : the only way i see would be to pass the name of the attribute to the filter :
ng-reapeat="item in items | myFilter:'param1'"
myApp.filter('myFilter', function () {
return function(inputs,attributeName) {
var output = [];
angular.forEach(inputs, function (input) {
if (input[attributeName] == 'test')
output.push(input);
});
return output;
};
});
Note that if you want to go more than one level, you'll have to use $eval or make add some code for this to work.

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);
};
});

Categories

Resources