Knockout js and sort issues - javascript

I've been searching online and it seems that I cannot make sort work on my observable array. I am definitely wrong somewhere but not sure where, here is the code:
var availableProducts = [{"Id":"1","Description":"Product 1","Rate":2956.00},{"Id":"3","Description":"Product 2","Rate":1518.00},{"Id":"2","Description":"Product 3","Rate":750.00}];
function productViewModel() {
var self = this;
self.products = ko.observableArray();
}
var productDetails = new productViewModel();
$(document).ready(function () {
productDetails.products = (availableProducts);
ko.applyBindings(productDetails, document.getElementById("product-edit"));
}
And HTML looks like this:
<tbody data-bind="foreach: sortedProducts">
<tr>
<td><span data-bind="text: Description"></span></td>
<td><span data-bind="currency: Rate"></span></td>
</tr>
</tbody>
So as it can be seen IDs are in order 1, 3, 2 and I would like to sort them and show them in order, but I cannot seem to sort products. I tried putting following in my ViewModel
self.sortedProducts = function () {
return self.products().sort(function (a,b) {
return a.Id < b.Id ? -1 : a.Id > b.Id ? 1 : 0;
});
This did not work, I tried then adding same code to "$(document).ready(...)" with exception of replacing self with productDetails but did not work. I do not want to make button to be called to sort my data, I want them sorted before presenting them.

The sort function looks OK, although you should check - preferrably with unit tests - whether your supported browsers actually understand the compareFunction handed to Array.prototype.sort. It's just an issue I ran into a while ago.
1) You are overwriting the ko.observableArray() with the native array availableProducts- this should give you an error when accessing the property with the getter syntax self.products().
2) The foreach binding in your template shouldn't work because you're handing it a plain function rather than an observable array, I'd guess that it actually iterates over the properties of the function object itself (.length, .prototype, .hasOwnProperty etc.).
3) I'd recommend using the Knockout Projections library that adds efficient observable handling to arrays in case you plan on handling larger arrays. In my experience with over more than say 1000 items in a collection the UI is not as fluid as you'd like it to be without the projections feature.
My take on this would be:
var availableProducts = [{"Id":"1","Description":"Product 1","Rate":2956.00},{"Id":"3","Description":"Product 2","Rate":1518.00},{"Id":"2","Description":"Product 3","Rate":750.00}];
function productViewModel() {
var self = this;
// In case you know the products at this stage you could just
// specify it as the first argument
self.products = ko.observableArray(/* [{...}] */);
// The sorted products are computed from the original ones
self.sortedProducts = ko.computed(function () {
// I'd probably use explicit parens for better
// readability - I had to look twice to get the comparer :-)
return self.products().sort(function (a, b) {
return a.Id < b.Id ? -1 : (a.Id > b.Id ? 1 : 0);
});
//// With ko-projections this would become:
//// (note the missing parens on `products`)
//return self.products.sort(function (a, b) {
// return a.Id < b.Id ? -1 : a.Id > b.Id ? 1 : 0;
//});
});
}
var productDetails = new productViewModel();
$(document).ready(function () {
// Set the observable array value
productDetails.products(availableProducts);
ko.applyBindings(productDetails, document.getElementById("product-edit"));
});

Related

React-table sort custom rendered cells containing two values or symbols

In this case, I have two values in each cell (like rank and value) but I only want to sort on one of the two but setting sortBy doesn't work. I tried creating a custom function using sortMethod, but I have no idea how it works like if I did something like that. (I just copy-pasted the function from some other response to see if I could get anything to even print)
sortMethod: (a, b) => {
console.log(a,b)
if (a.length === b.length) {
return a > b ? 1 : -1;
}
return a.length > b.length ? 1 : -1;
}
I have no idea what a and b are or how to reference them for my situation and I can't even get a and b to print to the console when I click which makes me think that I'm doing something so wrong that the function's not even triggering.
Also to render the cell, I couldn't figure out how to just add a value without adding a wrapped div, so it looks like this because I need two different styles for the two values
Cell: ({ row: { original } }) => (<div className = 'pctl' data-rk={original.percentile}>
{original.percentile}
<span className='stat'> {original.value}</span>
</div>),

Finding elements in an array of objects where you have a bigger key

I'm writing a tiny reactive framework where I need to find out which subscriber needs updating. I'm implementing deep binding and I'm running into a wall how to find subscribers in an effective manner.
A stored variable can be an object, so for example
{
"user": {
"preferences": {
"food": "vegetarian"
}
}
}
You can get content to any level of this variable like this
getVar("user_preferences_food");
getVar("user_preferences");
However, you can also update it like that
setVar("user_preferences_food", "meat");
setVar("user_preferences", {"food": "meat"});
But in case of the first setVar (user_preferences_food) how can I find the subscriber using getVar("user_preferences"); or even getVar("user"); most effectively.
I already got it working by splitting the var on _ and then one by one concatting the next level and merging all the resulting arrays. But this is very resource intensive. Especially if there are a lot of subscribers. There must be a better way to find them that is less resource intensive.
Edit: I left out part of the explanation.
There is a subscribe method too
subscribe("user", cb);
subscribe("user_preferences", cb);
subscribe("user_preferences_food", cb);
These subscriptions are stored in an array in the framework.
As soon as "user_preferences_food" is updated for example, all subscriptions above should be triggered. But obviously not subscribe('othervar');
simplification of the subscribe method:
var subscriptions = [];
function subscribe(var, callback){
subscriptions.push({var: var, cb: callback});
}
Simplification of getVar
vars = {};
getVar(var){
// find var in vars with this logic: https://stackoverflow.com/a/18937118/249710
// current exact match on subscribers, but need the "parents, grandparents etc here
var toUpdate = _.where(subscriptions, {
"var" : var
});
_.each(toUpdate, function(sub){ sub.cb();});
}
Storing or getting data as part of the key I've already got covered. It is just finding the subscribers in the most effective manner
ps: this is in an environment where I cannot rely on ES6 yet (not all users have it enabled), there is no DOM but I do have underscore included. (Titanium app development platform)
I would try to make a list for the callbacks, so you loop trough one list so you dont have to search, because you know the list is there with all the callbacks.
So if you call setVar('user_prefs') you set a seperate list with the root var. in this case its the user.
if any object is changed with setVar (in depth or not) you go to you're root var, get the list and loop trough this list with the callbacks.
The beauty of this is you can set a list with the root var,
var cbList[FIRSTVAR] this contains all the callbacks. No searching just loop.
Its the mongoDb principle, the data is ready to go, you don't search because you know the list is already there.
You could split the string and use it for reduceing the object.
function getVar(object, path) {
return path
.split('_')
.reduce(function (o, k) {
return (o || {})[k];
}, object);
}
function setVar(object, path, value) {
var keys = path.split('_'),
last = keys.pop();
keys.reduce(function (o, k) {
return o[k] = o[k] || {};
}, object)[last] = value;
}
var object = { user: { preferences: { food: "vegetarian" } } };
console.log(getVar(object, "user_preferences_food"));
console.log(getVar(object, "user_preferences"));
setVar(object, "user_preferences_food", "meat");
console.log(object);
setVar(object, "user_preferences", {"food": "meat"});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I ended up doing this:
var options = [];
var parts = key.split('_');
var string = parts[0];
_.each(parts, function(p, i){
if (i > 0) string += '_' + p;
options.push(string);
});
var toUpdate = _.filter(subscribers, function(sub){
if (sub.var.indexOf(key + '_') === 0) return true;
if (options.indexOf(sub.var) > -1) return true;
return false;
});
So checking with indexOf on the string to see if there are children. And building an array with parents so any layer is a match, and doing an indexOf on that as well. I think this is the least complicated method of implementing it

keep observableArrays sorted after push

I have two observable arrays, and I need to remove elements from the first one and push to the second one and vice versa. But when I do so, the alphabetical sorting is messed up.
self.allCourses = ko.observableArray([]);
self.selectedCourses = ko.observableArray([]);
I will interchange courses between the two arrays, and using this :
self.sortArrays = function(){
self.allCourses.sort(function (l, r) {
return l.code() < r.code() ;
});
self.selectedCourses.sort(function (l, r) {
return l.code() < r.code() ;
});
}
not only is it not efficient, but also doesnt work as expected ;I call the function each time I call one of these functions
self.addCourse = function(course){
self.selectedCourses.push(course);
self.allCourses.remove(course);
self.sortArrays();
};
self.removeCourse = function(course){
self.allCourses.push(course);
self.selectedCourses.remove(course);
self.sortArrays();
};
I would consider two approaches.
Keep your data always sorted. Instead of calling .sort(), search for the right location to put the element, and call .splice() to insert it in the right place. This is a O(n) algorithm, but should be fast in practice.
Use something like https://libraries.io/npm/dsjslib to maintain a sorted data structure at all times. This makes insert/delete a O(log(n)) operation. However every operation now has extra complexity.
Which one to use will depend on whether your operations are dominated by the effort of insert/delete, or by running through the list and displaying it. My best guess is that running through the list and displaying it matters more.
Furthermore the next question is whether it is better to do the search by scanning through the array, or by binary search. Scanning is O(n) but branch prediction mistakes cost so much that I've seen it be faster than binary search for inserting into lists of hundreds of elements.
Using knockout, u can also create computed based on your observable array, so you always will have sorted array
self.allCoursesSorted = ko.computed(function(){
return this.allCourses.sort(function (l, r) {
return l.code() < r.code() ;
});
}, this);
for selected courses you can use same approach but with filter
self.allCoursesSelected = ko.computed(function(){
return ko.utils.arrayFilter(this.allCoursesSorted(),
function (item) {
return item.selected === true;
});
}, this);
When removing an item from an array, you will never have to do a re-sort.
Instead of pushing and re-sorting, you could insert an item using your sort definition.
You'll only need to define the sorted inject function, since knockout observable arrays already have a remove method:
const sorter = (a, b) => a > b ? 1 : a < b ? -1 : 0;
const leftNumbers = ko.observableArray(
[3,5,1,2].sort(sorter)
);
const rightNumbers = ko.observableArray(
[4,1,3,5].sort(sorter)
);
// There are many ways to write this function, which you can probable
// find on stack overflow. The destructuring probably makes this slower
// than just re-sorting. I'll leave it up to you to optimize for performance.
const injectSorted = (sorter, arr, nr) => {
const pos = arr.findIndex(x => sorter(x, nr) > -1);
if (pos === -1) return arr.concat(nr);
return [
...arr.slice(0, pos),
nr,
...arr.slice(pos)
];
};
// Notice how we don't need to re-sort
const moveFromTo = (arr1, arr2) => x => {
arr2(injectSorted(sorter, arr2(), arr1.remove(x)));
};
ko.applyBindings({ leftNumbers, rightNumbers, moveFromTo });
div { display: flex; justify-content: space-around; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Click numbers to move between lists</p>
<div>
<ul data-bind="foreach: leftNumbers">
<li data-bind="click: moveFromTo(leftNumbers, rightNumbers), text: $data"></li>
</ul>
<ul data-bind="foreach: rightNumbers">
<li data-bind="click: moveFromTo(rightNumbers, leftNumbers), text: $data"></li>
</ul>
</div>

Updating ViewModel knockout with a nested loop

This may not be the completely ideal optimized way of accomplishing the task. I'm open for suggestions on any better ways. So far the loads work fine performancewise.
I have my knockout app working via ajax load. Inside the binding calls, I have a nested loop that includes a function that updates points based on a setting value.
When I attempt to add a new item, no errors are thrown, however the UI does not update and I can't seem to figure out why.
Here's a fiddle of what I'm trying to do.
http://jsfiddle.net/hjchvawr/2/
The addCombatant method does work, but for whatever reason the table will not rebind. You can see the added value in the VM json outputed to the console.
self.addCombatant = function(combatant){
ko.utils.arrayForEach(self.divisions(), function(d){
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
console.log(ko.toJSON(self.divisions));
}
)}.bind(this);
EDIT:
I've applied some updates suggested below and added another list to sort. It binds and updates however, when I add a combatant, it only binds to one event and the sorting is off. If I can't use sortDivision(combatants, 'swords'), how do would I make the automatic sorting work? In this fiddle (http://jsfiddle.net/4Lhwerst/2/) I want the event sorted by kills, then time. Is it possible to get this multilevel sorting done client side without creating another observeableArray?
This is the foreach binding in your table.
<!-- ko foreach: $root.sortDivision(combatants, 'swords') -->
sortDivision is defined:
self.sortDivision = function (div, evt) {
return div.sortBy(evt, 'time', 'asc').sortBy(evt, 'kills', 'desc');
};
Your sortBy function creates a new observableArray. That is not the same observableArray as is being pushed to.
ko.observableArray.fn.sortBy = function (evt, fld, direction) {
var isdesc = direction && direction.toLowerCase() == 'desc';
return ko.observableArray(this.sort(function (a, b) {
a = ko.unwrap(evt ? a[evt][fld]() : a[fld]());
b = ko.unwrap(evt ? b[evt][fld]() : b[fld]());
return (a == b ? 0 : a < b ? -1 : 1) * (isdesc ? -1 : 1);
}));
};
You should use computeds (or pureComputeds) for things that are a re-presentation or re-combination of data. Store any data item in one place.
You are pushing into the underlying combatants array and thus bypassing change tracking. Either remove the parentheses (d.combatants.push) or call valueHasMutated after you are done.
You need either:
if(d.name() == combatant.division){
d.combatants.push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
}
Or:
if(d.name() == combatant.division){
d.combatants().push({name: combatant.name,
ID: combatant.ID,
swords:{points: 0, time:'none', kills: 0}
});
d.combatants.valueHasMutated();
}

Javascript Array Crossbrowser issues

I am working on a simple Web App, where Users can comment articles chronologically (like comments on an blog article). Every comment has a timestamp. I use KnockoutJS for a client side view model and due to problems with the date object in IE9 I use MomentJS for crossbrowser timestamp parsing (the Timestamp Property of every comment is in fact a MomentJS generated object). Data comes from an REST Endpoint as JSON to the client where it is instantiated in the Knockout view model. The constructor of my article model looks like this (shortened):
GC.ko.modelArticle = function (a) {
this.Id = ko.observable(a.Id);
this.Title = ko.observable(a.Title).extend({ required: true, minLength: 3 });
: //some more Properties
this.Comments = ko.observableArray();
if (util.isDefined(a.Comments)) {
for (var i = 0; i < a.Comments.length; i++) {
this.Comments.push(new GC.ko.modelComment(a.Comments[i]));
}
}
this.Comments.sort(function (left, right) {
return left.Timestamp == right.Timestamp ? 0 : (left.Timestamp < right.Timestamp ? -1 : 1);
});
};
As you can see, if the JSON (a) contains comments, these are pushed onto a Knockout observableArray. Afterwards I am sorting the Array chronologically ascending, so that newer Comments appear after older ones in the UI.
In Firefox and Chrome the array gets sorted ascending, as it should.
In IE9 it is sorted descending.
Does this happen because of
crossbrowser issues of the Array().push() function
or the Array().sort() function,
or because Knockout observable Arrays have issues with sorting,
or is it because of some error in my code?
EDIT: comment.Timestamp is an Knockout Observable. I tried two variants:
First returning a plain Javascript Date Object (which had Timestamp parsing issues in IE, so I had to modify this):
this.Timestamp = ko.observable(c.Timestamp)
Second returning a moment Object:
this.Timestamp = ko.observable(moment(c.Timstamp)
'c' is the JSON for a comment
EDIT 2: It turns out, that the sort() function of observableArray() in Knockout 2.2.1 seems to be the problem. I modified my code to the following (first sorting the plain javascript array, then pushing the elements to the KO Observable Array) and everything works as it should now. Here's the code:
GC.ko.modelArticle = function (a) {
this.Id = ko.observable(a.Id);
this.Title = ko.observable(a.Title).extend({ required: true, minLength: 3 });
: //some more Properties
this.Comments = ko.observableArray();
if (util.isDefined(a.Comments)) {
a.Comments.sort(function(left,right) {
return left.Timestamp == right.Timestamp ? 0 : (left.Timestamp < right.Timestamp ? -1 : 1);
});
for (var i = 0; i < a.Comments.length; i++) {
this.Comments.push(new GC.ko.modelComment(a.Comments[i]));
}
}
};
Given that .Timestamp is a moment you should use the .isBefore() and .isSame() comparison methods as follows:
this.Comments.sort(function (left, right) {
return left.Timestamp.isSame(right.Timestamp) ? 0 : (left.Timestamp.isBefore(right.Timestamp) ? -1 : 1);
});

Categories