I have an ember component with a template that contains a table. In my component - I would like to issue out ajax calls to a 3rd party service and get some data. After getting this data, I would have to make another subsequent ajax call based on the input. Since this would take time, I would like to update the view one after the other when the ajax called finishes. In a visual sense - this would be adding a new row to the table after one request is processed.
Currently, ember only allows us to pass an array object via its model() method in the router.
I looked into several projects like List-view but it's not solving the above-mentioned problem.
EDIT:-
Below is what I am currently doing -
import Ember from 'ember';
export default Ember.Route.extend({
model() {
var list = [];
var promise = $.ajax({
type: 'GET',
url: 'thirdPartyService'
});
promise = promise.then(function(data){
data = JSON.parse(data);
return data[1];
});
promise.then(function(data){
for (var i = 0; i < data.length; i++) {
var idea = data[i];
var url = "thirdPartyService";
var secondPromise = $.ajax({
type: 'GET',
url: url,
dataType: "text"
});
secondPromise = secondPromise.then(function(data){
var result = x2js.xml_str2json(data);
return result;
});
secondPromise.then(function(data){
list.pushObject(item);
});
return secondPromise;
}
});
return list;
}
});
My template
<tbody>
{{#each model as |idea|}}
<tr>
<td><input type="checkbox" name="id" value="{{idea.id}}"></td>
<td>{{idea.field4}}</td>
<td>{{idea.field5}}</td>
<td>{{idea.field}}</td>
<td>{{idea.field2}}</td>
<td>{{idea.field3}}</td>
</tr>
{{/each}}
</tbody>
The Ember view will be rendered when the list changes. What I want to do is to - add a row to the table when list.pushObject(item); is called. Currently - I see that the view is waiting till the everything is returned.
Also, this application is an electron app - due to this I don't have a backend per say and I am not using ember-data. I am calling couple of 3rd party services so there are no ember-models involved.
Update:
Demo: https://ember-twiddle.com/eb79994c163dd1db4e2580cae066a318
In this demo, I download your github data, and the list of repos, so the model hook will return when both api call returned... and finally it will provide the list of repo names.
Using loading.hbs the loading message automatically appears until the model hook promises resolved.
In your code, probably overwriting the promise variable is not the best approach, because you overwrite a variable which is not resolved yet.
If you would like a better user experience, the best if you have only one ajax call in the model, and return that promise... so your model contains the first promise result... and you can use this data to fire a promise in the controller and download the list, so the user can see the header of the table and the loading message until downloading the list.
Demo extended with a second page: https://ember-twiddle.com/eb79994c163dd1db4e2580cae066a318
If you have to deal with more models in your page, I would download them in the route handler, with RSVP.hash.
// for example: app/routes/books.js, where books are the main model
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return Ember.RSVP.hash({
books: this.store.findAll('book'),
authors: this.store.findAll('author'),
libraries: this.store.findAll('library')
});
},
setupController(controller, model) {
const books = model.books;
const authors = model.authors;
const libraries = model.libraries;
this._super(controller, books);
controller.set('authors', authors);
controller.set('libraries', libraries);
},
});
In the above example, you have model (with the list of books), authors and libraries in your controller, so you can pass forward these arrays to components.
Do you use Ember Data to access your server API or 3rd party service? In this case, you can inject store service in your component and use there as you would use in route handler.
Other option using closure actions, so your actions would be in controller and you can call these actions from components, so all the component would be responsible only for viewing data and other snippets.
In the following example, we create a self contained component, where you have a button to fire an action, which will download data from the server, using classic jquery ajax call, which is wrapped in a promise, so you can chain these promises and fire them in a row.
Create a component
$ ember g component my-component
The template app/templates/components/my-components.hbs, with a button, a flag and the list.
<button {{action 'downloadSomething'}}>Download</button>
in progress: {{downloadInProgress}}
{{#each downloaded as |item|}}
{{item.name}}
{{/each}}
The connected js file app/components/my-components.js
import Ember from 'ember';
const { $ } = Ember;
export default Ember.Component.extend({
downloaded: [],
actions: {
downloadSomething() {
this.set('downloadInProgress', true);
this._getData('http://localhost:8080').then(
(response) => {
this.set('downloadInProgress', false);
this.get('downloaded').pushObjects(response.contacts);
},
(error) => {
// some error handling...
}
);
}
},
// this function wraps the ajax call in a Promise
_getData(url) {
return new Ember.RSVP.Promise((resolve, reject) =>
$.ajax(url, {
success(response) {
resolve(response);
},
error(reason) {
reject(reason);
}
})
);
}
});
In the ajax response, we push the returned Objects in the downloaded array, so it would always added to the list.
Related
There's a well known approach to support loading more than one model promise in an Ember route, using Ember.RSVP.hash:
// app/routes/posts.js
export default Ember.Route.extend({
model(params) {
return Ember.RSVP.hash({
posts: this.store.findAll('post', params),
tags: this.store.findAll('tag', params),
});
},
});
Now I have a page param, to be able to load the posts in batches, instead of loading them and showing them all at once. But page changes do not alter the tags. However, when the page param changes, the whole model of the route is triggered again to re-load, causing the app to re-fetch both the posts for the new page, and the same tags all over again.
Is there a way to fine-tune this so that the tags are not loaded when certain params change?
There are several ways to refacto your code. Like moving tags out of model. Or doing pagination differently (w/o model refresh). The one I like is writing a custom getAll utility.
var cache = {}; // model_name => Promise<[Record]>
function getAll(store, model) {
if(!store.modelFor(model).cacheable) {
return store.findAll(model);
}
if(!cache[model]) {
cache[model] = store.findAll(model);
}
return cache[model];
}
And in your model now
import { getAll } from 'utils/store';
...
tags: getAll('tag'),
I am trying to make a request from the store for a model based on the result of a previous model request. Please find the code below:
For the route:
model(params) {
var id = params.framework_id;
var main = this;
return Ember.RSVP.hash({
question: this.store.query('question', {orderBy: 'framework', equalTo: id}),
framework: this.store.find('frameworks', id),
frameworks: this.store.findAll('frameworks')
})
}
Then in the route there is a setupController:
setupController: function(controller, model) {
this._super(controller, model);
var main = this;
...
controller.set("frameworkName", model.framework.get('name'));
var includedFramework = model.framework.get('includedFramework');
var includedFrameworkModel = this.store.find('frameworks', includedFramework);
Ember.Logger.info(model.framework)
Ember.Logger.info(includedFrameworkModel);
if (model.framework.get('includedFramework') != undefined) {
var linked = main.store.find("frameworks", model.framework.get('includedFramework'));
controller.set("linkedFramework", {id: linked.get('id'), name: linked.get('name')});
}
}
In the controller setup, using model.framework.get('name') works without a problem. mode.framework.get('includedFramework') works fine and returns an ID to another framework that is stored in the framework model. I then intend to pull the "included framework" in the store with another store.find request. Unfortunately this second store.find doesn't return the model record in the same way as the first. Here is an inspector view of what each request returns:
model.framework -
includedFrameworkModel -
Any help is greatly appreciated!
Well, every call to ember-data that returns something that may require a request to the server, like find(), findAll(), query(), save(), and async relationships returns a PromiseObject or a PromiseArray.
It works the same way for objects and arrays, so just lets describe how arrays work.
A PromiseArray is both, a Promise and a ArrayProxy.
This is very useful because you can work with it in both ways, depending on your situation.
Because the request to the server may take some time, the resulting ArrayProxy part is often empty, and will be populated with data later. This is very useful because your handlebars template and computed properties will update when the ArrayProxy changes.
The Promise part of the PromiseArray will resolve as soon the data are received from the server, with an actual array, that also may update later when you do further changes on your data.
The ember router however will wait for the promise to be resolved before it loads the route, which allows you to specify a loading substrate.
This is why model.framework is different in setupController. It's not the result of the .find() but the result of the resolved promise. Thats basically what Ember.RSVP.hash does for you.
Generally I recommend you two things.
Don't store a model id on a model, but use a relationship.
Don't call the store in .setupController(). Do all your requests in the .model() hook and use the promise chain to do so.
Maybe take this as a inspiration:
return Ember.RSVP.hash({
question: this.store.query('question', {orderBy: 'framework', equalTo: id}),
framework: this.store.find('frameworks', id),
frameworks: this.store.findAll('frameworks')
}).then(({question, framework, frameworks}) => {
let includedFramework = framework.get('includedFramework');
let includedFrameworkModel = this.store.find('frameworks', includedFramework);
return {question, framework, frameworks, includedFrameworkModel};
});
var includedFrameworkModel = this.store.find('frameworks', includedFramework);
The store.find() method will return the promise, the object was not resolved when you print in log.
Changed your script to something like this.
this.store.find('frameworks', includedFramework).then(function(includedFrameworkModel) {
Ember.Logger.info(includedFrameworkModel);
});
In Ember, you can reference a property in a template, and the template will wait for that property to be populated before rendering.
This works great for obtaining a list of entries, which are populated by an external REST endpoint:
App.ItemsListRoute = Ember.Route.extend({
model: function() {
return {
client: App.Client.create()
}
}
});
Where the Client constructor looks like:
App.Client = Ember.Object.extend({
init: function() {
var _this = this; // For referencing in AJAX callback
$.ajax({
url: MY_API_URL,
type: 'GET'
}).done(function(res) {
_this.set('itemsList', parsedDataFromRes);
});
},
});
For my template that relies on the itemsList, this works great:
...
{{#each item in model.client.itemsList}}
<tr>
...
However, I have another route for a statistics, in which I would like to do some calculations on the results of the request, and return those values to the template:
App.StatsPageRoute = Ember.Route.extend({
model: function() {
var itemCount = getClient().get('itemsList').length;
return {
numItems: itemCount
}
})
I realize this is a contrived example - I could query the length on the template and it would work fine - but you'll have to humor me.
The issue with the above example is that the get('itemsList') is likely to return an undefined value, based on the data race of rendering the template, and the AJAX response and property setter being called.
How can I "wait" for a property to become available in my JS (not template code) so that it can be used to provide a model for the template?
Is converting the 'itemsList' property to a function returning a promise the most "Ember-Like" way of doing things? Would this heavily complicate my template logic?
You can use a promise and do additional operation in the then function call.
For example you can do
App.StatsPageRoute = Ember.Route.extend({
model: function() {
var itemCount = getClient().then(function(promiseResult){
var itemCount = promiseResult.get('itemsList').length;
return {
numItems: itemCount
}
})
})
To use this however, you need to make sure that getClient returns a promise. I suggested you use ic-ajax (included in Ember). It's an abstraction over jquery ajax that returns promises instead of expecting success and error callbacks
Furthermore, I strongly suggest that you look into ember-data and try to build a backend compliant with the spec. This way, you have a nice abstraction for your data and it increase development velocity tremendously, since you don't have to worry about interaction with the backend.
Edit: Returning a promise instead of data will not impact your template logic. Ember will resolve the promise and the Ember run loop will update the template automatically when the value changes. What you might want to have though, is perhaps a default value or a spinner of some kind. However, this is probably something you would have done regardless of if your model was returning a promise or not.
I'm working on a webapp to teach myself Ember, and I've walked into one large issue:
The page halts while it is attempting to fetch json, and my IndexRoute and IndexController feel very bloated. Additionally, this.store.find('pokemon') uses the RESTAdapater, and can freeze the page from rendering anything (besides the loader) for up to 1.5 seconds.
App.IndexRoute = Ember.Route.extend({
model: function() {
var store = this.store;
return Ember.RSVP.hash({
pokeballs: App.Pokeball.all(),
pokemon: store.find('pokemon'),
status: App.Status.all(),
levels: App.Levels
});
}
});
Updated Question: As it is now, my IndexController is larger than I would like, and is acting as a mediator for the pokeballs and pokemon collections. I am thinking it would be a good idea to split up IndexController so that I have an IndexController, a PokemonListController, and a PokeballListController. The problems I have are:
How should I populate the content of the PokemonListController and PokeballListController if I am on '/', which maps to the IndexRoute?
Is this actually a good idea, am I treating controller's they way they are intended to be treated?
Webapp Demo: http://theirondeveloper.github.io/pokemon-catch-rate
Github: https://github.com/TheIronDeveloper/pokemon-catch-rate
On one hand you are not tied to a single controller in a route, there is generally only a single controller associated with a route, but you can always set more controllers if you need them to, remember they are decorators of your models.
App.IndexRoute = Ember.Route.extend({
model: function() {
return store.find('pokemon');
},
setupController: function(controller, model) {
var pokemonListController = this.controllerFor('pokemons');
var pokeballListController = this.controllerFor('pokeball');
controller.set('model', model); //this would be the index controller
pokemonListController.set('model', model.pokemon);
pokeballListController.set('model', model.pokeballs);
}
});
Also you can render your page if you need to, without waiting for the responses, Ember will handle updating your UI once the response is received. if your response is too slow, the user will see the page, and an empty list (in this case, empty list of pokemon), and then once the request is resolved, the list will fill up with it.
To do that, just return an empty array from your model hook, and update it async:
App.IndexRoute = Ember.Route.extend({
model: function() {
var pokemon = [];
var store = this.store;
store.find('pokemon').then(function(allPokemon) {
pokemon = allPokemon; //untested, you may need to push them instead
});
return Ember.RSVP.hash({
pokeballs: App.Pokeball.all(),
pokemon: pokemon,
status: App.Status.all(),
levels: App.Levels
});
}
});
Not seeing anything "bloated" about your IndexRoute or IndexController. It is true that a lot of Ember apps will have multiple routes and thus multiple controllers, but that happens when it makes sense to switch to other routes. If it doesn't make sense for your application - then what you have is great.
If you have multiple routes (and thus multiple controllers), the approach #Asgaroth suggested will work great for setting multiple controllers. Otherwise, if you only have a single route - there is really no need to have multiple controllers.
The fact that your data gets fetched and that takes some time is normal. Now, ideally this (data fetching) should only happen once and your data would then get cached and as you peruse around your other routes (which you currently do not have) your data would already be available to you without any extra penalty.
If you do need to have multiple controllers and are wondering how to communicate between them, you are probably looking for the needs API outlined here.
UPDATE
I took another look at the model hook and it is weird how 3 out of 4 things in there are not promises at all and don't look like they belong in there.
So, here is how you can clean that up.
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('pokemon');
}
});
That's the only thing that belongs in there. The other properties might as well be properties on your controller, as in:
App.IndexController = Ember.Controller.extend({
levels: function(){
return App.Levels;
}.property(),
pokeballs: function(){
return App.Pokeball.all()
}.property(),
status: function(){
return App.Status.all();
}.property(),
Of course, you would then need to change references to those properties in your template and other code. So, for example, you would change from model.pokeballs to just pokeballs. You would also change from model.pokemon to just model
I submitted a pull request to show you the way I did this - see here
Not a full answer, but to reveal the magic between the route and controller ... here is how the model gets drop'd into the controller instance for you
App.IndexRoute = Ember.Route.extend({
model: function() {
return store.fin('pokemon');
},
setupController: function(controller, model) {
//the model that gets returned from the above method is added to the controller instance for you in this generated method on the route
controller.set('model', model); //also alias'd as content in older versions of ember
}
});
I'm trying to write a route that need access to its parent's model. I use this.modelFor(), but when I do that, the parent's model isn't completely loaded, so all its properties contains null.
This is the router, with two dynamic segments:
MGames.Router.map(function () {
this.resource('games', function () {
this.resource ('game', {path: '/:game_id'}, function () {
this.resource('board', {path: '/boards/:board_id'});
});
});
});
This is my GameRoute, who works perfectly:
MGames.GameRoute = Ember.Route.extend ({
model: function (params) {
return MGames.Game.find(params.game_id);
}
});
And finally this is the child route, who need access to the Game model, and this is what I wrote. But no matters what I do, the console.log() always prints null. If I check the game variable, the isLoad property is always null:
MGames.BoardRoute = Ember.Route.extend ({
model: function (params) {
var game = this.modelFor ('game');
console.log (game.get("id"));
return MGames.Board.find(game.get("id"), params.board_id);
}
});
Am I doing something wrong, or (as I suspect) I'm missing some Ember concept?
This part of your code looks good. Your assumptions are correct in that, the nested route should get the model of the parent via modelFor.
I suspect your find method is the source of the bug. I looked at your previous question, and I'm assuming the same Game.find is used here(?)
The problem is to do with Promises. Ember's router understands the async nature of the model hook. But it relies on you returning a Promise to do it's work. Currently you are using the jQuery promise but returning the game object immediately in it's uninitialized state. The query loads from the server but the model() hook is assumed to have resolved before that happens.
You want to directly return the jQuery Promise from your model hook + Do the parsing in the first then and return that as the result.
Here's your modified Game.find. Same principles apply to the other finders.
find: function (game, board) {
url = [MGames.GAMES_API_URL];
url.push ('games');
url.push (game);
url.push ('boards');
url.push (board);
url = url.join('/');
return $.getJSON(url)
.then(function(response) {
var game = MGames.Game.create({isLoaded: false});
game.setProperties(response);
game.set('isLoaded', true);
return game;
});
}
Note that, the game object is returned as is. Ember understands that when the promise is resolved(by returning anything other than a promise), that result is the model for the model() hook. This game object is the model that will be now be available in modelFor in the nested route.