When viewing a "story", I want to be automatically subscribed to that story and change the subscribed story as I change pages.
This is what I got: It seems to work but multiple autosubscribe seems wrong?
route("stories/:storytitle/:storyID", function(storyTitle, storyID) {
Session.set('storyID', storyID)
Meteor.autosubscribe(function() {
var storyID = Session.get('storyID');
if (storyID)
Meteor.subscribe("story", Session.get("storyID"), function() {
Router.goto('story')
});
});
});
Template.story.data = function() {
var storyID = Session.get('storyID');
var story = Stories.findOne({
_id: storyID
})
return story;
};
This seems to be more in line with what I'm looking for in general, but there's a ton of boilerplate. It also seems wrong to put a query into the route rather than just have it be in template helper.
route("stories/:storytitle/:storyID", function(storyTitle, storyID) {
Session.set('storyID', storyID)
var story = Stories.findOne({
_id: storyID
})
if (story)
Router.goto('story')
});
Meteor.autosubscribe(function() {
var storyID = Session.get('storyID');
if (storyID)
Meteor.subscribe("story", Session.get("storyID"), function() {
Router.goto('story')
});
});
Template.story.data = function() {
var storyID = Session.get('storyID');
var story = Stories.findOne({
_id: storyID
})
return story;
};
Are either of these the correct way to do it? How do I keep an auto subscription on a story, with it automatically changing subscriptions as I change pages?
Intuitively I would try this:
route("stories/:storytitle/:storyID", function(storyTitle, storyID) {
Session.set('storyID', storyID)
Router.goto('story')
});
Meteor.autosubscribe(function() {
var storyID = Session.get('storyID');
if (storyID)
Meteor.subscribe("story", Session.get("storyID"), function() {
Router.goto('story')
});
});
This simply doesn't work. It would try to goto the story route before the story loads and throws a white screen/error.
The third approach is correct, although the second approach has it's benefits if you want to route somewhere else (e.g. 404) if the story is not found. Some notes:
To avoid the error on the third approach, just make sure (in your templates) to deal with the case when findOne doesn't return anything. You should expect to see this before the data has fully loaded from the server; the template will re-render when the data is ready.
There's nothing wrong with putting a query in your route in the second case, but be aware it will most likely return null initially. You'll want to wrap your code in a reactive context so that it re-executes when the data is ready. You might want to use my reactive router to achieve this, or just copy the technique.
This way you won't need to use the onReady callback in the subscription. (actually you shouldn't need to do this in either case).
The first technique is definitely not the right way to do it :)
If you do want to route to 404 if the story doesn't exist, you should wait until the data has loaded, see: https://github.com/tmeasday/unofficial-meteor-faq#how-do-i-know-when-my-subscription-is-ready-and-not-still-loading
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
i have a Meteor Application which is very "slow" as there are a lot of API-Calls.
What i try to do is to break apart the loading/calls.
What i just did is:
i have loading template via iron-router
i waitOn for the first API-Call has finished
then i start the next API-calls in the Template.myTemplate.rendered - function
This was already a big benefit for the speed of my Application, but i want to break it up even more as the second call is in fact more like 5-25 API-calls.
So what i try to do now is inside the rendered function is a self-calling function which calls itself as long as there are no more to do and saves the response inside a session. (Until now it just rewrites, but even to this point i can´t get)
Template.detail.rendered = function(){
//comma separated list of numbers for the API-Call
var cats = $(this.find(".extra")).attr('data-extra').split(',');
var shop = $(this.find(".extra")).attr('data-shop');
var counter = 0;
var callExtras = function(_counter){
var obj = {
categories : [cats[_counter]],
shop : shop
};
if(_counter <= cats.length){
Meteor.subscribe('extra', obj,function(result){
//TODO dickes todo... nochmal nachdenken und recherchieren
//console.log(_counter);
Session.set('extra',Extra.find('extra').fetch()[0].results);
counter++;
callExtras(counter);
});
}
};
callExtras(counter);
Session.set('loading_msg', '' );
};
Now i have again problems with my reactive parts of the app desscribed here - Meteor: iron-router => waitOn without subscribe As i can´t find a proper way to update my client-side per user base collection. Also in the docs it is described the publish method also creates a new collection. (The new document´s ID) here - http://docs.meteor.com/#/full/publish_added
here is the publish from server
Meteor.publish('extra', function(obj){
var that = this;
Meteor.call('extra', obj, function(error, result){
if (result){
//console.log(result);
that.added("extra", "extra", {results: result});
//that.changed('extra','extra',{results: result});
that.ready();
} else {
//that.ready();
}
});
});
So my question is: Is there from scratch a better way to structuring my code means solving the problem somehow different? If not how can i achive it the cleanest way? Because for my understanding this is just strange way to do it.
EDIT:
For example.
Can i do a per-user-collection (maybe only client-side like now) and push data from the server and just subscribe to this collection? But then how can i check when the async API-Call has finshed to start the next round. So the view gets data piece by piece. I am just confused right now.
My fault was simple as i thaught: You don´t need to use subscribe.
I just added "error,result" in the callback of Meteor.call
Only "result" leads to the result is always undefined.
var cats = $(this.find(".extra")).attr('data-extra').split(',');
var shop = $(this.find(".extra")).attr('data-shop');
var counter = 0;
var callExtras = function(_counter){
var obj = {
categories : [cats[_counter]],
shop : shop
};
if(_counter <= cats.length){
Meteor.call('extra', obj,function(error,result){
var actual_session = Session.get('extra');
if(actual_session === false){
actual_session = [];
}
actual_session = actual_session.concat(result);
Session.set('extra',actual_session);
counter++;
callExtras(counter);
});
}
};
callExtras(counter);
Then in the template helper
"extra" : function(){
return Session.get('extra');
},
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));
}
I am building what should be a fairly simple project which is heavily based on Ampersand's starter project (when you first run ampersand). My Add page has a <select> element that should to be populated with data from another collection. I have been comparing this view with the Edit page view because I think they are quite similar but I cannot figure it out.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
Below you can see that I am trying to fetch the app.brandCollection and set its value to this.model, is this correct? I will need to modify the output and pass through the data to an ampersand-select-view element with the correct formatting; that is my next problem. If anyone has suggestions for that I would also appreciate it.
var PageView = require('./base');
var templates = require('../templates');
var ProjectForm = require('../forms/addProjectForm');
module.exports = PageView.extend({
pageTitle: 'add project',
template: templates.pages.projectAdd,
initialize: function () {
var self = this;
app.brandCollection.fetch({
success : function(collection, resp) {
console.log('SUCCESS: resp', resp);
self.brands = resp;
},
error: function(collection, resp) {
console.log('ERROR: resp', resp, options);
}
});
},
subviews: {
form: {
container: 'form',
waitFor: 'brands',
prepareView: function (el) {
return new ProjectForm({
el: el,
submitCallback: function (data) {
app.projectCollection.create(data, {
wait: true,
success: function () {
app.navigate('/');
app.projectCollection.fetch();
}
});
}
});
}
}
}
});
This is only the add page view but I think that is all that's needed.
The form subview has a waitFor attribute but I do not know what type of value it is expecting - I know it should be a string - but what does that string represent?
This string represents path in a current object with fixed this context. In your example you've waitFor: 'brands' which is nothing more than PageView.brands here, as PageView is this context. If you'd have model.some.attribute, then it'd mean that this string represents PageView.model.some.attribute. It's just convenient way to traverse through objects.
There's to few informations to answer your latter question. In what form you retrieve your data? What do you want to do with it later on?
It'd be much quicker if you could ping us on https://gitter.im/AmpersandJS/AmpersandJS :)
Hopefully you can help me! :)
My issue is that I have a Route that looks like this which I'm expecting to populate a list of items... i.e. "only the tagged ones, please", but it doesn't:
App.TaggedItemsListRoute = App.ItemsRoute.extend({
model: function() {
var store = this.get("store");
var storePromise = store.find("item", { has_tags: true });
var filtered = store.filter("item", function(item) {
return item.get("hasTags");
});
return storePromise.then(function(response) {
return filtered;
});
}
});
Now... that just plain doesn't work because "hasTags" returns false because it relies on "tags" which returns a ManyArray which is temporarily empty beacuse it hasn't resolved yet (see models below). This seems crappy to me. It's saying "Hey I've gone none in me!" but what I want it to be saying is "please recalculate me later" and the filter is looking for a boolean, but what I want to pass it is "hey, don't resolve the filter until all hasTags have resolved" or at least to recompute the ManyArray that it passes.
If I just pass back a promise as the return value for the filter then it sort of works...
return item.get("tags").then(function(tags){ return item.get("hasTags"); });
Except that it's actually not, beacuse filter is getting a Promise, but it's not aware of promises, apparently, so when it's looking for a boolean it gets a promise which it evaluates as true, and then it pretty much shows all the items in the list. That's not a problem until I go to a different route for items which has, say, all the items on it, then come back... and BAM it's got all the items in it... hm....
The following is how I've "gotten around" it temporarily ... ie it's still buggy, but I can live with it...
App.TaggedItemsListRoute = App.ItemsRoute.extend({
model: function() {
var store = this.get("store");
var storePromise = store.find("item", { has_tags: true });
var filtered = store.filter("item", function(item) {
var tags = item.get("tags");
if tags.get("isFulfilled") {
return item.get("hasTags");
} else {
return tags.then(function() {
return item.get("hasTags");
});
}
});
return storePromise.then(function(response) {
return filtered;
});
}
});
I think the only way to really get around this at this stage would be to use RSVP.all... any thoughts?
Actually one thing I haven't tried which I might go try now is to use setupController to do the filtering. The only trouble there would be that ALL the items would get loaded inot the list and then visually "jump back" to a filtered state after about 1 second. Painful!
Models
My Ember App (Ember 1.5.1) has two models (Ember Data beta7): Item and Tag. Item hasMany Tags.
App.Item = DS.Model.extend({
tags: DS.hasMany("tag", inverse: "item", async: true),
hasTags: function() {
return !Em.isEmpty(this.get("tags"));
}.property("tags")
});
App.Tag = DS.Model.extend(
item: DS.belongsTo("item", inverse: "tags"),
hasItem: function() {
return !Em.isEmpty(this.get("item"))
}.property("item")
);
If I change the model to the following, it actually does print something to the logs when I go to the route above, so it is fulfilling the promise.
App.Item = DS.Model.extend({
tags: DS.hasMany("tag", inverse: "item", async: true),
hasTags: function() {
this.get("tags").then(function(tags) {
console.log("The tags are loding if this is printed");
});
return !Em.isEmpty(this.get("tags"));
}.property("tags")
});
This is a spin off question from Ember Data hasMany async observed property "simple" issue because I didn't really explain my quesiton well enough and was actually asking the wrong question. I originally thought I could modify my model "hasTags" property to behave correctly in the context of my Route but I now don't think that will work properly...
This seems like a perfectly good candidate for RSVP.all. BTW if you want a rundown on RSVP I gave a talk on it a few weeks back (don't pay too much attention too it, pizza came halfway through and I got hungry, http://www.youtube.com/watch?v=8WXgm4_V85E ). Regardless, your filter obviously depends on the tag collection promises being resolved, before it should be executed. So, it would be appropriate to wait for those to resolve before executing the filter.
App.TaggedItemsListRoute = App.ItemsRoute.extend({
model: function() {
var store = this.get("store");
return store.find("item", { has_tags: true }).then(function(items){
var tagPromises = items.getEach('tags');
return Ember.RSVP.all(tagPromises).then(function(tagCollections){
// at this point all tags have been received
// build your filter, and resolve that
return store.filter("item", function(item) {
return item.get("hasTags");
});
});
});
}
});
Example using a similar idea with colors (I only show it if the relationship has 3 associated colors)
http://emberjs.jsbin.com/OxIDiVU/454/edit
On a separate note, if you felt like you wanted this hook to resolve immediately, and populate magically after, you could cheat and return an array, then populate the array once the results have come back from the server, allowing your app to seem like it's reacting super quick (by drawing something on the page, then magically filling in as the results come pouring in).
App.TaggedItemsListRoute = App.ItemsRoute.extend({
model: function() {
var store = this.get("store"),
quickResults = [];
store.find("item", { has_tags: true }).then(function(items){
var tagPromises = items.getEach('tags');
return Ember.RSVP.all(tagPromises).then(function(tagCollections){
// at this point all tags have been received
// build your filter, and resolve that
return store.filter("item", function(item) {
return item.get("hasTags");
});
});
}).then(function(filterResults){
filterResults.forEach(function(item){
quickResults.pushObject(item);
});
});
return quickResults;
}
});
Example of quick results, returns immediately (I only show it if the relationship has 3 associated colors)
http://emberjs.jsbin.com/OxIDiVU/455/edit