The get function in Backbone does not seem to take a success callback. Is there a way to delay the creation of the view in the code below until I'm sure self.restaurant exists?
self.restaurant = app.collections.restaurants.get(id);
this.showView( '#main', new app.Views.RestaurantDetailView({model: self.restaurant }) );
I can do this
if (self.restaurant){
this.showView( '#main', new app.Views.RestaurantDetailView({model: self.restaurant }) );
}else{
}
but if self.restaurant doesn't exist then I end up doing something awkward to make it work. I'm thinking there must be an simple solution to this problem that I don't know.
Instead of calling get on the collection with the id, I rather created a new model with the id, and then called fetch on the model. Fetch takes a success callback so there's no issue of waiting until the query's finished before creating the view
var restaurant = new app.Models.Restaurant({ id: id });
var self = this;
restaurant.fetch({
success:function () {
self.showView( '#restaurants', new app.Views.RestaurantDetailView({model: restaurant}) );
}
});
Related
I have a collection that I am subscribing to, but when I attempt to access it from my onRendered event it always returns as an empty array. Below is the method I am using:
FlightCounts = new Mongo.Collection("flightCounts");
if (Meteor.isClient) {
Meteor.subscribe('flightCounts');
Template.hello.rendered = function(){
var counts = FlightCounts.find().fetch()
console.log(counts)
}
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
Meteor.publish('flightCounts', function(){
return flightCounts.find();
})
});
}
Can anyone see why my collection would always be empty here? Any suggestions on how to get it populated?
The underlying issue is that the publish function should reference Meteor's Mongo.Collection name, FlightCounts, and not the raw db.collection name flightCounts:
Meteor.publish('flightCounts', function(){
return FlightCounts.find();
});
I also agree with the prior answer that your template should check to ensure the subscription is ready before logging the data, otherwise it may not have arrived yet:
Template.hello.onRendered(function(){
this.autorun(() => {
let ready = Template.instance().subscriptionsReady();
if (!ready){ return; }
let counts = FlightCounts.find().fetch();
console.log(counts);
});
});
(Note that the newer syntax for Template.name.rendered is Template.name.onRendered().)
if (Meteor.isClient) {
Meteor.subscribe('flightCounts');
Template.hello.rendered = function() {
var template = this;
template.autorun(function () {
var counts = FlightCounts.find().fetch()
console.log(counts)
});
}
}
Your collection is not populated because, it is still getting data from server. So you should wrap your code inside an autorun, so that it will re-run whenever data is updated.
P.S: I am on mobile, so formatting may not be proper. Sorry about it.
Alternatively, you can use subscriptions of FlowRouter or waitOn of IronRouter to put the subscribe there
Update Surprisingly, IronRouter now has the subscriptions option as well
For some odd reason, Backbone is trying to put my model object instead of posting it to the server.
The error is an HTTP 500 error because Backbone is trying to put a model with no id (because I have made it undefined):
PUT /api/teams/undefined 500 285ms - 135b
Here's my code:
this.model.id = undefined;
this.model._id = undefined;
teamCollection.add(this.model);
this.model.save(null,
{
success: function (model) {
$('.top-right').notify({
message: {text: ' New team added! '},
type: 'info',
fadeOut: {
delay: 3500
}
}).show();
window.userHomeMainTableView.render();
}
,
error: function (model) {
teamCollection.remove(model);
$('.top-right').notify({
message: {text: ' Error adding team :( '},
type: 'danger',
fadeOut: {
delay: 3500
}
}).show();
}
});
even after "forcing" the model.id and model._id to be undefined, Backbone still tries to do an HTTP PUT. How can this be?
The syncing process internally uses Model#isNew to decide if it should PUT or POST. isNew is very simple minded:
isNew model.isNew()
[...] If the model does not yet have an id, it is considered to be new.
and that check is done using:
!this.has(this.idAttribute)
The has method is just a simple wrapper around attr:
has: function(attr) {
return this.get(attr) != null;
}
so these are irrelevant:
this.model.id = undefined;
this.model._id = undefined;
when Backbone is deciding to PUT or POST, those don't really remove the id attribute, they break your model.
I think you'd be better off copying the model (without the id) and saving the copy:
var data = m.toJSON();
delete data.id;
var copy = new Model(data);
Or better, create a whole new model, populate it with data, and then save the new model instance.
I think you'd need to unset the id attribute and manually remove the id property to make what you're trying to do work.
that's strange it looks like it should do a POST from your code. It looks like you can ovverride it though. From the second answer here
fooModel.save(null, {
type: 'POST'
});
use the following code snippet to set the id to undefined.
this.model.set('id',undefined');
although if it's a new model you don't need to do this. it will be undefined by default.
if you have used the idAttribute for defining the model and say it's value is userId then you need to do something like this.
this.model.set('userId',undefined');
I am fooling around with a loop and a ajax request in react I cannot seem to get working. Its suppose to loop over, set the object array and then push that object array to the state for later use.
The issue is that I am failing at promises in general. I am using this concept from the react docs to set the state of a component upon mounting to return an array of "links".
/** #jsx React.DOM */
var Temp = {
object: new Array()
}
var CommentsRow = React.createClass({
getInitialState: function(){
return {
href: ''
}
},
componentDidMount: function(){
var self = this
this.props.comments.slice(0, 5).map(function(comment){
var postUrl = window.Development.API_URL + 'posts/' + comment.post_id
$.get(postUrl, function(post){
Temp.object.push(post.post.title);
if (self.isMounted()) {
self.setState({
href: Temp.object
});
}
});
});
},
render: function() {
console.log(this.state)
}
});
The gist of whats going on above is:
I have a bunch of comments coming in and I take the first five. From there I loop over each comment object and grab the title, creating my api link. With that I want to say get me the post based on this link, assuming it works we then want to set a temp object, this will create "five arrays" each going from a count of 1,2,3,4 and finally 5 elements.
from there we take that and set the state. This part works, but because its a ajax request the state out side the request is empty even if I use the if (isMounted()){ ... } option.
any idea how I can set the state doing something like this and still have access to it?
You either want async.js or promises to help manage multiple async actions. Async integrates a bit better with jQuery, so I'll show it with that.
componentDidMount: function(){
async.map(this.props.comments.slice(0, 5), function(comment, cb){
var postUrl = window.Development.API_URL + 'posts/' + comment.post_id;
$.get(postUrl, function(data){
cb(null, {title: data.post.title, href: ???});
});
}, function(err, posts){
// posts is an array of the {title,href} objects we made above
this.setState({posts: posts});
}.bind(this));
}
Can anyone see what may be wrong in this code, basically I want to check if a post has been shared by the current logged in user AND add a temporary field to the client side collection: isCurrentUserShared.
This works the 1st time when loading a new page and populating from existing Shares, or when adding OR removing a record to the Shares collection ONLY the very 1st time once the page is loaded.
1) isSharedByMe only changes state 1 time, then the callbacks still get called as per console.log, but isSharedByMe doesn't get updated in Posts collection after the 1st time I add or remove a record. It works the 1st time.
2) Why do the callbacks get called twice in a row, i.e. adding 1 record to Sharescollection triggers 2 calls, as show by console.log.
Meteor.publish('posts', function() {
var self = this;
var mySharedHandle;
function checkSharedBy(IN_postId) {
mySharedHandle = Shares.find( { postId: IN_postId, userId: self.userId }).observeChanges({
added: function(id) {
console.log(" ...INSIDE checkSharedBy(); ADDED: IN_postId = " + IN_postId );
self.added('posts', IN_postId, { isSharedByMe: true });
},
removed: function(id) {
console.log(" ...INSIDE checkSharedBy(); REMOVED: IN_postId = " + IN_postId );
self.changed('posts', IN_postId, { isSharedByMe: false });
}
});
}
var handle = Posts.find().observeChanges({
added: function(id, fields) {
checkSharedBy(id);
self.added('posts', id, fields);
},
// This callback never gets run, even when checkSharedBy() changes field isSharedByMe.
changed: function(id, fields) {
self.changed('posts', id, fields);
},
removed: function(id) {
self.removed('posts', id);
}
});
// Stop observing cursor when client unsubscribes
self.onStop(function() {
handle.stop();
mySharedHandle.stop();
});
self.ready();
});
Personally, I'd go about this a very different way, by using the $in operator, and keeping an array of postIds or shareIds in the records.
http://docs.mongodb.org/manual/reference/operator/query/in/
I find publish functions work the best when they're kept simple, like the following.
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('sharedPosts', function(postId) {
var postRecord = Posts.findOne({_id: postId});
return Shares.find{{_id: $in: postRecord.shares_array });
});
I am not sure how far this gets you towards solving your actual problems but I will start with a few oddities in your code and the questions you ask.
1) You ask about a Phrases collection but the publish function would never publish anything to that collection as all added calls send to minimongo collection named 'posts'.
2) You ask about a 'Reposts' collection but none of the code uses that name either so it is not clear what you are referring to. Each element added to the 'Posts' collection though will create a new observer on the 'Shares' collection since it calls checkSharedId(). Each observer will try to add and change docs in the client's 'posts' collection.
3) Related to point 2, mySharedHandle.stop() will only stop the last observer created by checkSharedId() because the handle is overwritten every time checkSharedId() is run.
4) If your observer of 'Shares' finds a doc with IN_postId it tries to send a doc with that _id to the minimongo 'posts' collection. IN_postId is passed from your find on the 'Posts' collection with its observer also trying to send a different doc to the client's 'posts' collection. Which doc do you want on the client with that _id? Some of the errors you are seeing may be caused by Meteor's attempts to ignore duplicate added requests.
From all this I think you might be better breaking this into two publish functions, one for 'Posts' and one for 'Shares', to take advantage of meteors default behaviour publishing cursors. Any join could then be done on the client when necessary. For example:
//on server
Meteor.publish('posts', function(){
return Posts.find();
});
Meteor.publish('shares', function(){
return Shares.find( {userId: this.userId }, {fields: {postId: 1}} );
});
//on client - uses _.pluck from underscore package
Meteor.subscribe( 'posts' );
Meteor.subscribe( 'shares');
Template.post.isSharedByMe = function(){ //create the field isSharedByMe for a template to use
var share = Shares.findOne( {postId: this._id} );
return share && true;
};
Alternate method joining in publish with observeChanges. Untested code and it is not clear to me that it has much advantage over the simpler method above. So until the above breaks or becomes a performance bottleneck I would do it as above.
Meteor.publish("posts", function(){
var self = this;
var sharesHandle;
var publishedPosts = [];
var initialising = true; //avoid starting and stopping Shares observer during initial publish
//observer to watch published posts for changes in the Shares userId field
var startSharesObserver = function(){
var handle = Shares.find( {postId: {$in: publishedPosts}, userId === self.userId }).observeChanges({
//other observer should have correctly set the initial value of isSharedByMe just before this observer starts.
//removing this will send changes to all posts found every time a new posts is added or removed in the Posts collection
//underscore in the name means this is undocumented and likely to break or be removed at some point
_suppress_initial: true,
//other observer manages which posts are on client so this observer is only managing changes in the isSharedByMe field
added: function( id ){
self.changed( "posts", id, {isSharedByMe: true} );
},
removed: function( id ){
self.changed( "posts", id, {isSharedByMe: false} );
}
});
return handle;
};
//observer to send initial data and always initiate new published post with the correct isSharedByMe field.
//observer also maintains publishedPosts array so Shares observer is always watching the correct set of posts.
//Shares observer starts and stops each time the publishedPosts array changes
var postsHandle = Posts.find({}).observeChanges({
added: function(id, doc){
if ( sharesHandle )
sharesHandle.stop();
var shared = Shares.findOne( {postId: id});
doc.isSharedByMe = shared && shared.userId === self.userId;
self.added( "posts", id, doc);
publishedPosts.push( id );
if (! initialising)
sharesHandle = startSharesObserver();
},
removed: function(id){
if ( sharesHandle )
sharesHandle.stop();
publishedPosts.splice( publishedPosts.indexOf( id ), 1);
self.removed( "posts", id );
if (! initialising)
sharesHandle = startSharesObserver();
},
changed: function(id, doc){
self.changed( "posts", id, doc);
}
});
if ( initialising )
sharesHandle = startSharesObserver();
initialising = false;
self.ready();
self.onStop( function(){
postsHandle.stop();
sharesHandle.stop();
});
});
myPosts is a cursor, so when you invoke forEach on it, it cycles through the results, adding the field that you want but ending up at the end of the results list. Thus, when you return myPosts, there's nothing left to cycle through, so fetch() would yield an empty array.
You should be able to correct this by just adding myPosts.cursor_pos = 0; before you return, thereby returning the cursor to the beginning of the results.
Lets say i have a JSON like this:
JSON example, my json is validated on jsonlint and it works.
json_object = {
"texts_model": {
"hello": "hola",
"icon_text": "Icon!"
},
"collection_vias": {
"text": "hola",
"icon_text": "Icon!"
}
};
I have made a Collection that parse the contents of the json and generates model and collections from this json.
App.TemplatesCollection = Backbone.Model.extend({
model: App.TemplateModel,
url: TEMPLATES_SERVICE,
initialize: function(){
this.fetch({
success: (function () {
console.log(' Success ');
}),
error:(function (e) {
//console.log(' Error: ' + e);
}),
complete:(function (e) {
console.log(' Fetch completado con exito. ');
})
});
},
//Here i generate all my models and collections.
parse: function(response){
App.texts = new App.TemplateModel(response.text_model);
App.vias = new App.ViasCollection(response.collection_vias);
return response;
},
//I was trying with the get function but i the only thing i got was undefined.
plain_texts: function(){
return( this.get('plain_texts') ) ;
}
});
And the view is like this:
App.TemplateView = Backbone.View.extend({
el: App.$main_content,
initialize: function(){
_.bindAll(this, 'render');
},
//Here i pass the template(html source) i want to render.
render: function(template){
var html = render(template, this.model.toJSON() );
App.$main_content.html(html);
return this;
}
});
And my start.js where they live all the declarations of my models and views:
//app
App = {
init: function(){
console.log('Iniciando...');
//variables y constantes
App.$main_content = $('#main-content-content');
App.$main_header = $('#main-header-content')
App.$main_navigation = $('#main-navigation-content');
//data
App.templates = new App.TemplatesCollection();
//views
App.templateView = new App.TemplateView({model: App.texts});
//router
App.router = new App.Router();
},
start: function(){
//init
App.init();
//router
Backbone.history.start();
}
}
And the router:
//router
App.Router = Backbone.Router.extend({
routes:{
"" : "index",
":web" : "url"
},
index: function(){
console.log("index");
//Here i do not know what to do, i mean do i have to instiate the View each time i go to index? or only render?
App.templateView = new App.TemplateView({model: App.texts});
App.templateView.render("sections/login/form");
},
url: function(web){
console.log(web);
}
});
//on document ready
$(function(){
App.start();
});
My problem is that when the html is loaded the only thing i have is:
"Uncaught TypeError: Cannot call method 'toJSON' of undefined "
But when i put this on the developer console:
App.templateView = new App.TemplateView({model: App.texts});
App.templateView.render("sections/login/form");
My view is rendered correctly.
Why my view isn't rendered on the load and only when i put my code on the developer console?
How can i render my model on the view on the router url?
Why do i have undefined on the html loaded on the developer console?
----EDIT---
All right,
I think i understand. Maybe I'm generating a problem of a thing that does not have to have a problem.
Now my Model is like this:
App.TemplatesCollection = Backbone.Model.extend({
model: App.TemplateModel,
url: TEMPLATES_SERVICE,
plain_texts: function(){
return this.get('texts') ;
},
initialize: function(){
this.fetch();
}
});
And the View:
App.TemplateView = Backbone.View.extend({
el: App.$main_content,
initialize: function(){
console.log(this.collection);
var ea = this.collection.get('texts');
console.log(ea);
},
render: function(template){
console.log(this.collection);
return this;
}
});
Now i see my collection inside my View.
But when i try to do this to get only the text version on my View:
var ea = this.collection.get('texts');
console.log(ea);
Im getting the error of undefined:
Uncaught TypeError: Cannot call method 'get' of undefined
Any idea about how can i resolve this?
I'm trying to solve this by myself. I do not want to look like im asking to develop my solution.
Thanks in advance.
It's a little hard to read, but at a quick glance: your App.texts = is in in the parse() function of your Collection. As a result, it gets called once the .fetch() on the collection is performed... until then, your App.texts is undefined!
If App.texts is undefined when you create the TemplateView, then the view's model will actually be undefined, and so, in the render, when the template engine you use is doing a toJSON(), it will say that it has an undefined value...
There may be other problems, but this one is the most glaring. Here is a quick&dirty fix: once the fetch() is done, your collection will trigger a reset event. That's your cue for doing the rendering. So, what you can do, is instead of passing the model to the View, you can pass the collection instead:
App.templateView = new App.TemplateView({collection: App.templates});
Now, in your View's initialize, you can do something like:
if(App.texts) {
//Your collection has already fetched and already went through parse()
this.model = App.texts;
this.render("sections/login/form");
} else {
//Your collection hasn't done the fetch yet
view = this;
this.collection.one("reset", function(){
view.model = App.texts;
view.render("sections/login/form");
});
}
If you give a collection as a param to a View's construction, it'll be stored in this.collection, same as with model. The idea here is to use the events to know when to do the rendering, and also let the view tell you when it's ready to render. You could also do something in your render() function to check if the model is defined!
To see if this analysis is correct, you can put a console.log(App.texts); in your index function in the router.
One way to make the code a bit more obvious is to initialize your App.texts and App.vias directly in your App's init. And give a reference to them to your AppTemplatesCollection if you really need to side-load them in the parse of AppTemplates' fetch(). The difference that makes is that you can bind to events from the App.vias collection ('add', 'remove', 'reset') or to the App.texts model ('change').
Another thing I noticed is that you have a collection of App.TemplateModel but you are still creating a App.texts where you put the result of the fetch into your own instance of App.TemplateModel? That doesn't seem right, maybe you have a reason for doing so, but in the most general case, the collection is suppose to handle the creation of the models, especially after a fetch!
The usual use case of the parse() method is to side-load data (other models/collection), change the format (from XML to something JS can understand) or to remove useless keys (for instance user: {id: ..., name: ... }, you'll return response.user so that Backbone can play with the correct hash directly). What you are doing here seems to fall out of this pattern so maybe it's a cause for worry?
In your code you have created collection as :
App.TemplatesCollection = Backbone.Model.extend({
//rest of the code
If you want to create a collection you need to extend Backbone.Collectionand not Backbone.Model.
App.TemplatesCollection = Backbone.Collection.extend({
//rest of the code