Sortable table with backbone collection - javascript

I want to create a table that is sortable using the attributes of a collection.
So far i've being able to make the tab table sortable using two attributes, but i would like to be sortable based the value of the sort key attribute.
e.g when the "task_status = 'open'"
Here what i having working now
var TaskCollection = Backbone.Collection.extend({
//Model
model:Task,
//url
url:"./api/tasks",
//construct
initialize: function() {
this.sort_key = 'end';
this.fetch();
},
comparator: function(a,b) {
a = a.get(this.sort_key);
b = b.get(this.sort_key);
return a > b ? 1
: a < b ? -1
: 0;
},
sort_by_status: function() {
this.sort_key = 'task_status';
this.sort();
},
sort_by_task_tag: function() {
this.sort_key = 'task_group';
this.sort();
}
});
This sorts the collection but does not reverse the order, or allow me to sort by the particular value of an attribute. How can this be modified to work

In the comparator, have a state variable for "reversed" and have it take values 1 and negative 1. Multiply it by the previous return value. Setting the state variable on the collection and then again sorting should get things done.

Related

UI5 remove duplicate feed list items

I have a Feed List for posting comments in my UI5 xml view
<layout:content>
<m:FeedInput post="onFeedPost" class="sapUiSmallMarginTopBottom"/>
<m:List id="feedList" showSeparators="Inner" items="{path: '/table', sorter: {path: 'DATE', descending: true}}">
<m:FeedListItem sender="{MEMBERID}" timestamp="{DATE}" text="{COMMENT}" convertLinksToAnchorTags="All"/>
</m:List>
</layout:content>
I want to not display duplicate comments that have the same text and date, but keep them in the database. My idea was to in the controller iterate over over the items to do this, but I am not sure what to do with the resulting array
var results = [];
var comments = feed.getItems();
for (var n = 0; n < comments.length - 1; n++) {
var contained = false;
for (var m = n + 1; m < comments.length; m++) {
if (comments[n].getText() === comments[m].getText() &&
comments[n].getDate() === comments[m].getDate()) {
comments.pop(m);
contained = true;
if (!results.includes(comments[n])) {
results.push(comments[n]);
}
}
}
if (!contained && !results.includes(comments[n])) {
results.push(comments[n]);
}
}
// replace list items with results array
I can't figure out how to replace the feed list's items with the new array as there is a getItems function but not a setItems function. It occurs to me there is probably a simpler more idiomatic UI5 way to do this but I haven't found it yet.
First off, the correct way to handle this situation is in the OData service. The service should remove the duplicates before sending the data to the client. If we assume, however, that you can't do this server side, then you have some options.
1.) Do not bind the list items to anything. Instead, use the ODataModel to read the data, then filter out duplicates, create a new list item and add it to the list
Read the data using the ODataModel, then pass the results to a method that will filter and add them items to the list
oModel.read("/EntitySet", {
success: function(oResponse) {
this._addCommentsToList(oResponse.results)
}.bind(this)
})
In your method to handle the results, you'll need to do three things -- create a new FeedListItem, set the binding context of the list item, and then add the list item to the list
var aDistinctComments = //use your logic to filter out duplicates
aDistinctComments.forEach(function(oComment) {
//to set the binding context, you'll need the entity key/path
var sCommentKey = oModel.createKey("/EntitySet", oComment)
//create a new binding context
var oContext = oModel.createBindingContext(sCommentKey)
//create a new FeedListItem
var oItem = new FeedListItem({
sender: "{MemberId}",
...
});
//set the context of the item and add it to the list
oItem.setBindingContext(oContext);
oList.addItem(oItem);
})
2.) Bind the list directly to the OData entity set and then when the list receives the data, iterate over the items and hide the duplicates
<List items="{/EntitySet}" updateFinished="onListUpdateFinished"....>
----- onListUpdateFinished ---
var aItems = oList.getItems();
for (var m = n + 1; m < aItems.length; m++) {
//set a boolean, true if duplicate
var bDuplicate = aItems[m].getText() ==== aItems[n].getText() &&
aItems[m].getDate() === aItems[n].getDate();
//set the visibility of the item to true if it is not a duplicate
aItems[m].setVisible(!bDuplicate)
}
3.) Read the data manually, remove duplicates, and stash it in a JSON model, and bind the table to your JSON model path
oModel.read("/EntitySet", {
success: function(oResponse) {
this._addCommentsToJSONModel(oResponse.results)
}.bind(this)
})
You can stash an array of objects in your JSON model, and then bind the table items to that path
var aDistinctComments = // your logic to get distinct comments
oJSONModel.setProperty("/comments", aDistinctComments)
oList.setModel(oJSONModel);
-----
<List items="{/comments"}....>
4.) Bind your list items to your entity set, iterate over the items, and then remove duplicates from the list. I don't recommend this approach. Removing items manually from lists bound to an entity set can lead to trouble with duplicate IDs.
var oItem = //use your logic to find a duplicate list item
oList.removeItem(oItem)
I recommend first handling this server side in the OData service, and if that's not an option, then use option 1 above. This will give you the desired results and maintain the binding context of your list items. Options 2 and 3 will get you the desired results, but depending on your applicaiton, may make working with the list more difficult.
Here is one approach :
Do not directly bind the list to your oData.
You can create a JSON model which will be the resulting model after removing duplicate items.
Bind the JSON model to the List as such:
var oList = this.getView().byId("feedList");
oList.bindAggregation("items", "pathToJsonArray", template);
(The template is feedlistitem in this case).

Efficient Marionette sorted CollectionView for multiple attributes

I am trying to find a way to efficiently display a sorted Marionette.CollectionView, sorted by multiple attributes without modifying the underlying Backbone collection. For this example, I am using 'name' and 'online' attributes, and want my CollectionView to be displayed in 2 parts:
online, alphabetically
offline, alphabetically
I have a single Collection of Backbone.Models and want to use this across my web application. So having a sort function on my Collection doesn't feel right to me.
My example code is as follows:
var MyCollection = Backbone.Collection.extend({
model:Backbone.Model
});
var myCollection = new MyCollection();
myCollection.set([
{ name : 'Freddy', online : true },
{ name : 'Zorro', online : false },
{ name : 'Charlie', online : false },
{ name : 'Alice', online : true }
]);
var MyView = ...
/*
omitted for brevity, though my template is like
<li>{{name}} {{#if online}}(Online){{/if}}</li>
*/
var MyCollectionView = Marionette.Collection.extend({
childView:MyView,
viewComparator: function (item1, item2) {
var item1Online = item1.get('online');
var item2Online = item2.get('online');
if (item1Online != item2Online)
return item1Online ? -1 : 1;
var item1Name = item1.get('name');
var item2Name = item2.get('name');
return item1Name.localeCompare(item2Name);
}
});
var myCollectionView = new MyCollectionView({collection:myCollection});
appView.getRegion('example').show(myCollectionView);
I would like this to be displayed as:
Alice (Online)
Freddy (Online)
Charlie
Zorro
This is fine when all of the data is added to the collection at once, or add/remove events but if one of the attributes is updated on a model that is already in the collection, the view does not update.
If Charlie's 'online' property changed to true - e.g by performing.
charlieModel.set('online', true)
I would like the CollectionView to have rendered automatically as:
Alice (Online)
Charlie (Online)
Freddy (Online)
Zorro
Any suggestions? Many thanks in advance.
From the backbone documentation
Collections with a comparator will not automatically re-sort if you later change model attributes, so you may wish to call sort after changing model attributes that would affect the order.
You can put somewhere convenient in your code a listener on a change in the model attributes your are targeting that will trigger a re-sort of your collection.
// for example in your collection
initialize: function() {
this.on('change:name', function() { this.sort() }, this);
}
The Marionette team advised having a separate Backbone collection behind the scenes, rather than using the viewComparator on the CollectionView.
Using reorderOnSort on the CollectionView made a huge difference (at least 10x speed up) in terms of render speed.
This option is useful when you have performance issues when you resort your CollectionView.
Without this option, your CollectionView will be completely re-rendered, which can be
costly if you have a large number of elements or if your ChildViews are complex. If this option
is activated, when you sort your Collection, there will be no re-rendering, only the DOM nodes
will be reordered.
My final example code:
var MyView = Backbone.Marionette.ItemView.extend({
modelEvents:{
'change:name change:online': 'render'
},
template:template
});
var MyCollection = Backbone.Collection.extend({
initialize : function(){
this.on('change:name change:online', this.sort, this);
},
comparator : function(item1, item2){
var item1online = item1.get('online');
var item2online = item2.get('online');
if (item1online != item2online)
return item1online ? -1 : 1;
return item1.get('name').localeCompare(item2.get('name'));
}
});
var myCollection = new MyCollection();
var MyCollectionView = Marionette.CollectionView.extend({
childView : MyView,
reorderOnSort : true
});
var myCollectionView = new MyCollectionView({
collection : myCollection
});
appView.region('example').show(myCollectionView);

Reverse sorting backbonejs

I am using the below function to sort a backbone collection. I am sorting the collection with reference to Date(creationdate). The output I am fetching has oldest item first, But I want the output to be as latest item first and the oldest item last.
var campaignsNewCampaigns = _.sortBy(campaigns, function(campaign) {
if (campaign.creationdate) {
var getDate = campaign.creationdate.replace("-", "").replace("-", "").replace(":", "").replace(":", "");
return -Number(getDate);
}
return 0;
});
In backbone.js, you have a comparator field and sort function.
To Set your collection sort, you should add this function on your collection :
//...
sortByDate : function(){
this.comparator = function(campaign){
// I consider creationDate is a Date Object
return -campaign.get('creationdate').getTime();
};
this.sort();
},
//...
And now, to sort your collection, call this method like this :
var campaignsNewCampaigns = campaigns.sortByDate();
I got the solution for this.
if (campaign.creationdate) {
var getDate = (new Date(campaign.creationdate)).getTime();
return -Number(getDate);
}

Ember.js: Group a model (using ArrayProxy)

I have an array whose items I want to group, and then display in this grouped fashion. It's all terribly confusing:
App.GroupedThings = Ember.ArrayProxy.extend({
init: function(modelToStartWith) {
this.set('content', Ember.A());
this.itemsByGroup = {};
modelToStartWith.addArrayObserver(this, {
willChange: function(array, offset, removeCount, addCount) {},
didChange: function(array, offset, removeCount, addCount) {
if (addCount > 0)
// Sort the newly added items into groups
this.add(array.slice(offset, offset + addCount))
}
});
},
add : function(things) {
var this$ = this;
// Group all passed things by day
things.forEach(function(thing) {
var groupKey = thing.get('date').clone().hours(0).minutes(0).seconds(0);
// Create data structure for new groups
if (!this$.itemsByGroup[groupKey]) {
var newArray = Ember.A();
this$.itemsByGroup[groupKey] = newArray;
this$.get('content').pushObject({'date': groupKey, 'items': newArray});
}
// Add to correct group
this$.itemsByGroup[groupKey].pushObject(thing);
});
}
});
App.ThingsRoute = Ember.Route.extend({
model: function() {
return new App.GroupedThings(this.store.find('thing'));
},
});
This only works if I use the following template:
{{#each model.content }}
These don't render anything (an ArrayController is used):
{{#each model }}
{{#each content }}
{{#each}}
Why? Shouldn't the ArrayController proxy to "model" (which is GroupedThings), which should proxy to "content"?
The reason this becomes a problem is that I then want to sort these groups, and as soon as I change the entire contents array (even using ArrayProxy.replaceContent()), the whole views rebuilt, even if only a single item is added to a single group.
Is there a different approach to this entirely?
I've tended to use ArrayProxies slightly differently when doing such things.
I'd probably get Ember to do all the heavy lifting, and for sorting get it to create ArrayProxies based around a content collection, that way you can sort them automatically:
(note I haven't run this code, but it should push you off in the right direction)
App.GroupedThings = Em.ArrayProxy.extend({
groupKey: null,
sortKey: null,
groupedContent: function() {
var content = this.get('content');
var groupKey = this.get('groupKey');
var sortKey = this.get('sortKey');
var groupedArrayProxies = content.reduce(function(previousValue, item) {
// previousValue is the reduced value - ie the 'memo' or 'sum' if you will
var itemGroupKeyValue = item.get('groupKey');
currentArrayProxyForGroupKeyValue = previousValue.get(itemGroupKeyValue);
// if there is no Array Proxy set up for this item's groupKey value, create one
if(Em.isEmpty(currentArrayProxyForGroupKeyValue)) {
var newArrayProxy = Em.ArrayProxy.createWithMixins(Em.SortableMixin, {sortProperties: [sortKey], content: Em.A()});
previousValue.set(itemGroupKeyValue, newArrayProxy);
currentArrayProxyForGroupKeyValue = newArrayProxy;
}
currentArrayProxyForGroupKeyValue.get('content').addObject(item);
return previousValue;
}, Em.Object.create());
return groupedArrayProxies;
}.property('content', 'groupKey', 'sortKey')
);
You'd then Create a GroupedThings instance like this:
var aGroupedThings = App.GroupedThings.create({content: someArrayOfItemsThatYouWantGroupedThatHaveBothSortKeyAndGroupKeyAttributes, sortKey: 'someSortKey', groupKey: 'someGroupKey'});
If you wanted to get the groupedContent, you'd just get your instance and get.('groupedContent'). Easy! :)
...and it'll just stay grouped and sorted (the power of computed properties)... if you want an 'add' convenience method you could add one to the Em.Object.Extend def above, but you can just as easily use the native ones in Em.ArrayProxy, which are better IMHO:
aGroupedThings.addObjects([some, set, of, objects]);
or
aGroupedThings.addObject(aSingleObject);
H2H

JavaScript: How do I dynamically "filter" my objects

How do I user the JavaScript "filter" attribute as filter my JavaScript object?
I've been reading the following StackOverflow post, and am in a similar situation.
I have the following JavaScript object:
{
'cars' :
[{
"car_id" : "1",
"price" : "42999",
"make_id" : "050",
"year_built" : "2007",
"color_id" : "832"
},
..........
]}
I'm using JQuery to display controls to allow people to filter based on: Price, Make, Year Built, Color
Per that other post, I can use the following code:
// if using an old browser, define the 'filter' attribute
if (!Array.prototype.filter)
{
Array.prototype.filter = function(fun /*, thisp*/)
{
var len = this.length >>> 0;
if (typeof fun != "function")
throw new TypeError();
var res = new Array();
var thisp = arguments[1];
for (var i = 0; i < len; i++)
{
if (i in this)
{
var val = this[i]; // in case fun mutates this
if (fun.call(thisp, val, i, this))
res.push(val);
}
}
return res;
};
}
then to perform the actual filter, I can do:
result = cars.
filter(function(p) { return p.price >= 15000 }).
filter(function(p) { return p.price <= 40000 }).
filter(function(p) { return p.year_built >= 2000 }) etc
What I don't understand is, how do I use my JQuery controls to dynamically change the filter once the filter has already been set? Meaning, let's say I have the filter applied from above, then the user changes there mind and wants to increase the maximum they are willing to pay for a car from $40,000 to $50,000.
How would I problematically modify my filter from :
filter(function(p) { return p.price <= 40000 }).
to:
filter(function(p) { return p.price <= 50000 }).
how do I use my JQuery controls to dynamically change the filter once the filter has already been set?
You don't set a filter. You call filter() with a filter function and get a filtered array back; you can't change the filter that was applied to the array afterwards. Instead you must call filter() again, and pass a different filter function.
Or the same filter function with a closure over a variable that has changed:
var minprice= 10000;
var minpricefilter= function(p) { return p.price>=minprice };
result= cars.filter(minpricefilter);
minprice= 20000;
result= cars.filter(minpricefilter);
You could use a function generator.
function createPriceFilter(price)
{
filter = function(){ return p.price >= price };
return filter;
}
Then, when you filter, always use a function generator.
cars
.filter( createPriceFilter( mySelectedPrice ) )
.filter( createSomethingFilter(...) )
. (...)
Instead of filter, how about a plain old loop:
var min_year = 2000;
var min_price = 15000;
var max_price = 40000;
function fillTable() {
clearTheTable();
for (i = 0; i < cars.length; i++) {
var car = cars[i];
if (p.price >= min_price && p.price <= max_price && p.year_built >= min_year)
addCarToTable(car);
}
}
Each time your parameters change, just call fillTable() again to regenerate the whole table.
(There are much cleverer things you can do but this is the simplest thing I could think of.)
Forget callback based filtering. Enter jOrder: http://github.com/danstocker/jorder.
Filtering by iterating over your entire table is tedious and slow. With jOrder, you search by index:
var table = jOrder(json.cars)
.index('id', ['car_id'])
.index('price', ['price'], { grouped: true, ordered: true, type: jOrder.number })
.index('maker', ['maker_id'], { grouped: true })
.index('year', ['year_built'], { grouped: true, ordered: true, type: jOrder.number })
.index('color', ['color_id'], { grouped: true });
Then you get the records you want by:
var filtered = table.where([{ price: { lower: 15000, upper: 40000 } }], { mode: jOrder.range });
Note that you can only apply one inequality filter at a time. To do more, use filtered as an input for a different jOrder table, put only the necessary index on it, and perform the second inequality filter on that one. And so on. Even if you stack up a couple of filters like this, it will be still faster than iteration by a factor of about 10 to 100 depending on the size of your table.

Categories