knockout.js computed function to create two arrays? - javascript

I have an array of objects that a user can perform search on. I'm using a ko.computed function based on the search to create another array of matched items for display.
self.matchedRecords = ko.computed(function() {
return ko.utils.arrayFilter(self.transponders(), function(r) {
return r.title.toLowerCase().indexOf($('.search-input').val().toLowerCase()) > -1
}
};
This works great and I'm really impressed with the performance so far.
This issue is I also need the "unmatched" records because I have to perform an addition operation on them in some cases (60% of the time). I don't really want to create a second ko.computed function because then I have to run through this array a second time every time a search is performed.
So, my question: Is there a way that I can use the same ko.computed to create a second array of unmatched items? Basically run through the array and put each item in the matched or unmatched array...
If not, is it faster to:
1) create a second ko.computed to get the unmatched items from my array as the user searches; or
2) write an arrayDiff function and determine the unmatched items on demand if I need them.
Cheers!

If you are worried about performance, you could have a non-observable array that you populate while you iterate the search results in your computed. Also note that you are repeatedly selecting using jQuery inside your loop, which I think negates any KO-caused slowdowns.
self.missedRecords = [];
self.matchedRecords = ko.computed(function() {
var searchQuery = $('.search-input').val().toLowerCase(),
transponders = self.transponders(),
matched = [];
// Clear out missed records
self.missedRecords.length = 0;
_.each(transponders, function(transponder) {
if (transponder.title.toLowerCase().indexOf(searchQuery) >= 0) {
matched.push(transponder);
} else {
self.missedRecords.push(transponder);
}
});
return matched;
});
I used _.each from Underscore to keep the code shorter. The drawback of this approach is that changes to missedRecords can't (reliably) be bound to the UI (e.g. in case you have a foreach binding).
If you do need the missedRecords array to be observable, and still want to keep things fast(er), you could do something like this:
self.missedRecords = ko.observableArray([]);
self.matchedRecords = ko.computed(function() {
var searchQuery = $('.search-input').val().toLowerCase(),
transponders = self.transponders(),
matched = [],
missed = [];
_.each(transponders, function(transponder) {
if (transponder.title.toLowerCase().indexOf(searchQuery) >= 0) {
matched.push(transponder);
} else {
missed.push(transponder);
}
});
// Clear out missed records, without triggering subscriptions
self.missedRecords().length = 0;
// Copy the local missed array to the KO observable array
// This will NOT trigger notifications
ko.utils.arrayPushAll(self.missedRecords(), missed);
// Tell KO that the observable array has mutated - this will trigger changes
// to anything observing the missedRecords array
self.missedRecords.valueHasMutated();
return matched;
});
You could also skip computed altogether and just subscribe to changes to change the state of your arrays. For example:
self.missedRecords = ko.observableArray([]);
self.matchedRecords = ko.observableArray([]);
self.transponders.subscribe(function(newTransponders) {
var matched = [],
missed = [];
_.each(newTransponders, function(transponder) {
// Populate matched/missed local arrays
});
// Copy the arrays to the observableArray instances using the technique above
});

Related

jQuery's .add on an empty set?

I am working on a plugin with quite a few options and as a consequence I am trying to keep track of a set of elements and put them in a variable. The variable cannot be empty (but that is of no concern here). Let's say there are only two options, then the variable will hold one or two elements as a jQuery object, i.e. $("#el1, #el2"). I tried the following, but the result of adding is still $([]).
var track = $([]);
someFunc() {
if (option1) track.add("#el1");
if (option2) track.add("#el2");
}
// result is `$([])`
Note that I don't want an array back, but a jQuery selector as I posted in the example above.
You could first sort out which elements/selectors you need.
And then use these to init the track variable with an jQuery object passing in all the relevant selectors.
var track = someFunc();
// you would have to check the length of `track` first as it may be only an empty array (length == 0) and no real jQuery object
if (track.length) {
//...
}
// returns a jQuery object with all the matched elements
// or an empty array if there is no relevant selector
function someFunc() {
// place to store the selectors
var selectors = [];
// store the relevant selectors in <selectors>
if (option1) selectors.push("#el1");
if (option2) selectors.push("#el2");
// if there is at least one selector in <selectors>
if (selectors.length > 0) {
// create a jQuery object of them and return it
return $(selectors.join())
} else {
// otherwise we return an empty array
// this allows us to use .length in both cases
return [];
}
// or always return a jQuery object
// return $(selectors.join());
}
Use a array join on coma:
var elements = [];
elements.push('#one');
console.log(elements.join(','));
$(elements.join(','));
https://jsfiddle.net/8xx8x1xe/

How to add/remove elements from array based on array contents

I've been struggling with this piece for a few days and I can't seem to find what's wrong. I have an array with a few objects:
myMainPostObj.categories = [Object, Object]
This is for add/removing categories to a post. Imagine I'm editing an existing post which is already associated with a couple of categories (as per code above).
I also have an array which has all categories in the db (including the ones which are associated with the post). On my js controller I have the following code:
$scope.addCategory = function (cat) {
for (var i in $scope.post.category_ids) {
if (cat._id === $scope.post.category_ids[i]) {
$scope.post.category_ids.slice(i, 1);
} else if (cat._id !== $scope.post.category_ids[i]) {
$scope.post.category_ids.push(cat._id);
}
}
}
The above function is called every time the user click on a category. The idea is for the above function to loop through the categories within the post (associated with the post) and compares it with the category passed as argument. If it matches the function should remove the category. If it doesn't it should add.
In theory this seems straight forward enough, but for whatever reason if I tick on category that is not associated with the post, it adds two (not one as expected) category to the array. The same happens when I try to remove as well.
This is part of a Angular controller and the whole code can be found here
The error in your code is that for each iteration of the loop you either remove or add a category. This isn't right... You should remove if the current id matches but add only if there was no match at all. Something like this:
$scope.addCategory = function (cat) {
var found = false;
for (var i in $scope.post.category_ids) {
if (cat._id === $scope.post.category_ids[i]) {
$scope.post.category_ids.splice(i, 1); // splice, not slice
found = true;
}
}
if (!found) // add only if it wasn't found
$scope.post.category_ids.push(cat._id);
}
I guess the problem could be that you're altering the category_ids array while you're iterating over it with the for loop. You might be better off trying something like this:
$scope.addCategory = function (cat) {
var catIndex = $scope.post.category_ids.indexOf(cat._id);
if (catIndex > -1)
$scope.post.category_ids.splice(catIndex, 1);
else
$scope.post.category_ids.push(cat._id);
}
Note that indexOf doesn't seem to be supported in IE7-8.
Let's simplify this a bit:
const CATEGORIES = [1, 2, 3];
let addCategory = (postCategories, categoryId) => {
CATEGORIES.forEach((cId, index) => {
if (postCategories[index] === cId) console.log("Removing", categoryId);
else console.log("Adding", categoryId);
});
return postCategories;
};
Please ignore the fact that we actually are not mutating the array being passed in.
A is either equal or not equal to B - there is no third option (FILE_NOT_FOUND aside). So you are looping over all of your categories and every time you don't find the category ID in the array at the current index you add it to the postCategories array.
The proper solution to your problem is just to use a Set (or if you need more than bleeding edge ES6 support, an object with no prototype):
// Nicer, ES6-or-pollyfill option
var postCategories = new Set();
postCategories.add(categoryId);
// Or, in the no-prototype option:
var postCategories = Object.create(null);
postCategories[categoryId] = true;
// Then serialization needs an extra step if you need an array:
parentObject.postCategories = Object.keys(parentObject.postCategories);

Is there a non loop-every-single-list-item approach to find unique list items?

I know I could use the loop-every-single-list-item approach to filter out unique elements in a given list, but I feel like there's probably a neat, quick way to do it.
How can I find unique list items in JavaScript, without looping through and filtering them manually?
Lately I was working on event handling patch and needed fast method for filtering out unique function handlers in a callback lists which got to be run quite frequently.
Here's what I'm trying to do:
Array.prototype.unique = (function () {
// main Array#unique method
var uni = function uni () {
return this.filter(uni.x);
};
// attach a helper for resolving unique elements
// if element is at current position, not before,
// it's unique one, pass `true` flag to .filter()
uni.x = function (node, pos, ls) {
return pos === ls.indexOf(node);
};
// save
return uniq;
})();
Implementation:
// sample list:
// generate ~1K long list of integers:
// get the keys of string object of length 32,
// map every item to key-list itself,
// flatten, shuffle..
var ls =
Array.prototype.concat.apply([],
Object.keys(new String('1'.repeat(32)))).
map(function (node, pos, list) { return list; }).
sort(function () { return Math.random() < Math.random(); });
// run each function 1K times fetching unique values
for (
var
it = -1,
l = 1000,
// record iteration start
tm = Date.now();
++it < l;
ls.unique()
);
No. If you have a list, you will need to look at least once at every single item to determine whether it is unique.
If you need something faster, don't use a list.
Btw, even on a list you can implement a unique-algorithm in less than the O(n²) that you currently have. See Easiest way to find duplicate values in a JavaScript array for some clever approaches.
I was working on event handling patch and needed fast method for filtering out unique function handlers in a callback list which got to be run quite frequently.
Then you don't want to put them in that list in the first place. Don't check the list for duplicates when you run it (which as you say is frequent), but when you insert a new handler.
If you think that using .indexOf to find a handler in the list is still too slow, you can mark every function object that it is already contained in the list. Choose a unique (per list) property name, and put a value on that property of each function that is in the list. You can then check in constant runtime for duplicates.
If you have a unique key, using a dictionary is a good option. However, if you have some logic that needs to be executed to perform your filtering, I'd go with UnderscoreJS. Check out the _.filter method. It's a great library with lots to offer in this area.
http://underscorejs.org/#filter
I don't think there is a way to get unique list of items without iterating through each item. If you're looking for a built-in library function, I don't think there is one in Angular.
It would be simple enough to create one:
function unique(array, fn) {
var hash = [];
var list = [];
for(var i = 0; i < array.length; ++i) {
var key = fn(array[i]);
if (key && !hash[key]) {
list.push(array[i]);
hash[key] = key;
}
}
return list;
}
Usage:
var myList = [ { id:1, name="oranges"},
{ id:2, name="apples" },
{ id:1, name="oranges"},
{ id:3, name="pears" },
{ id:3, name="pears" } ];
var uniqueList = unique(myList, function(item) { return item.id; });

change array passed to function

I pass 2 arrays to a function and want to move a specific entry from one array to another. The moveDatum function itself uses underscorejs' methods reject and filter. My Problem is, the original arrays are not changed, as if I was passing the arrays as value and not as reference. The specific entry is correctly moved, but as I said, the effect is only local. What do I have to change, to have the original arrays change as well?
Call the function:
this.moveDatum(sourceArr, targetArr, id)
Function itself:
function moveDatum(srcDS, trgDS, id) {
var ds = _(srcDS).filter(function(el) {
return el.uid === uid;
});
srcDS = _(srcDS).reject(function(el) {
return el.uid === uid;
});
trgDS.push(ds[0]);
return this;
}
Thanks for the help
As mentioned in the comments, you're assigning srcDS to reference a new array returned by .reject(), and thus losing the reference to the array originally passed in from outside the function.
You need to perform your array operations directly on the original array, perhaps something like this:
function moveDatum(srcDS, trgDS, id) {
var ds;
for (var i = srcDS.length - 1; i >= 0; i--) {
if (srcDS[i].uid === id) {
ds = srcDS[i];
srcDS.splice(i,1);
}
}
trgDS.push(ds);
return this;
}
I've set up the loop to go backwards so that you don't have to worry about the loop index i getting out of sync when .splice() removes items from the array. The backwards loop also means ds ends up referencing the first element in srcDS that matches, which is what I assume you intend since your original code had trgDS.push(ds[0]).
If you happen to know that the array will only ever contain exactly one match then of course it doesn't matter if you go forwards or backwards, and you can add a break inside the if since there's no point continuing the loop once you have a match.
(Also I think you had a typo, you were testing === uid instead of === id.)
Copy over every match before deleting it using methods which modify Arrays, e.g. splice.
function moveDatum(srcDS, trgDS, id) { // you pass an `id`, not `uid`?
var i;
for (i = 0; i < srcDS.length; ++i) {
if (srcDS[i].uid === uid) {
trgDS.push(srcDS[i]);
srcDS.splice(i, 1);
// optionally break here for just the first
i--; // remember; decrement `i` because we need to re-check the same
// index now that the length has changed
}
}
return this;
}

Nested jQuery loops - how can this be better accomplished?

I'm sprucing up a library I inherited and came across this little tidbit:
queries = urlParams.query.split("&");
// Split URL Queries
$.each(queries, function (i, val) {
val = val.split("=");
args[val[0]] = val[1]; //Assign query args into args object using their name (ie: test=123) as the key
});
// Loop through arguments
$.each(args, function (i, val) {
// Loop through affiliates to compare url arguments against those of the affiliates
$.each(self.affiliates, function (inc, value) {
if (value.urlTag === i) {
self.setAffiliateCookies(i, val, 1); //Set affiliate cookies
gotAff = true;
return false;
}
});
});
The gist of what's happening above is that it is parsing the querystring and breaking up the elements into key-value pairs. Easy enough.
What happens after that is that it loops over that new array and then tests to see if the value of args exists in the object literal value of self.affiliates.urlTag. If so, it sets a cookie, sets gotAff to true, and then returns false to kill the $.each.
Something about this doesn't seem very efficient to me. I've been playing with a recursive function and I'm not quite there, and I'm not sure if I'm going down the wrong path. I'm not sure that killing the $.each with a return false is the most efficient method either.
Any thoughts? Any tips? This sort of pattern is repeated in multiple places and I'd love to know how better to accomplish it.
If I understand the data structures correctly this might be a little cleaner:
$.each(self.affiliates, function (inc, value) {
if (args.hasOwnProperty(value.urlTag)) {
self.setAffiliateCookies(value.urlTag, args[value.urlTag], 1); //Set affiliate cookies
gotAff = true;
return false;
}
});
I'm pretty sure that no matter what it is an n² operation, because you have to go through each argument, and see if it is present in a list. The best you could do in this case is somehow clarify the code, but I'm not really sure you have the correct functions to do that within jQuery.
As an example, here's how I would do it in MooTools:
// Create array of affiliate URL tags.
var affiliateURLs = self.affiliates.map(function(affiliate) {
return affiliate.urlTag;
});
// Filter args list to those with affiliates.
// This is the n² part.
var matchedArgs = Object.filter(args, function(arg, argURL) {
return affiliateURLs.contains(argURL);
});
// Create cookie for each matched arg.
Object.each(matchedArgs, function(arg, argURL) {
self.setAffiliateCookies(argURL, arg, 1);
});
// Note that gotAff is simply
// (Object.getLength(matchedArgs) > 0)

Categories