The Backbone.Collection object proxies Underscore's Collection.shuffle() so all Backbone collections have the method shuffle().
Underscore's shuffle works fine:
If I wanted to shuffle only the last 5 items in an underscore collection, it can be done with slice():
You can also slice and shuffle a Backbone Collection:
But for a Backbone Collection that fetches items multiple times from a database, I cannot seem to shuffle only the items that were fetched by the last fetch call, because items in the collection appear to be sorted by the order in which they are fetched, even after a slice-shuffle.
a.models // say, [id:1, id:2, id:3]
a.fetch().done(function (data) {
return data = _.shuffle(data)
})
a.models // [id:1, id:2, id:3, id:4, id:5, id:6], still sorted
It appears that sorting inside the deferred object does not alter the outside collection in any way.
So, how can I sort only n items at the end of a Backbone Collection?
You can use parse function to shuffle returned data from server :
MyCollection.prototype.parse = function(resp){
return _.shuffle(resp);
};
myCollection.fetch();
You could use a randomizing comparator to shuffle items as they are added to the collection.
comparator: function(a) {
var rand = _.random(0, 1000);
a.set({rand: rand});
return rand;
}
fiddle: http://jsfiddle.net/pYyRp/
Related
I'm using ng-repeat to (guess) put array content in table.
Content is drawn dynamically, and it works well, when I'm modifying single elements of an array. But when I reload a whole array, there is this moment, when array is reassigned with new value, and ng-repeat draws blank table (which is actually logically correct). Is there a way to delay redrawing of content that way, the ng-repeat ignores the moment when the array is empty? Like the content is switched to new content without the 'clear' time.
I'm assigning new elements to array this way:
items = newItems;
where items is the array ng-repeat uses and newItems is an array of items freshly downloaded from database. The newItems is complete, when the assignment occurres. I'm not doing items = []; before the assignemt.
I'm usign angular 1.3
EDIT:
the ng-repeat:
<tr ng-repeat="order in submittedOrders">
stuff
<\tr>
js:
`$scope.reloadView = function() {
$scope.submittedOrders = OrdersService.getOrdersByStatus(ORDER_STATUS.submitted);
};`
Can it be the that the table is cleared in the first place, before call to database(service takes data from database) and during the wait, the table is cleared?
You may have to make use of Observables and async pipe of Angular.
Here are few steps you can take:
Convert your newItems to a rxjs Subject.
newItems$ = new Subject();
Whenever you get new values for your array, emit them via subject.
this.newItems$.next(newItems);
Make the items an observable of newItems$, and filter out empty arrays.
items = this.newItems$.pipe(
filter((a:any[]) => {
return a.length != 0;
})
);
In your template, use async pipe to iterate over array.
*ngFor="item of items | async"
Below is relevant parts of code that can get you started.
import { Observable, of, from, Subject } from 'rxjs';
import { filter, mapTo } from 'rxjs/operators';
...
newItems$ = new Subject();
items = this.newItems$.pipe(
filter((a:any[]) => {
return a.length != 0;
})
);
...
// A test method - link it to (click) handler of any div/button in your template
// This method will emit a non-empty array first, then, after 1 second emit an empty
// array, and then, after 2 seconds it will emit a non-empty array again with updated
// values.
testMethod() {
this.newItems$.next([3,4,5]);
setTimeout((v) => {
console.log("Emptying the array - should not be displayed browser");
this.newItems$.next([]);
}, 1000);
setTimeout((v) => {
console.log("Updating the array - should be displayed in browser");
this.newItems$.next([3,4,4,5]);
}, 2000);
}
I have a 'flat' array with 3 items:
[{"title":"welcome","file":"default.aspx","category":"finance"},
{"title":"test2","file":"test2.aspx","category":"finance"},
{"title":"test1","file":"test1.aspx","category":"housing"}]
The objective is to transform this into a nested observableArray with 2 items:
[{"category":"finance","content":[
{"title":"welcome","file":"default.aspx","category":"finance"},
{"title":"test2","file":"test2.aspx","category":"finance"}]},
{"category":"housing","content":[
{"title":"test1","file":"test1.aspx","category":"housing"}]}]
http://www.knockmeout.net/2011/04/utility-functions-in-knockoutjs.html helped me to extract unique categories in two steps:
self.getcategories = ko.computed(function () {
var categories = ko.utils.arrayMap(self.pages(), function (item) {
return item.category();
});
return categories.sort();
});
self.uniqueCategories = ko.dependentObservable(function() {
return ko.utils.arrayGetDistinctValues(self.self.getcategories()).sort();
});
//uniqueCategories: ["finance","housing"]
However I can't figure out how to create the nested array. I got as far as this:
self.createCategories = ko.computed(function () {
ko.utils.arrayForEach(self.uniqueCategories(), function (item) {
var content = getCategoryContent(item);
var c = new category(item, content);
self.Categories.push(c);
});
return true;
});
function getCategoryContent(whichcategory) {
return ko.utils.arrayFilter(self.pages(), function (page) {
return page.category() === whichcategory;
});
}
It results however in 5 category items (finance 4x, housing 1x) where I expect just 2.
Your computed function createCategories is probably being invoked more than once. e.g. if you are adding items to your pages() array one at a time, this will trigger the createCategories function each time you add an item.
Normally you would make the categories array and return it from a computed function. Currently you are adding to the Categories observable array without clearing it each time.
An easy fix would be to clear out the Categories array at the top of the createCategories function. This would leave you in the odd situation of having to call createCategories at least once to set up the dependencies, but after that it would work automatically when the data changed.
Another option would be create an array and return it from the createCategories functions, then you could just rename the function to Categories and not have an observable array. This would be the way I would normally use computed functions.
On more option would be to just do the work as normal JavaScript functions (rather than computed) and just call createCategories manually when you change the original array (e.g. when you get a result back from your server).
I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?
I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);
I want to sort my colection before I pass it to the template. I use in the render function of my view
CollectionForTelplate : this.Collection
I do the fetch as
var self = this;
//fetch done here
if (Collection.length > 0) {
_.each(Collection, function(Model) {
JSON.stringify(Model);
}, this);
};
self.Collection = Collection;
self.render;
Is there any other way by means of which I can pass the collection to the template?
How do you sort a collection based on a string field of the model, say Model.name ? I tried writing a comparator in the collection and a sort function the view but unfortunately; it doesnot work for me
Implement your Collection's compatarator function, defined in the docs as
If you define a comparator, it will be used to maintain the collection
in sorted order.
That way your collection will be automatically kept in a sorted order after adds, removals etc. You can implement it as sort
comparator: function(model1, model2) {
if (model1 comes before model 2) {
return -1;
} else if (model1 is equal to model 2) {
return 0;
} else { // model1 comes after model 2
return 1;
}
}
or sortBy
comparator: function(model) {
// Return some numeral or string attribute and it will be ordered by it
// == smaller numbers come first / strings are sorted into alphabet order
return model.get('someAttribute');
}
Hope this helps!
Is there any other way by means of which I can pass the collection to
the template?
Yes, collection's have a toJSON() method so you can simply do something like
render: function() {
this._template({list: this.toJSON()}); //assuming your template is already compiled
return this;
}
How do you sort a collection based on a string field of the model, say
Model.name ? I tried writing a comparator in the collection and a sort
function the view but unfortunately; it does not work for me
You can simply define the comparator function on the collection, and it should keep itself sorted, here's the example given in the docs
chapters.comparator = function(chapter) {
return chapter.get("page");
};
I have a UI with a form that has dropdowns andcheckboxes in order to filter the results returned from the server.
I wonder if I have to create a Model in order to keep the current settings available or just get the vals with jquery and pass them to the Collections fetch.
What would be better?
That dependes how much you want to invest in design, or you just can live with a more pragmatic approach.
If you run for the design solution we can say that there is not a Collection of results what we are playing with here. It is a FilterSearch model, and every time we are sending the filter params to the server what we are doing is creating an instance of FilterSearch. Let's say:
POST http://myapp.com/filter_searchs?field1=value1&field2=value2
The server will take this request and will return the array of results. Let's say:
{
"field1": "value1",
"field2": "value2",
"results": [ "result1", "result2" ]
}
The Backbone FilterSearch model will be updated with the info responded by the server:
var myFilterSearch = App.FilterSearch.new({ field1: "value1", field2: "value2" });
myFilterSearch.save({ wait:true });
And from there we can create our results Collection:
var myResults = App.Results.new( myFilterSearch.get( "results" ) );
(All code is simplified and not tested)
Another solution can be to store the filter fields into the Collection it self.
App.Results = Backbone.Collection.extend({
url: function() {
return "/results?field1=" + this.field1 + "&field2=" + this.field2 );
}
});
var myResults = App.Results.new({ field1: "value1", field2: "value2" });
myResults.fetch();
This approach is interesting because you can update the results just modifying the filter fields in the Collection and call to .fetch(), all the references to the Collection will remain.
(All code is simplified and not tested)
For filtering collection using backbone the best approach is to fetch the collection and the return a subset filtered collection this will also make you code more reusable
To make the filter you should have a filtered function
var yourCollection = Backbone.Collection.extend ({
filtered : function () {
I suggest to use UnderScore filter which will return true for valid and false for invalid where true is what you are looking for. use this.models to get the current collection models use model.get( '' ) to get the element you want to check for
for example you may have a check box which shows a specific category
var results = _.filter( this.models, function ( model ) {
if ( item.get('category') === 'y' )
return true ;
return false ;
});
You may keep filtering the result with all you dropdown and checkboxes
Then use underscore map your results and transform it to JSON like
results = _.map( results, function( model ) { return model.toJSON() } );
Finally returning a new backbone collection with only results
return new Backbone.Collection( results ) ;
Optionally if you don't want to keep all the data in the collection but just the filtered one you could reset the collection and skip the above return like
this.reset( results ) ;
View : Here your ui data requested you should call the filtered function after passing the ui for values using jquery