Background
I have an angular-meteor app and a collection of business objects in mongodb, e.g.:
{ startDate: new Date(2015, 1, 1), duration: 65, value: 36 }
I want to render this data in different views. One of the views is a graph, another is a list of entries.
The list of entries is easy. Just bind the collection to the model:
vm.entries = $scope.meteorCollection(MyData, false);
In the view I would do:
<div ng-repeat="entry in vm.entries">{{entry.startDate}} - ...</div>
But now for the graph. I want to transform each element into a { x, y } object and give the view that, e.g.:
vm.graphPoints = transformData(getDataHere());
The problem is that the data is not fetched here, in angular-meteor is looks like it is fetched when calling the entry in vm.entries in the view. The kicker is that the transformData method, needs to loop through the data and for each item index into other items to calculate the resulting x, y. Hence I cannot use a simple forEach loop (without having access to the other items in some way).
Question
So how can i fetch the data in the controller - transform it - and still have one-way binding (observing) on new data added to the database?
Thanks
Update
The following code works, but I think there could be performance problems with fetching the whole collection each time there is a change.
$scope.$meteorSubscribe("myData").then(function() {
$scope.$meteorAutorun(function() {
var rawData = MyData.find().fetch();
vm.model.myData = transformData(rawData);
});
});
EDIT:
This is the current solution:
$scope.collectionData = $scope.meteorCollection(MyData, false);
$meteor.autorun($scope, function() {
vm.graphPoints = transformData($scope.collectionData.fetch());
});
There is some missing information. do you wan't to have some kind of model object in the client? if that is correct I think you have to do something like this:
$meteor.autorun($scope, function() {
vm.graphPoints = transformData($scope.meteorCollection(MyData, false));
});
How about using the Collection Helper Meteor package to add the function:
https://github.com/dburles/meteor-collection-helpers/
?
Related
I have a dictionary of type {name: value}
A = {
name: x,
name: y,
name: z
}
I want to fetch a collection (consisting of models having one of their attribute as 'name'), but to be optimal I want to fetch such that the value of the attribute 'name' exists in my dictionary.
Is there a way to do specific filtering like that?
If you're doing the filtering client-side, overriding the filter method is really NOT the way to go.
Now you no longer have it available, should you need it later. Also, modifying the collection itself from within the filter method is an undesirable sideeffect.
Instead you should be using the parse method, which will automatically be called when fetching the collection.
Now as I understand it, you want to limit the fetched set to models with names matching the keys in your dictionary.
If so, I would do the following:
parse: function(response, options) {
// Do we want to filter the response?
if (options.filterNames) {
// Filter
response = _.filter(response, function(obj) {
// Check if this model name is one of the allowed names
return _.contains(options.filterNames, obj.name);
});
}
// Backbone will use the return value to create the collection
return response;
}
And then call fetch using
someCollection.fetch({filterNames: _.keys(someDictionary)});
If you're certain, you will always be filtering the collection on fetch, you can omit passing the option and just use the dictionary within parse.
Alternatively you could create a fetchFiltered() method on the collection, which would then invoke the line above.
After investigations and trials, here are the two ways this can be resolved:
1. Client side filtering after fetching the collection from the server. This is a less optimal method, especially when the collection is huge. In situations when you really want 5 models out of a 1000 model collection, it can be an overkill. But if the server side has no logic of accepting and using the filtering client side filtering should look something like:
Overload the collection filter code something like:
var filter = {
filter: function() {
var results = _.filter(this.models, function(model) {
// Perform the check on this model, like compare it to your local dict
if (checkPassed) {
return true;
}
return false;
});
results = _.map(results, function(model) {
return model.toJSON();
});
// Reset the existing collection to filtered models
this.reset(results) ;
};
var ExtendedCollection = OriginalCollection.extend(filter);
Pass a filter option in the fetch ajax call to the server, and the server should understand the filter and return the collection based off that.
Got a server returning a JSON object like so:
{
'key1':'value'
'key2':{
'key2_0':'value'
}
}
And a collection:
var Collection = Backbone.Collection.extend({
url:api.url//which returns the object above
});
var collection = new Collection();
collection.fetch({
success:function(data){
//do something
}
});
Now i need to use certain properties of the collection throughout my application, but say i need key1, i always have to do collection.at(0).get('key1');//returns 'value', because the data returned is stored within the collection, in a new Array at key 0.
Question:
How to directly... collection.get('key1')//now returns undefined... because it is.
I know i could expose an object to the global scope in the collection success function some_other_var = data.toJSON()[0] and access the some_other_var properties directly, but that's not what i'm looking for;
In order to use the get() function from a Backbone.Collection you need to know the model id or cid wanted.
For instance, lets say your data coming from the server is like follow:
[{
id: '123',
name: 'Alex'
}, {
id: '456',
name: 'Jhon'
}]
In that case you can do this:
this.collection.get('123').get('name') // Return "Alex"
Keep in mind that collection is just a set of model, so behind the scenes by doing collection.get() you are getting a model
Tip: If you don't have any kind of id in your server data, there is always the option of using underscore methods:
find
filter
some
contains
etc
It seems like you're trying to ascribe attributes to a collection, but a collection is merely a set of models. Having additional data that is constant throughout the collection suggests that it should be wrapped inside another Model, which is demonstrated here: Persisting & loading metadata in a backbone.js collection
I am creating a streaming paginated list of views. We start the app with an empty collection and add items to the collection at regular intervals. When the size of the collection passes a page_size attribute then the rest of the models should not get rendered, but the compositeView should add page numbers to click on.
I am planning on creating a render function for my compositeView that only renders items based on the current page# and page size by have a function in my collection that returns a list of models like this:
get_page_results: function(page_number){
var all_models = this.models;
var models_start = page_number * this.page_size;
var models_end = models_start + this.page_size;
//return array of results for that page
return all_models.slice(models_start,models_end);
}
My question is, should I even be using Marionette's composite view for this? It seems like im overwriting most of the functionality of Marionette's collectionView to get what I want.
Every time the number of items in my collection changes two things need to be updated:
The itemViews in the collection view
The page numbers at the bottom of the composite view
My strong recommendation is not to do this in the view layer. You're going to add a ton of code to your views, and you're going to end up duplicating a lot of this code between multiple views (one for displaying the data, one for page list and counts, one for ...).
Instead, use a decorator pattern to build a collection that knows how to handle this. I do this for filtering, sorting and paging data, and it works very well.
For example, here's how I set up filtering (running in a JSFdiddle here: http://jsfiddle.net/derickbailey/vm7wK/)
function FilteredCollection(original){
var filtered = Object.create(original);
filtered.filter = function(criteria){
var items;
if (criteria){
items = original.where(criteria);
} else {
items = original.models;
}
filtered.reset(items);
};
return filtered;
}
var stuff = new Backbone.Collection();
var filtered = FilteredCollection(stuff);
var view = Backbone.View.extend({
initialize: function(){
this.collection.on("reset", this.render, this);
},
render: function(){
var result = this.collection.map(function(item){ return item.get("foo"); });
this.$el.html(result.join(","));
}
});
In your case, you won't be doing filtering like this... but the idea for paging and streaming would be the same.
You would track what page # you are on in your "PagingCollection", and then when your original collection is reset or has new data added to it, the PagingCollection functions would re-calculate which data needs to be in the final pagedCollection instance, and reset that collection with the data you need.
Something like this (though this is untested and incomplete. you'll need to fill in some detail and flesh it out for your app's needs)
function PagingCollection(original){
var paged = Object.create(original);
paged.currentPage = 0;
paged.totalPages = 0;
paged.pageSize = 0;
paged.setPageSize = function(size){
paged.pageSize = size;
};
original.on("reset", function(){
paged.currentPage = 0;
paged.totalPages = original.length / paged.pageSize;
// get the models you need from "original" and then
// call paged.reset(models) with that list
});
original.on("add", function(){
paged.currentPage = 0;
paged.totalPages = original.length / paged.pageSize;
// get the models you need from "original" and then
// call paged.reset(models) with that list
});
return paged;
}
Once you have the collection decorated with the paging info, you pass the paged collection to your CollectionView or CompositeView instance. These will properly render the models that are found in the collection that you pass to it.
Regarding CollectionView vs CompositeView ... a CompositeView is a CollectionView (it extends directly from it) that allows you to render a model / template around the collection. That's the majority difference... they both deal with collections, and render a list of views from that collection.
We have built a set of mixins for bakcbone.marionette that you may find usefull (https://github.com/g00fy-/marionette.generic/)
You could use PaginatedMixin, that allows a Backbone collection to be paginated + a PrefetchMixin, so you don't have to pass a prefetched collection to a view.
the only code you would have to do is:
YourListView = Generic.ListView.mixin(PaginatedMixin,LoadingMixin,PrefetchMixin).extend({
paginateBy:10,
template:"#your-list-template",
itemViewOptions:{template:"#your-itemview-template"},
fetchPage:function(page){
this.page = page;
return this.collection.refetch({data:{page:page}}); // your code here
},
hasNextPage:function(){
return true; // your code here
},
});
For a working example see https://github.com/g00fy-/stack.reader/blob/master/js/views.js
I have a an "Asset" backbone model that has a custom attribute called "selected". Its custom in the sense that it is not part of the object on the server side. I use to represent which of the list of assets the user has currently selected.
var Asset = Backbone.Model.extend({
defaults: {
selected: false
},
idAttribute: "AssetId"
});
This model is part of a backbone collection that I fetch periodically to get any changes from the server.
The problem I have is that every time I fetch the collection, the collection is doing a reset (I can tell by the listening for the reset event) and hence the value of the selected attribute is wiped out by the data coming in from the ajax request.
The backbone.js documentation seems to suggest that there is a intelligent merge that will solve this problem. I believe I'm doing this in my fetch methods
allAssets.fetch({ update: true ,cache: false});
And I have also set the "idAttribute" field in the model so that the ids of the object coming in can be compared with the objects in the collection.
The way I have solved this is by writing my own Parse method in my collection object
parse: function (response) {
// ensure that the value of the "selected" for any of the models
// is persisted into the model in the new collection
this.each(function(ass) {
if (ass.get("selected")) {
var newSelectedAsset = _.find(response, function(num) { return num.AssetId == ass.get("AssetId"); });
newSelectedAsset.selected = true;
}
});
return response;
}
Is there a better way to do this?
Collection.update (introduced in Backbone 0.9.9) does indeed try to merge existing models, but does so by merging all set attributes in the new model into the old model. If you check Backbone source code, you'll see
if (existing || this._byCid[model.cid]) {
if (options && options.merge && existing) {
existing.set(model.attributes, options);
needsSort = sort;
}
models.splice(i, 1);
continue;
}
All attributes, including defaults, are set, that's why your selected attribute is reset to false. Removing the default value for selected will work as intended: compare http://jsfiddle.net/nikoshr/s5ZXN/ to http://jsfiddle.net/nikoshr/s5ZXN/3/
That said, I wouldn't rely on a model property to store my app state, I would rather move it to a controller somewhere else.
TL;DR: If I'm polling the entire collection of models from the server, how can I merge the changed attributes into each model and add/remove added/removed models from the collection?
In my backbone app, I am polling for the entire collection of models. I have a Backbone.Collection that I am basically calling reset on each time I get the array of models, so:
myCollection.reset(server_response);
The only problem with this is that it gets rid of the old models, kind of eliminating the benefit of events on a model. This is reset's purpose of course, but what I want to do is only modify the changed attributes of the models, and remove models not in the response, and add models that were in the response but not the collection.
Essentially I want a sort of merge of the data.
For models that are in the response and in the collection already, I believe I can just do model.set(attributes) and it takes care of seting only the ones that actually changed, triggering the change events in the process. This is great.
But how do I handle the cases where the models were in the response but not in the collection already, and vice versa, not in the response but in the collection?
My Proposed Solution
I don't know if backbone already has a way of doing this, and I may be overcomplicating which is why I'm asking, but I was thinking then of creating a method on my collection which gets passed the server_response.
It would get all of the id attributes of the server_response, and all of the id attributes of the models already in the collection.
The difference of the id's in response - collection would = added models, and vice versa would be removed models. Add and remove those models respectively from the collection.
The intersection of both sets of id's would be the modified models, so iterate through these id's and simply do a collection.get(id).set(attributes).
In pseudocoffeescript:
merge: (server_response) =>
response_ids = _.pluck(server_response, 'id')
collection_ids = #pluck('id')
added = _.difference(response_ids, collection_ids)
for add in added
#add(_.find(server_response, (model) ->
return model.id == add
))
removed = _.difference(collection_ids, response_ids)
for remove in removed
#remove(#get(remove))
changed = _.intersection(response_ids, collection_ids)
for change in changed
#get(change).set(_.find(server_response, (model) ->
return model.id == change
))
This technique is useful sometimes. We extend Collection with the following method. This should do what you're looking for. It's not in coffee, but you could easily port it. Enjoy!
// Take an array of raw objects
// If the ID matches a model in the collection, set that model
// If the ID is not found in the collection, add it
// If a model in the collection is no longer available, remove it
freshen: function (objects) {
var model;
// Mark all for removal
this.each(function (m) {
m._remove = true;
});
// Apply each object
_(objects).each(function (attrs) {
model = this.get(attrs.id);
if (model) {
model.set(attrs); // existing model
delete model._remove
} else {
this.add(attrs); // new model
}
}, this);
// Now check for any that are still marked for removal
var toRemove = this.filter(function (m) {
return m._remove;
})
_(toRemove).each(function (m) {
this.remove(m);
}, this);
this.trigger('freshen', this);
}
This just went into Backbone master 3 days ago:
https://github.com/documentcloud/backbone/pull/1220