Hopefully you can help me!
My Ember App (Ember 1.5.1) has two models (Ember Data beta7): Item and Tag. Item hasMany Tags.
I also have a computed property on tags which doesn't update. The computed property is a simple check to see if there are any tags (ie not empty). I've tried various things. I got as far as arrayComputed and then stopped. It should be reasonably trivial to check if an async related model has a count higher than 0! I've tried "tags.[]", "tags.#each", "tags.#each.status", "tags.#each.isLoaded", "tags.length" and various other things.
The computed property on the belongsTo works fine.
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('))
}.property("item")
);
If I change it to read like this, then the log gets a line printed in it, so the promise is actually fulfilling and loading. I just don't know what to put on the computed property to make it reload when the promise has fulfilled. I feel like I'm missing something super obvious:
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")
});
[ edit ] 7 May 2014
Okay so I didn't fully explain the question properly the first go around I guess because I didn't fully understand what was wrong... I've continued the quesiton in the following stack overflow issue: Route's Model Hook with Ember Data "filter" not loading dependent computed property
I believe what you are looking for is this:
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.#each")
});
Because tags is an array, you want to be notified when tags are added or removed. the '.#each' will make it update when this happens.
Okay hopefully someone has a better answer than this, but this is how I've "gotten around it" for now, in my routes (I feel like this is an absolutely ridiculous way to do it), by adjusting the thing that relies on "hasTags" rather than adjusting hasTags itself.
Note I'm not even sure if I "fixed up" "hasTags" in Item whether or not it would trigger the filtered reference below to update...?
I think perhaps we need filter to be promise-aware in its block (ie so you can use properties to filter that are promises and it'll do the right thing):
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;
});
}
});
Related
So After hunting around stack overflow for solutions to this problem I came across this https://dweldon.silvrback.com/common-mistakes and this https://dweldon.silvrback.com/guards Both very useful.
My problem is that one of my templates is loading twice, once with the collection undefined and then again with the collection defined. So in the console i see something like:
TRADEID
undefined
TRADEID
L…n.Cursor {collection: LocalCollection, sorter: null, matcher: M…o.Matcher, _selectorId: undefined, skip: undefined…}
In my attempts to fix this problem I have implemented guarding so my Js file looks like this
TRADEINFO = new Mongo.Collection("trade_info");
Template.E4E_tradeTile.onCreated(function(){
this.subscribe('users');
this.subscribe('trade_info');
});
Template.E4E_tradeTile.helpers({
borrow(){
console.log(this.tradeID);
var guard = TRADEINFO.findOne();
var query = TRADEINFO.find();
console.log(guard && query);
return guard && query;
}
});
and then in my template I read out this borrows
<h2 class="no-margins">{{borrow.element}}</h2>
<small>Borrow</small>
The template always seems to render blank, (there is no text) so I think that when the template is being renderd the collection is not avaliable, then when the collection becomes avaliable it is not updating.
Thanks again for the help!
So it turns out a combination of the two approches appears to have solved the problem. I added this to the route:
Router.route('/problem', {
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('trade_info', this.params._id);
},
action: function () {
if(Meteor.userId()){
this.render('problem_page');
}else {
this.render('login');
this.layout('blankLayout');
}
}
and used the guard! as seen above
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 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
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