Is there a way to load a single entity of a Backbone collection (from the server)?
Backbone.Collection.extend({
url: '/rest/product'
});
The following code can load the entire collection with a collection.fetch() but how to load a single model? Backbone's documentation says clearly that GET can be done /collection[/id] but not how.
The model has to be declared that way:
Backbone.Model.extend({
url: function() {
return '/rest/product/'+this.id;
}
});
Using it is simple as:
var model = new ProductModel();
model.id = productId;
model.fetch({ success: function(data) { alert(JSON.stringify(data))}});
While we set
url:"api/user"
for the collection, the equivalent for the model is
urlRoot:"api/user"
this will make backbone automatically append the /id when you fetch()
collection.fetch( {data: { id:56 } } )
I did this:
Catalog.Categories.Collection = Backbone.Collection.extend({
fetchOne : function (id, success) {
var result = this.get(id);
if (typeof result !== 'undefined') {
console.log(result, 'result')
success.apply(result);
return;
}
var where = {};
where[this.model.prototype.idAttribute] = id;
var model = new this.model(where);
this.add(model);
console.log(this._idAttr, where, this.model)
model.fetch({success: function () {
success.apply(model);
}});
}
};
Now call it:
collection.fetchOne(id, function () {console.log(this)});
No more guessing if the model is already in the collection!. However, you have to use a call back as you can't depend on an intimidate result. You could use async false to get around this limitation.
just .fetch a Model.
So create a model with it's own .url function.
Something like
function url() {
return "/test/product/" + this.id;
}
I did this:
var Product = Backbone.Model.extend({});
var Products = Backbone.Collection.extend({
model: Product,
url: '/rest/product'
});
var products = new Products();
var first = new Product({id:1});
first.collection = products;
first.fetch();
This has the advantage of working when you're not using a REST storage engine (instead, using something like the HTML5 Local storage, or so forth)
Related
So, meteor reruns helper code every time it is called in the template, right? My issue is that I have a heavy helper that returns a large object. I'm iterating over a list of these objects and then over some nested objects which is resulting in a really big lag.
So, are there any design patterns that prevent recalling the whole helper every time? Or do I just need to break up my object?
Template.deliveries.helpers({
current_delivery: function() {
var delivery_id = Template.instance().data.current_delivery_id;
var delivery = Deliveries.findOne({'_id': delivery_id});
var project = Projects.findOne({'_id':Session.get('current_project_id')});
var secondary_profile_names = [];
if (Session.get('delivery_include_secondaries')) {
for (var n in project.delivery_profiles) {
if (project.delivery_profiles[n].name === delivery.delivery_profile) {
if (project.delivery_profiles[n].secondary_deliverables) {
secondary_profile_names = project.delivery_profiles[n].secondary_deliverables;
}
break;
}
}
}
$("#delivery-profile").val(delivery.delivery_profile);
var elements = $.map(delivery.elements, function(id, idx) {
i_el = InternalElements.findOne({'_id': id});
i_el.source_element = SourceElements.findOne({'_id':i_el.source_element});
if (secondary_profile_names) {
i_el.secondary_elements = InternalElements.find({
'source_element':i_el.source_element._id,
'name':{'$in': secondary_profile_names},
"$or": [{'is_primary':false}, {'is_primary': {'$exists':false}}]
},{
'sort':{'version':-1},
'limit':1
}).fetch();
} else {
i_el.secondary_elements = [];
}
return i_el;
});
delivery.elements = elements.sort(function(a,b) { return (a.shot_name > b.shot_name) - (a.shot_name < b.shot_name); });
return delivery;
},
});
A pattern I've used successfully is to cache the results of expensive computations in a local collection.
MyLocalCache = new Mongo.Collection();
I like to make objects in this collection 1:1 with the original object so I reuse the _id from the original along with any keys and values that don't require transformation then extend the object with the transformed values.
I am trying to make a newsfeed similar to twitter, where new records are not added to the UI (a button appears with new records count), but updates, change reactively the UI.
I have a collection called NewsItems and I a use a basic reactive cursor (NewsItems.find({})) for my feed. UI is a Blaze template with a each loop.
Subscription is done on a route level (iron router).
Any idea how to implement this kind of behavior using meteor reactivity ?
Thanks,
The trick is to have one more attribute on the NewsItem Collection Say show which is a boolean. NewsItem should have default value of show as false
The Each Loop Should display only Feeds with show == true and button should show the count of all the items with show == false
On Button click update all the elements in the Collection with show == false to show = true
this will make sure that all your feeds are shown .
As and when a new feed comes the Button count will also increase reactively .
Hope this Helps
The idea is to update the local collection (yourCollectionArticles._collection): all articles are {show: false} by default except the first data list (in order not to have a white page).
You detect first collection load using :
Meteor.subscribe("articles", {
onReady: function () {
articlesReady = true;
}
});
Then you observe new added data using
newsItems = NewsItems.find({})
newsItems.observeChanges({
addedBefore: (id, article, before)=> {
if (articlesReady) {
article.show = false;
NewsItems._collection.update({_id: id}, article);
}
else {
article.show = true;
NewsItems._collection.update({_id: id}, article);
}
}
});
Here is a working example: https://gist.github.com/mounibec/9bc90953eb9f3e04a2b3.
Finally I managed it using a session variable for the current date /time:
Template.newsFeed.onCreated(function () {
var tpl = this;
tpl.loaded = new ReactiveVar(0);
tpl.limit = new ReactiveVar(30);
Session.set('newsfeedTime', new Date());
tpl.autorun(function () {
var limit = tpl.limit.get();
var time = Session.get('newsfeedTime');
var subscription = tpl.subscribe('lazyload-newsfeed', time, limit);
var subscriptionCount = tpl.subscribe('news-count', time);
if (subscription.ready()) {
tpl.loaded.set(limit);
}
});
tpl.news = function() {
return NewsItems.find({creationTime: {$lt: Session.get('newsfeedTime')}},
{sort: {relevancy: -1 }},
{limit: tpl.loaded.get()});
},
tpl.countRecent = function() {
return Counts.get('recentCount');
},
tpl.displayCount = function() {
return Counts.get('displayCount');
}
});
Template.newsFeed.events({
'click .load-new': function (evt, tpl) {
evt.preventDefault();
var time = new Date();
var limit = tpl.limit.get();
var countNewsToAdd = tpl.countRecent();
limit += countNewsToAdd;
tpl.limit.set(limit);
Session.set('newsfeedTime', new Date());
}
});
I've been having a problem using an RSS parser in meteor. It's an async call, so it needs ot be wrapped, however it still doesn't seem to work. I presume this is because the anonymous on('readable' function is outside the fiber, but I can't see how to resolve it.
var FeedParser = Meteor.require('feedparser');
var request = Meteor.require('request');
function getBlog(url, parameter, id){
request(parameter)
.on('error', function (error) {
console.error(error);
})
.pipe(new FeedParser())
.on('error', function (error) {
console.error(error);
})
.on('readable', function () {
var stream = this,
item;
while (item = stream.read()) {
Items.insert(new_item);
}
});
}
var wrappedGetBlog = Meteor._wrapAsync(getBlog);
Meteor.methods({
blog: function (url, parameter, id) {
console.log('parsing blog');
var items = wrappedGetBlog(url, parameter, id);
}
});
Meteor._wrapAsync() expects the wrapped function to return error and result to a callback. Your function, getBlog(), does not do that so _wrapAsync is not the right approach.
I have wrapped that function before but used a Future.
That approach allowed me to call feedparser from a Meteor.method(), which doesn't allow async functions, but you are also trying to do an insert inside the readable event. I think that insert will complain if it is not in a fiber. Maybe like this would also be necessary:
var r = request( parameter );
r.on( 'response' , function(){
var fp = r.pipe( new FeedParser() ); //need feedparser object as variable to pass to bindEnvironment
fp.on('readable', Meteor.bindEnvironment(
function () {
var stream = this,
item;
while (item = stream.read()) {
Items.insert(new_item);
}
}
, function( error ){ console.log( error );}
, fp // variable applied as `this` inside call of first function
));
});
Fibers is another option...
var Fiber = Npm.require( "fibers" );
.on('readable', function () {
var stream = this,
item;
while (item = stream.read()) {
Fiber( function(){
Items.insert(new_item);
Fiber.yield();
}).run();
}
});
I have a Template named movies, that has a method that returns a list of objects from a collection.
The query to generate that list of objects is created dynamically using data from another template method.
I would like to re-render the template, or just the components associated with that specific template method, whenever the filter data changes.
Here are the two methods used:
Template.movies.filter = function () {
if (Session.equals("filter", undefined)) {
return {};
}
return Session.get("filter");
};
Template.movies.movies = function () {
return Movies.find(Template.movies.filter(), {sort: [["Poster", "desc"]]});
};
On the HTML side it's a simple {{#each movies}}{{> movie}}{{/each}} to show the results from the movies method.
The problem is that when Session.get("filter") changes and therefore so does Template.movies.filter(), the HTML component relying on Template.movies.movies() data won't be updated with the new query results.
How would I achieve that behavior?
The easiest way is to just make a javascript function that both helpers utilize:
var getFilter = function() {
if (Session.equals("filter", undefined)) {
return {};
}
return Session.get("filter")
}
Template.movies.filter = function() { return getFilter(); }
Template.movies.movies = function () {
return Movies.find(getFilter(), {sort: [["Poster", "desc"]]});
};
This will react as you expect.
The current method I'm using is to filter a collection, which returns an array, and use
collection.reset(array)
to re-populate it. However, this modifies the original collection, so I added an array called "originalCollectionArray" which keeps track of the initial array state of the collection. When no filtering is active I simply use
collection.reset(originalCollectionArray)
But then, I need to keep track of adding and removing models from the real collection, so I did this:
// inside collection
initialize: function(params){
this.originalCollectionArray = params;
this.on('add', this.addInOriginal, this);
this.on('remove', this.removeInOriginal, this);
},
addInOriginal: function(model){
this.originalCollectionArray.push(model.attributes);
},
removeInOriginal: function(model){
this.originalTasks = _(this.originalTasks).reject(function(val){
return val.id == model.get('id');
});
},
filterBy: function(params){
this.reset(this.originalCollectionArray, {silent: true});
var filteredColl = this.filter(function(item){
// filter code...
});
this.reset(filteredColl);
}
This is quickly becoming cumbersome as I try to implement other tricks related to the manipulation of the collection, such as sorting. And frankly, my code looks a bit hacky. Is there an elegant way of doing this?
Thanks
You could create a collection as a property of the main collection reflecting the state of the filters:
var C = Backbone.Collection.extend({
initialize: function (models) {
this.filtered = new Backbone.Collection(models);
this.on('add', this.refilter);
this.on('remove', this.refilter);
},
filterBy: function (params){
var filteredColl = this.filter(function(item){
// ...
});
this.filtered.params = params;
this.filtered.reset(filteredColl);
},
refilter: function() {
this.filterBy(this.filtered.params);
}
});
The parent collection keeps its models whatever filters you applied, and you bind to the filtered collection to know when a change has occurred. Binding internally on the add and remove events lets you reapply the filter. See
http://jsfiddle.net/dQr7X/ for a demo.
The major problem on your code is that you are using a raw array as original, instead of a Collection. My code is close to the yours but use only Collections, so methods like add, remove and filter works on the original:
var OriginalCollection = Backbone.Collection.extend({
});
var FilteredCollection = Backbone.Collection.extend({
initialize: function(originalCol){
this.originalCol = originalCol;
this.on('add', this.addInOriginal, this);
this.on('remove', this.removeInOriginal, this);
},
addInOriginal: function(model){
this.originalCol.add(model);
},
removeInOriginal: function(model){
this.originalCol.remove(model);
},
filterBy: function(params){
var filteredColl = this.originalCol.filter(function(item){
// filter code...
});
this.reset(filteredColl);
}
});