I am having some issues with Ember.js and loading resources asynchronously (aka I don't know what I'm doing). This is what I have. It currently doesn't update the list parameter after receiving the data. The template does not render anything. Thanks for any help.
Utilities
import Ember from 'ember';
export var page = Ember.Object.extend({
type: null,
list: Ember.computed('type', function() {
var type = this.get('type');
var url = "/test/" + type + "/test2";
if (type) {
getArray(url).then(function(list) {
return list;
});
} else {
return [];
}
})
});
export function get(url) {
return Ember.$.ajax({
url: url,
type: 'GET',
dataType: 'text',
cache: true,
xhrFields: {
withCredentials: true
}
});
}
export function getArray(url) {
return get(url).then(
function(file) {
var array = file.split("\n");
array.pop();
return array;
},
function() {
return ["Error!"];
}
);
}
Route
import util from 'app/utils/utilities';
export default Ember.Route.extend({
model(params) {
var p = util.page.create({
type: params.log_type
});
return p;
}
});
Template
{{#each model.list as |item|}}
<li>{{item}}</li>
{{/each}}
Consider the following part of your code:
if (type) {
getArray(url).then(function(list) {
return list;
});
} else {
return [];
}
This is not going to do what you think it does. return list returns list as the value of the promise, but you are not then doing anything with that promise. In particular, be clear that the computed property list will not take on that value. When the if (type) branch is taken, the computed property list will have the value undefined (since it's not returning anything in that case).
Remember that model wants you to return a promise (at least, if you want it to do its thing, which is to wait for the promise to resolve, then proceed with the transition, then use the resolved value of the promise to call afterModel and setupController etc.) Therefore, instead of making list a computed property, make it a regular method which returns a promise for the model hook on your route to consume:
import Ember from 'ember';
export var page = Ember.Object.extend({
type: null,
list: function() {
var type = this.get('type');
var url = "/test/" + type + "/test2";
if (!type) return [];
return getArray(url);
});
Then in your route
import util from 'app/utils/utilities';
export default Ember.Route.extend({
model(params) {
var p = util.page.create({
type: params.log_type
});
return p.list();
}
});
Your model will then be the list, so in the template:
{{#each model as |item|}}
<li>{{item}}</li>
{{/each}}
Related
How to properly pass a variable to an Ember's class?
Controller:
import Controller from '#ember/controller';
import Object from '#ember/object';
function totalVotes(company) {
return company.upvotes + company.downvotes;
}
function calcPercent(company) {
return (company.upvotes * 100 / (company.upvotes + company.downvotes)).toFixed(2);
}
function percentComparator(a, b) {
return calcPercent(b) - calcPercent(a);
}
var Company = Object.extend({
score: function() {
return (this.get('upvotes') * 100 / totalVotes(this)).toFixed(2);
}.property('upvotes', 'downvotes')
});
var AppModel = Object.extend({
topCompanies: function() {
return this.get('companies')
.sort(percentComparator)
.slice(0, 8);
}.property('companies.#each.upvotes', 'companies.#each.downvotes'),
});
var appModel = AppModel.create({
companies: getCompaniesJSON().map(function(json) {
return Company.create(json);
})
});
export default Controller.extend({
topCompanies: appModel.topCompanies,
});
Template:
<ul>
{{#each topCompanies as |company|}}
<li>{{company.title}} {{company.score}}%</li>
{{/each}}
</ul>
The result of the above in the browser console:
jquery.js:3827 Uncaught TypeError: Cannot read property 'sort' of undefined
this.get('companies') is undefined. Why? I'm passing companies to AppModel.create. What am I doing wrong?
var appModel = AppModel.create({
companies: getCompaniesJSON().map(function(json) {
return Company.create(json);
})
});
should something like this (untested):
var appModel = AppModel.create({
init() {
this._super(...arguments);
this.set('companies', getCompaniesJSON().map((json) => {
return Company.create(json);
});
}
});
i'm assuming the code is written this way (with global variables) for illustrative purposes and so i will ignore the other issues with the code sample that are likely not present in your real code, such as the property in your controller needing to be a computed.alias instead of direct assignment etc.
I am making a request to a 3rd api through my backend. The api returns an array of events to my frontend and I am having trouble storing it in the ember DS. The route for my API request is eventful and the route/model I am trying to create an event for is eventful-event. I call:
this.get('store').createRecord('eventful-event', concert)
in my eventful adapter and get that 'store' is undefined. Here is my code for my eventful route:
import Ember from 'ember';
export default Ember.Route.extend({
model () {
return this.get('store');
},
data : {},
ajax: Ember.inject.service(),
actions: {
searchForEvents (data) {
let eventful = this.get('store').createRecord('eventful', data);
return eventful.save();
// .then(() => this.transitionTo('search-results'));
// return this.get('store').findAll('eventful');
// let something = this.get('store').findAll('eventful');
// console.log('something:', something);
// return eventful.save();
}
}
});
and my eventful adapter:
import ApplicationAdapter from 'ga-wdi-boston.event-bookmarker/application/adapter';
import Ember from 'ember';
export default ApplicationAdapter.extend({
ajax: Ember.inject.service(),
createRecord (store, type, record) {
let serialized = this.serialize(record, { includeId: true});
let data = { serialized };
let dataToSend = {'keywords': record.record.keywords, 'location': record.record.location };
return this.get('ajax').request('/eventful', {
method: 'POST',
data: dataToSend,
}).then((events) => {
events['eventful-event'].forEach(function(concert){
this.get('store').createRecord('eventful-event', concert);
});
});
}
});
This forEach is where the error is thrown. I'm new to ember so I apologize if I am overlooking something simple, or if my approach is not correct. Any advice or help would be much appreciated.
Just put a debugger in. 'This' in the adapter is undefined.
I believe context is your problem. Try using store (argument of adapter method) instead of this.get('store') inside local function:
createRecord (store, type, record) {
let serialized = this.serialize(record, { includeId: true});
let data = { serialized };
let dataToSend = {'keywords': record.record.keywords, 'location': record.record.location };
return this.get('ajax').request('/eventful', {
method: 'POST',
data: dataToSend,
}).then((events) => {
events['eventful-event'].forEach(function(concert){
store.createRecord('eventful-event', concert);
});
});
}
I have pass two callback function success and error on promise returned from ajax call using then method. Now i am unable to get Ember component object inside success/error method.
import Ember from 'ember';
export default Ember.Component.extend({
data:null,
issueType:'',
description:null,
prepareSubmitRaiseIssueModal:function(){
var data = this.get('data');
this.set('ticket.category',data.category);
this.set('ticket.name',this.get('session.currentUser.first_name'));
this.set('ticket.phone',this.get('session.currentUser.phone'));
this.set('ticket.groupId',data.groupId);
this.set('ticket.ownerId',this.get('session.currentUser.id'));
this.set('ticket.oyoId',this.get('session.currentOwnerHotelOyoId'));
this.set('ticket.ticketRaisedBy','owner');
this.set('ticket.bookingId',data.bookingId);
this.set('ticket.subType',data.subType);
this.set('ticket.subSubIssue',data.subSubIssue);
this.set('ticket.email',this.get('ticket.oyoId')+'#oyoproperties.com');
this.set('ticket.subject',this.get('ticket.oyoId')+' : '+this.get('ticket.category'));
this.set('ticket.description',this.get('description'));
},
success:function(){
console.log(this.get('description'));
},
error:function(){
console.log(this.get('description'));
},
actions :{
submitIssue:function(){
this.prepareSubmitRaiseIssueModal();
this.get('ticket').submitRaiseIssue().then(this.success,this.error);
//this.send('closeRaiseIssueModal');
},
closeRaiseIssueModal:function(){
this.sendAction('closeRaiseIssueModal');
}
}
});
i am able to get Ember component object if instead of passing named function i pass anonymous function.
submitIssue:function(){
var self = this;
this.prepareSubmitRaiseIssueModal();
this.get('ticket').submitRaiseIssue().then(function(response){
console.log(self.get('description'));
},
function(err){
console.log(self.get('description'));
});
//this.send('closeRaiseIssueModal');
},
is there any way i can get the Ember component object's reference for former case??
Wow speaking of a spaghetti.
prepareSubmitRaiseIssueModal:function(){
var data = this.get('data');
this.set('ticket.category',data.category);
this.set('ticket.name',this.get('session.currentUser.first_name'));
this.set('ticket.phone',this.get('session.currentUser.phone'));
this.set('ticket.groupId',data.groupId);
this.set('ticket.ownerId',this.get('session.currentUser.id'));
this.set('ticket.oyoId',this.get('session.currentOwnerHotelOyoId'));
this.set('ticket.ticketRaisedBy','owner');
this.set('ticket.bookingId',data.bookingId);
this.set('ticket.subType',data.subType);
this.set('ticket.subSubIssue',data.subSubIssue);
this.set('ticket.email',this.get('ticket.oyoId')+'#oyoproperties.com');
this.set('ticket.subject',this.get('ticket.oyoId')+' : '+this.get('ticket.category'));
this.set('ticket.description',this.get('description'));
},
How about
prepareSubmitRaiseIssueModal:function(){
var data = this.get('data');
var ticket = this.get('ticket')
ticket.setProperties({
'category': data.category,
'name': ...
})
},
And to pass reference's you can either use
promise.then(function() {
this.mysuccess();
}.bind(this), function() {
this.myerror();
}.bind(this))
const self = this;
promise.then(function() {
self.blah();
});
promise.then(result => {
this.blah();
})
In your case I would write a utility JS file for displaying notifications.
And handle success for each promise personally and let errors be handled in a general error method.
utils/notifications.js
function reportError(error) {
displayNotification('error', getErrorMessage(error));
}
import reportError from 'utils/notifications';
export default Ember.Controller.extend({
....
promise.then(result => {
// Do custom stuff with result;
}, reportError);
....
});
And promise within a promise
return promise1.then(x => {
return promise2.then(x2 => {
return promise3 ... etc
})
}).catch(reportError); // Single hook needed but u need to return promises
I am building routes/states and the menu based on what the user is authorized to see. I've looked around and tried a few different things, but i'm hitting a brick wall. The SessionService object in the RoleService Factory is empty whenever RoleService.validateRole() is called. No route is added and the app is effectively dead. Why is the injected factory empty and the methods undefined.
Here is a simplified layout of the app starting in order of dependencies.
In app.run(), I am adding the states to the app instead of doing it in the config.
$stateProviderRef.state(value.stateName, state);
The states come from (a factory) AppConfig.getStates(), which returns an array.
var states = AppConfig.getStates();
In getStates() we validate each route's role.
if(RoleService.validateRole(routes[i].role))
The RoleService depends on the SessionService and the validateRole function does this check:
if(SessionService.currentUser.role === role)
The SessionService depends on the AuthenticationService which is just a factory that returns a promise using $http (the user object). The SessionService.currentUser is a function that .then()s the returned promise from the AuthenticationService.
return {
currentUser: function(){
AuthenticationService.then(function(result){
return result;
});
}
};
I'm not sure of a better way to explain the code without including the entire files.
Based on the plunker (mentioned in comment), I updated/cloned it to another, which is working
I. simple - when static data are returned (no $http)
Because the service SessonService was defined like this:
return {
currentUser: function() {
...
we cannot call it as a property:
...
return {
validateRoleAdmin: function () {
if (SessionService.currentUser.role === 'admin') {
...
},
validateRole: function (role) {
if(SessionService.currentUser.role === role){
...
it is a function it must be called as a function currentUser():
return {
validateRoleAdmin: function () {
if (SessionService.currentUser().role === 'admin') {
...
},
validateRole: function (role) {
if(SessionService.currentUser().role === role){
...
II. waiting for async calls
The adjusted example
Next, if we in example create a static result of the service AuthenticationService:
angular.module('daedalus').factory('AuthenticationService',
function() {
return {"idsid": "ad_jdschuma","role": "user","id": "33333"}
}
)
we cannot expect there will be some then method:
currentUser: function() {
//AuthenticationService.then(function(result) {
// return result;
//});
return AuthenticationService;
}
And to make it really async we can replace it with this:
angular.module('daedalus').factory('AuthenticationService',
['$timeout', function($timeout) {
return {
getData: function() {
return $timeout(function() {
return {
"idsid": "ad_jdschuma",
"role": "user",
"id": "33333"
}
})
}
};
}])
And then use even the .then() - Session service:
angular.module('daedalus').factory('SessionService', ['AuthenticationService',
function(AuthenticationService) {
return {
currentUser: function(){
return AuthenticationService
.getData()
.then(function(result){
return result;
});
}
};
}]
)
And the RoleService:
return {
...
validateRole: function(route) {
console.log('SessionService currentUser: ' + JSON.stringify(SessionService))
return SessionService
.currentUser()
.then(function(userRole) {
if (userRole.role === route.role) {
return route;
} else {
return null;
}
})
}
And with this in place in appConfig
getStates: function(){
var items = [];
var deffered = $q.defer();
var validatedCount = routes.length;
for(var i=0,len=routes.length; i<len; i++){
var route = routes[i];
RoleService
.validateRole(route)
.then(function(route){
if(route) {
items.push(route.stateConfig)
}
if(--validatedCount === 0 ){ // all processed
deffered.resolve(items)
}
})
}
return deffered.promise;
}
We can do that in run:
AppConfig
.getStates()
.then(function(states) {console.log(states)
angular.forEach(states, function(value, key) {
var state = {
"url": value.url,
"templateUrl": value.templateUrl,
"controller": value.controller
};
$stateProviderRef.state(value.stateName, state);
});
// Configures $urlRouter's listener *after* your custom listener
$urlRouter.sync();
});
$urlRouter.listen();
Check it here
The concept of the second solution (async) is too .thenified(). I just intended to show that all is working. Better approach how to get security data is completely covered here:
Confusing $locationChangeSuccess and $stateChangeStart
I have something like:
App.IndexController = Ember.ObjectController.extend({
results : function(){
return new Ember.RSVP.Promise(function(resolve, reject){
Ember.$.getJSON('/search').then(function(res){
console.log('response data is: ', res);
resolve(res)
})
})
}.property(),
...
})
// data
$.mockjax({
url: '/search',
responseText : {
type: 'different people',
res: [
{name: 'charlie', age: '55'},
{name: 'bobby', age: '19'},
{name: 'raymond', age: '39'}
]
}
})
my jsbin
How do I actually use the returned data? Currently, results returns a promise ( this.get('results') ) so I can't use it in my hbs template. Do I need to convert it to an object and then return that object?
Answering the Question
Internally Ember uses a PromiseProxyMixin to do the magic rendering of promises that we know and love. Here is your updated JSBin working with a PromiseProxy:
http://emberjs.jsbin.com/danazu/edit?html,js,output
Your results property becomes this:
results: Ember.computed.promise(function(resolve, reject) {
Ember.$.getJSON('/search').then(function(res) {
console.log('response data is: ', res);
return resolve(res);
});
})
However I don't recommend this. Take a look at this discourse thread to get some information on why you might not want to do this. In short, it will be clunky to handle all the different states of promises.
Looking For a Better Way
The Router is the perfect place to deal with promises with AJAX requests. There must be some reason that you're not just loading this data in your Router in the model or afterModel hooks. Could you create another nested resource to represent these search results and then just link-to that resource?
In your Router:
Router.map(function() {
this.resource('search', { path: '/search/:term' });
});
App.PeopleRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON('/search?term=' + params.term);
}
});
If that won't work at the very least you could:
Send an action when you want the search results
Handle the AJAX request in the controller
Set the results on the controller when they are resolved.
I use ic ajax for easy use with promises. I don't currently use Ember data so I wrote myself a nice rest client on top of ic ajax to easily get/post/put to urls:
rsvpAjax: function(url,method, data){
var headers = {};
// build request
var opts = {};
var defaultOpts = {
type: method,
headers: headers,
data: JSON.stringify(data),
dataType: 'json',
xhrFields: {
withCredentials: true
}
};
return ajax.request(url, $.extend({}, defaultOpts, opts));
}
And then I have get method:
getAsync: function(url){
return this.rsvpAjax(url, "GET");
}
Then using it elsewhere in a facade/helper class:
import objFactory from 'appname/models/obj';
...
...
foo: function(){
//get your rest client somehow
var apiFacade = this.getRestClient();
return restClient.getAsync('url_string').then(function(response){
//targets some part of the response - here its an array of
var data = response.data;
return data.map(function(obj){
return objFactory.create(obj);
});
});
}
foo returns an array of Ember objects. the objFactory I import is just a Ember.Object class. The obj passed into the create generally is a one to one match with the properties in said Ember.Object. With an initializer, I inject this object into every route:
import someFacade from "app/facades/facade";
export default {
name: 'someFacade',
initialize: function(container, app) {
//a singleton by default
container.register('facades:someFacade',someFacade);
app.inject('route', 'someFacade', 'facades:someFacade');
}
};
Any of my routes can use this facade without importing it thanks to DI.
//Ember blocks
model: function(){
return this.someFacade.foo();
}
or:
setupController(controller, model){
//set controller prop on return of resolved val
this.someFacade.foo().then(function(foosReslovedVal){
controller.set('someProp', foosResolvedVal);
});
}