this.col = Backbone.Collection.extend({
model: M,
comparator: function(item) {
return item.get("level");
}
});
This above code sorts items by level. I want to sort by level, then by title. Can I do that? Thanks.
#amchang87's answer definitely works, but another that I found worked is simply returning an array of the sortable fields:
this.col = Backbone.Collection.extend({
model: M,
comparator: function(item) {
return [item.get("level"), item.get("title")]
}
});
I haven't tested this in multiple browsers yet as I think it relies on JS' behavior in sort order for arrays (based on their contents). It definitely works in WebKit.
String concatenation works fine when sorting multiple fields in ascending order, but it didn't work for me because 1) I had to support asc/desc per field and 2) certain fields were number field (i.e., I want 10 to come after 2 if it is ascending). So, below was a comparator function I used and worked OK for my needs. It assumes the backbone collection has a variable assigned with 'sortConfig', which is an array of JSON objects with field name and sort order direction. For example,
{
"sort" : [
{
"field": "strField",
"order": "asc"
},
{
"field": "numField",
"order": "desc"
},
...
]
}
With the JSON object above assigned as 'sortConfig' to the collection, the function below will make Backbone sort by strField in ascending order first, then sort by numField in descending order, etc. If no sort order is specified, it sorts ascending by default.
multiFieldComparator: function(one, another) {
// 'this' here is Backbone Collection
if (this.sortConfig) {
for (var i = 0; i < this.sortConfig.length; i++) {
if (one.get(this.sortConfig[i].field) > another.get(this.sortConfig[i].field)) {
return ("desc" != this.sortConfig[i].order) ? 1 : -1;
} else if (one.get(this.sortConfig[i].field) == another.get(this.sortConfig[i].field)) {
// do nothing but let the loop move further for next layer comparison
} else {
return ("desc" != this.sortConfig[i].order) ? -1 : 1;
}
}
}
// if we exited out of loop without prematurely returning, the 2 items being
// compared are identical in terms of sortConfig, so return 0
// Or, if it didn't get into the if block due to no 'sortConfig', return 0
// and let the original order not change.
return 0;
}
Returning an array is not consistent if you need to sort descending and some ascending...
I created a small set of functions which can be used to return the relevant comparison integer back to Backbone Comparator function:
backbone-collection-multisort
The main thing is that Backbone sorts by a single relative value of one item to another. So it's not directly possible to sort twice in a single collection but I'd try this.
this.col = Backbone.Collection.extend({
model: M,
comparator: function(item) {
// make sure this returns a string!
return item.get("level") + item.get("title");
}
});
What this will do is return a string of like "1Cool", "1title", "2newTitle" ...
Javascript should sort the strings by the numerical character first then each character afterwards. But this will only work as long as your levels have the same amount of digits. IE "001title" vs "200title". The main idea though is that you need to produce two comparable objects, line a number or string, that can be compared to each other based on one criteria.
Other solution would be to use underscore to "groupby" your level then use "sortby" to manually sort each level group then manually replace the underlying collection with this newly created array. You can probably setup a function to do this whenever the collection "changes".
"inspired" in hyong answer.
This also allows you to change the data before compare it, valueTransforms is an object, if there is an attribute in that object that has a function, it will be used.
/*
* #param {Object} sortOrders ie:
* {
* "description": "asc",
* "duedate": "desc",
* }
* #param {Object} valueTransforms
*/
setMultiFieldComparator: function(sortOrders, valueTransforms) {
var newSortOrders = {}, added = 0;
_.each(sortOrders, function(sortOrder, sortField) {
if (["asc", "desc"].indexOf(sortOrder) !== -1) {
newSortOrders[sortField] = sortOrder;
added += 1;
}
});
if (added) {
this.comparator = this._multiFieldComparator
.bind(this, newSortOrders, valueTransforms || this.model.prototype.valueTransforms || {});
} else {
this.comparator = null;
}
},
_multiFieldComparator: function(sortOrders, valueTransforms, one, another) {
var retVal = 0;
if (sortOrders) {
_.every(sortOrders, function(sortOrder, sortField) {
var oneValue = one.get(sortField),
anotherValue = another.get(sortField);
if (valueTransforms[sortField] instanceof Function) {
oneValue = valueTransforms[sortField](oneValue);
anotherValue = valueTransforms[sortField](anotherValue);
}
if (oneValue > anotherValue) {
retVal = ("desc" !== sortOrder) ? 1 : -1;
} else if (oneValue < anotherValue) {
retVal = ("desc" !== sortOrder) ? -1 : 1;
} else {
//continue
return true;
}
});
}
return retVal;
},
Related
I have a Vue component that builds the below into a blog form field. The writer is allowed to creatively add/slot any field of choice in between each other when building a blog post ...(like: title, paragraph, blockquote, image) in an object like:
{"post":{"1":{"title":{"name":"","intro":""}},"2":{"paragraph":{"text":"","fontweight":"default-weight","bottommargin":"default-bottom-margin"}},"3":{"image":{"class":"default-image-class","creditto":""}},"4":{"subheading":{"text":"","size":"default"}}}};
I've tried using jQuery each to iterate and add it up into a makedo "dataObj" object and inject it back on the data:
data: { treeData: myUserData.post },
injectFieldType: function(type, position){
var storeObj = {};
var dataObj = this.treeData;
var crntKey;
$.each( dataObj, function( key, value ) {
if(key < position)
{
//remain same as key is not to change
}
else if(key == position)
{
dataObj[''+(parseInt(key)+1)] = dataObj[key]; /*push key further right with +1*/
dataObj[key] = /*add injected field here*/;
}
else if(key > position)
{
dataObj[''+(parseInt(key)+1)] = dataObj[key]; /*push the rest*/
}
});
and inject it back with (this.treeData = dataObj;) when it has injected the desired key and has shifted the rest by adding 1 to their keys when this is clicked:
<button type="button" v-on:click="injectFieldType('image','2')">
I need to have {"post":{"1":{"title":{"name":"","intro":""}},"2":{"image":{"class":"default-image-class","creditto":""}},"3":{"paragraph":{"text":"","fontweight":"default-weight".... When I try to inject the image field in-between the existing "name" and "paragraph" fields and make the paragraph key now 3 (instead of the old 2).
I want "{1:{foo}, 2:{bar}"} to become => {"1:{foo}, 2:{moo}, 3:{bar}" }(notice 3 changed key)
NOTE: the number order is needed to align them reliably in publishing. And data: { treeData: myUserData.post } needs to agree with the changes to allow creating the field and updating each form "name" attribute array.
There are a few problems to address here.
Firstly, trying to use var dataObj = this.treeData; and then this.treeData = dataObj isn't going to help. Both dataObj and this.treeData refer to the same object and that object has already been processed by Vue's reactivity system. You could address the reactivity problems by creating a totally new object but just creating an alias to the existing object won't help.
Instead of creating a new object I've chosen to use this.$set in my example. This isn't necessary for most of the properties, only the new one added at the end really needs it. However, it would have been unnecessarily complicated to single out that one property given the algorithm I've chosen to use.
Another potential problem is ensuring all numbers are compared as numbers and not as strings. In your example you're passing in the position as the string '2'. Operators such as < will give you the expected answer for numbers up to 9 but once the number of items in treeData reaches 10 you may start to run into problems. For string comparision '2' < '10' is false.
The next problem is the order you're moving the entries. In your current algorithm you're overwriting entry key + 1 with entry key. But that means you've lost the original value for entry key + 1. You'll end up just copying the same entry all the way to the end. There are two ways you could fix this. One would be to use a new object to hold the output (which would also help to address the reactivity problem). In my solution below I've instead chosen to iterate backwards through the keys.
new Vue({
el: '#app',
data () {
return {
newEntry: 'Yellow',
newIndex: 4,
treeData: {
1: 'Red',
2: 'Green',
3: 'Blue'
}
}
},
computed: {
treeDataLength () {
return Math.max(...Object.keys(this.treeData))
}
},
methods: {
onAddClick () {
const newIndex = Math.round(this.newIndex)
if (newIndex < 1 || newIndex > this.treeDataLength + 1) {
return
}
this.injectFieldType(this.newEntry, newIndex)
},
injectFieldType (type, position) {
const list = this.treeData
for (let index = this.treeDataLength + 1; index >= position; --index) {
if (index === position) {
this.$set(list, index, type)
} else {
this.$set(list, index, list[index - 1])
}
}
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="index in treeDataLength">
{{ index}}. {{ treeData[index] }}
</li>
</ul>
<input v-model="newEntry">
<input v-model="newIndex">
<button #click="onAddClick">Add</button>
</div>
The decision to use an object with number keys seems very strange. This would all be a lot easier if you just used an array.
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
I have this algorithme issue, I would like to check if an Object is already present in my Array before adding it.
I tried many different approaches (indexOf, filter...), and my last attempt is with an angular.foreach.
The problem is my $scope.newJoin remains always empty. I understood why, it's because the if is never read, because of the 0 size of my $scope.newJoin, but I don't know how to figure this out...
$scope.newJoinTMP is composed by : 6 Objects, within each a timePosted attribute (used for compare these different array Objects).
$scope.newJoin is an empty Array. I want to fill it with the Objects inside $scope.newJoinTMP but with the certainty to have once each Objects, and not twice the same ($scope.newJoinTMP can have duplicates Objects inside, but $scope.newJoin mustn't).
angular.forEach($scope.newJoinTMP, function(item)
{
angular.forEach($scope.newJoin, function(item2)
{
if (item.timePosted === item2.timePosted)
{
//snap.val().splice(snap.val().pop(item));
console.log("pop");
}
else
{
$scope.newJoin.push(item);
console.log("newJoin :", $scope.newJoin);
}
});
});
if(!$scope.newJoin.find(el=>item.timePosted===el.timePosted){
$scope.newJoin.push(item);
console.log("newJoin :", $scope.newJoin);
}
You dont want to push inside an forEach, as it will push multiple times...
There might be better ways to handle your particular situation but here's a fix for your particular code.
Replaced your inner for each with some which returns boolean for the presence of element and by that boolean value, deciding whether to add element or not
angular.forEach($scope.newJoinTMP, function(item)
{
var isItemPresent = $scope.newJoin.some(function(item2)
{
return item.timePosted === item2.timePosted;
//you dont need this conditional handling for each iteration.
/* if (item.timePosted === item2.timePosted)
{
//snap.val().splice(snap.val().pop(item));
console.log("pop");
}
else
{
$scope.newJoin.push(item);
console.log("newJoin :", $scope.newJoin);
} */
});
if( ! isItemPresent ) {
$scope.newJoin.push(item);
} else {
//do if it was present.
}
});
If you want to avoid the nested loop (forEach, some, indexOf, or whatever) you can use an auxiliar object. It will use more memory but you will spent less time.
let arr = [{ id: 0 }, { id:0 }, { id: 1}];
let aux = {};
const result = arr.reduce((result, el) => {
if (aux[el.id] === undefined) {
aux[el.id] = null;
return [el, ...result];
} else {
return result;
}
}, []);
console.log(result);
You can use reduce
$scope.newJoin = $scope.newJoinTMP.reduce(function(c, o, i) {
var contains = c.some(function(obj) {
return obj.timePosted == o.timePosted;
});
if (!contains) {
c.push(o);
}
return c;
}, []);
The problem with your current code is, if newJoin is empty, nothing will ever get added to it - and if it isnt empty, if the first iteration doesn't match the current item being iterated from newJoinTMP - you're pushing.
I would like to add an improvement to a currenct filter of mine which takes a collection and filters it by rating (1/2/3/4/5).
At the moment it can only filter by one of these (1/2/3/4/5) numbers but I would like to filter using all of them or part of them together. sorting doesn't matter.
filter_by_rating: function(rating) {
var filtered;
if (!!rating && rating !== '') {
filtered = this.filter(function(model) {
return model.get("rating") >= parseInt(rating) && model.get("rating") <= parseInt(rating) + 0.5;
});
return new Collection(filtered);
} else {
}
return this;
}
If in the current function it accepts only one number , I would like to pass a string of numbers , for example: "3,4,5" and it should filter and bring only the models with the rating of 3 4 and 5
Also, if I have in the array the numbers ["3","4","5"] I would it to filter also "3.5", "4.5"
Use indexOf to check if the rating is in your array.
filter_by_rating: function(rating){
if(!Array.isArray(rating)) {
rating = [rating];
}
return new Collection(this.filter(function(model){
return rating.indexOf(Math.floor(model.get('rating'))) !== -1;
}));
}
I am using backbone's collection model to display a sorted list of strings on a backbone view. Here is the model and the comparator:
var MenuItems = Backbone.Collection.extend({
comparator: function (a, b) {
if (a.get('name') < b.get('name')) {
return 1;
} else if (b.get('name') > a.get('name')) {
return -1;
}
},
model: MenuItem,
url: '/items'
});
When the code is run, only the first six of the twelve items in the list are sorted, the rest remains unsorted. When comparator: 'name' is used the list is fully sorted, but when a function is used, this problem occurs.
Anyone know why this might be happening? Could this be a Backbone bug? I am using Backbone 1.1.0
Here is a working code.
var MenuItems = Backbone.Collection.extend({
comparator: function (a, b) {
if (a.get('name') < b.get('name')) {
return -1;
} else if (a.get('name') > b.get('name')) {
return 1;
}
}
});
Here is jsfiddle with output so you can compare http://jsfiddle.net/ek44Z/2/
The main problem was with function content. You need return -1 in if statement and compare a and b in else if and return 1. Basically your else if have never been called.
Have a good coding.