I'm trying to set a variable in my router from the inside of a promise, but it seems that it is not set properly.
I have create a small example to show you :
App.ApplicationRoute = Ember.Route.extend({
myVar: null,
model: function() {
var data = getData();
console.log(myVar); // = null
return data;
},
getData: function() {
var self = this;
return new Ember.RSVP.Promise( function(resolve, reject) {
self.setProperties({ myVar: 42 });
resolve(someGoodStuff);
reject(someBadStuff)
});
}
})
When I try to display myVar, it still at null even when it was waiting for the promise to resolve...
Do I need to do something special ? or I'm doing it wrongly ?
I believe you are looking for something like this: http://emberjs.jsbin.com/gosoj/3/edit?html,css,js,output
I'm not entirely sure what you are going for here, so there is also some other commented code that may have been your intention.
I think maybe the problem was when you were console logging, as well as the fact that you weren't using your getters and setters properly, and the way you were calling getData()
Here is the code:
App.ApplicationRoute = Ember.Route.extend({
myVar: null,
model: function() {
var data = this.getData();
console.log(data);
console.log(this.get('myVar')); // = null
return data;
},
getData: function() {
var self = this;
return new Ember.RSVP.Promise( function(resolve, reject) {
self.setProperties({ myVar: 42 });
resolve(42);
reject(reason);
}).then(function (value) {
//self.setProperties({ myVar: value });
console.log(self.get('myVar'));
}, function(reason){
console.log(reason);
});
}
})
Related
I have an embedded object called Comment, inside a Game. Each game can have many Comments.
When a user (called a Parent) views the game page, they can leave a comment.
The problem I have is that I cannot seem to reset the body of the comment field back to empty after the comment is saved.
This is the component:
MyApp.AddCommentComponent = Ember.Component.extend({
body: '',
actions: {
addComment: function() {
var comment, failure, success;
if (!!this.get('body').trim()) {
comment = this.store.createRecord('comment', {
body: this.get('body'),
game: this.get('game'),
parent_id: this.get('parent_id'),
parent_name: this.get('parent_name')
});
success = (function() {
this.set('body', '');
});
failure = (function() {
return console.log("Failed to save comment");
});
return comment.save().then(success, failure);
}
}
}
});
The error is on the 'this.set' line - this.set is not a function
All the examples I find are about doing this in a controller or by creating a new record upon route change (but the route is not changing in my example, since it is just adding another embedded object to the existing page).
You are using
this.set('body', '');
in success, but the scope of this here is changed, you need to keep the controller scope and set the body to empty string like
MyApp.AddCommentComponent = Ember.Component.extend({
body: '',
actions: {
addComment: function() {
var that = this;
var comment, failure, success;
if (!!this.get('body').trim()) {
comment = this.store.createRecord('comment', {
body: this.get('body'),
game: this.get('game'),
parent_id: this.get('parent_id'),
parent_name: this.get('parent_name')
});
success = (function() {
that.set('body', '');
});
failure = (function() {
return console.log("Failed to save comment");
});
return comment.save().then(success, failure);
}
}
}
});
When you introduce a function, you must remember that the value for this is not (necessarily) the same as the enclosing scope's value for this. Save the reference to the Component to use in a closure, like this:
MyApp.AddCommentComponent = Ember.Component.extend({
body: '',
actions: {
addComment: function() {
var comment, failure, success;
var self= this;
if (!!this.get('body').trim()) {
comment = this.store.createRecord('comment', {
body: this.get('body'),
game: this.get('game'),
parent_id: this.get('parent_id'),
parent_name: this.get('parent_name')
});
success = (function() {
self.set('body', '');
});
failure = (function() {
return console.log("Failed to save comment");
});
return comment.save().then(success, failure);
}
}
}
});
I've been pulling out my hair with this for a few hours now so I thought I'd just ask :)
In the model hook of my route, I'm grabbing the account ID from the session store. I'm also returning an Ember hash of layouts using a (presently) hard-coded ID:
model: function() {
var accountId = this.get('session.currentUser').then(function(user) {
return user;
}).then(function(user) {
return user.get('account');
}).then(function(account) {
var accountId = parseInt(account.get('id'));
console.log(accountId); // outputs 2
return accountId;
});
return Ember.RSVP.hash({
layouts: this.store.query('layout', { account_id: 2 })
});
},
/* {{log layouts}} in the template returns the correct list of layouts */
However, when I try and use the value of the first promise in the hash, as follows:
return Ember.RSVP.hash({
layouts: this.store.query('layout', { account_id: accountId })
});
I get the following error:
You must pass a resolver function as the first argument to the promise constructor
TypeError: You must pass a resolver function as the first argument to the promise constructor
I can almost understand this, as perhaps the accountID promise isn't resolved before the hash function is called.
But then I tried:
var _this = this;
var accountId = this.get('session.currentUser').then(function(user) {
return user;
}).then(function(user) {
return user.get('account');
}).then(function(account) {
var accountId = parseInt(account.get('id'));
console.log(accountId); // outputs 2
return accountId;
}).then(function(accountId) {
console.log(accountId); // outputs 2
return Ember.RSVP.hash({
layouts: _this.store.query('layout', { account_id: accountId })
});
});
This does not give any errors, but {{log layouts}} in the template returns 'undefined'.
Can anyone help, please?
Instead of returning the hash at the end, structure your promise the other way around:
var _this = this;
return Ember.RSVP.hash({
layouts: this.get('session.currentUser').then(function(user) {
return user;
}).then(function(user) {
return user.get('account');
}).then(function(account) {
return parseInt(account.get('id'), 10);
}).then(function(accountId) {
return _this.store.query('layout', { account_id: accountId });
})
});
The problem with this code is that the render code is entered twice, and the buffer is not where I expect it. Even when I get the buffer, the stuff I push in is not rendered to the screen.
App.FilterView = Ember.View.extend({
init: function() {
var filter = this.get('filter');
this.set('content', App.ViewFilter.find(filter));
this._super();
},
render: function(buffer) {
var content = this.get('content');
if(!this.get('content.isLoaded')) { return; }
var keys = Object.keys(content.data);
keys.forEach(function(item) {
this.renderItem(buffer,content.data[item], item);
}, this);
}.observes('content.isLoaded'),
renderItem: function(buffer, item, key) {
buffer.push('<label for="' + key + '"> ' + item + '</label>');
}
});
And the App.ViewFilter.find()
App.ViewFilter = Ember.Object.extend();
App.ViewFilter.reopenClass({
find: function(o) {
var result = Ember.Object.create({
isLoaded: false,
data: ''
});
$.getJSON("http://localhost:3000/filter/" + o, function(response) {
result.set('data', response);
result.set('isLoaded', true);
});
return result;
}
});
I am getting the data I expect and once isLoaded triggers, everything runs, I am just not getting the HTML in my browser.
As it turns out the answer was close to what I had with using jquery then() on the $getJSON call. If you are new to promises, the documentation is not entirely straight forward. Here is what you need to know. You have to create an object outside the promise - that you will return immediately at the end and inside the promise you will have a function that updates that object once the data is returned. Like this:
App.Filter = Ember.Object.extend();
App.Filter.reopenClass({
find: function(o) {
var result = Ember.Object.create({
isLoaded: false,
data: Ember.Object.create()
});
$.getJSON("http://localhost:3000/filter/" + o).then(function(response) {
var controls = Em.A();
var keys = Ember.keys(response);
keys.forEach(function(key) {
controls.pushObject(App.FilterControl.create({
id: key,
label: response[key].label,
op: response[key].op,
content: response[key].content
})
);
});
result.set('data', controls);
result.set('isLoaded', true);
});
return result;
}
});
Whatever the function inside then(), is the callback routine that will be called once the data is returned. It needs to reference the object you created outside the $getJSON call and returned immediately. Then this works inside the view:
didInsertElement: function() {
if (this.get('content.isLoaded')) {
var model = this.get('content.data');
this.createFormView(model);
}
}.observes('content.isLoaded'),
createFormView: function(data) {
var self = this;
var filterController = App.FilterController.create({ model: data});
var filterView = Ember.View.create({
elementId: 'row-filter',
controller: filterController,
templateName: 'filter-form'
});
self.pushObject(filterView);
},
You can see a full app (and bit more complete/complicated) example here
I have a route like following where it builds the data from multiple rest calls.
App.IndexRoute = Ember.Route.extend({
model: function() {
var id = 1; //will get as url param later
var modelData = {ab:{},ef:{}};
return ajaxPromise('https://url1/'+ id +'?order=desc').then(function(data){
modelData.ab = data.items[0];
return ajaxPromise('https://url2/'+ id +'/?order=desc').then(function(data){
modelData.ab.x = data.items;
return modelData;
})
});
}
});
My ajaxPromise function is as follows:
var ajaxPromise = function(url, options){
return Ember.RSVP.Promise(function(resolve, reject) {
var options = options || {
dataType: 'jsonp',
jsonp: 'jsonp'
};
options.success = function(data){
resolve(data);
};
options.error = function(jqXHR, status, error){
reject(arguments);
};
Ember.$.ajax(url, options);
});
};
Now the issue is i know that i can use RSVP.all with promise instances but the data returned from these url has to be set to model object like above.
Also there may be few more rest calls which require data from other rest call. Is there any other way i can handle this promises.
PS: data is required right away for a single route
App.IndexRoute = Ember.Route.extend({
model: function() {
var id = 1; //will get as url param later
return Ember.RSVP.hash({
r1: ajaxPromise('https://url1/'+ id +'?order=desc'),
r2: ajaxPromise('https://url2/'+ id +'/?order=desc')
});
},
setupController:function(controller, model){
model.ab = model.r1.items[0];
model.ab.x = model.r2.items;
this._super(controller, model);
}
);
If you have two that have to run synchronously(second depends on first), you can create your own promise, which eon't resolve until you call resolve.
model: function() {
var promise = new Ember.RSVP.Promise(function(resolve, reject){
var modelData = {ab:{},ef:{}};
ajaxPromise('https://url1/'+ id +'?order=desc').then(function(data){
modelData.ab = data.items[0];
ajaxPromise('https://url2/'+ id +'/?order=desc').then(function(data){
modelData.ab.x = data.items;
resolve(modelData);
})
});
});
return promise;
},
I have two models:
App.User = DS.Model.create({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.create({
user: DS.belongsTo('App.User')
});
When a user is deleted, it also will delete all its comments on the backend, so I should delete them from the client-side identity map.
I'm listing all the comments on the system from another place, so after deleting a user it would just crash.
Is there any way to specify this kind of dependency on the association? Thanks!
I use a mixin when I want to implement this behaviour. My models are defined as follows:
App.Post = DS.Model.extend(App.DeletesDependentRelationships, {
dependentRelationships: ['comments'],
comments: DS.hasMany('App.Comment'),
author: DS.belongsTo('App.User')
});
App.User = DS.Model.extend();
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post')
});
The mixin itself:
App.DeletesDependentRelationships = Ember.Mixin.create({
// an array of relationship names to delete
dependentRelationships: null,
// set to 'delete' or 'unload' depending on whether or not you want
// to actually send the deletions to the server
deleteMethod: 'unload',
deleteRecord: function() {
var transaction = this.get('store').transaction();
transaction.add(this);
this.deleteDependentRelationships(transaction);
this._super();
},
deleteDependentRelationships: function(transaction) {
var self = this;
var klass = Ember.get(this.constructor.toString());
var fields = Ember.get(klass, 'fields');
this.get('dependentRelationships').forEach(function(name) {
var relationshipType = fields.get(name);
switch(relationshipType) {
case 'belongsTo': return self.deleteBelongsToRelationship(name, transaction);
case 'hasMany': return self.deleteHasManyRelationship(name, transaction);
}
});
},
deleteBelongsToRelationship: function(name, transaction) {
var record = this.get(name);
if (record) this.deleteOrUnloadRecord(record, transaction);
},
deleteHasManyRelationship: function(key, transaction) {
var self = this;
// deleting from a RecordArray doesn't play well with forEach,
// so convert to a normal array first
this.get(key).toArray().forEach(function(record) {
self.deleteOrUnloadRecord(record, transaction);
});
},
deleteOrUnloadRecord: function(record, transaction) {
var deleteMethod = this.get('deleteMethod');
if (deleteMethod === 'delete') {
transaction.add(record);
record.deleteRecord();
}
else if (deleteMethod === 'unload') {
var store = this.get('store');
store.unloadRecord(record);
}
}
});
Note that you can specify via deleteMethod whether or not you want to send the DELETE requests to your API. If your back-end is configured to delete dependent records automatically, then you will want to use the default.
Here's a jsfiddle that shows it in action.
A quick-and-dirty way would be to add the following to your user model
destroyRecord: ->
#get('comments').invoke('unloadRecord')
#_super()
I adapted the answer of #ahmacleod to work with ember-cli 2.13.1 and ember-data 2.13.0. I had an issue with nested relationships and the fact that after deleting an entity from the database its id was reused. This lead to conflicts with remnants in the ember-data model.
import Ember from 'ember';
export default Ember.Mixin.create({
dependentRelationships: null,
destroyRecord: function() {
this.deleteDependentRelationships();
return this._super()
.then(function (model) {
model.unloadRecord();
return model;
});
},
unloadRecord: function() {
this.deleteDependentRelationships();
this._super();
},
deleteDependentRelationships: function() {
var self = this;
var fields = Ember.get(this.constructor, 'fields');
this.get('dependentRelationships').forEach(function(name) {
self.deleteRelationship(name);
});
},
deleteRelationship (name) {
var self = this;
self.get(name).then(function (records) {
if (!records) {
return;
}
var reset = [];
if (!Ember.isArray(records)) {
records = [records];
reset = null;
}
records.forEach(function(record) {
if (record) {
record.unloadRecord();
}
});
self.set(name, reset);
});
},
});
Eventually, I had to set the relationship to [] (hasMany) or null (belongsTo). Else I would have run into the following error message:
Assertion Failed: You cannot update the id index of an InternalModel once set. Attempted to update <id>.
Maybe this is helpful for somebody else.