Fine-tune refreshing of multiple models in Ember route - javascript

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'),

Related

Aurelia doesn't 'refresh' vm when navigating

Jolly good evening! In my Aurelia-App I'm using a viewModel to deal with various views via an navigationStrategy (reading out route-parameters and setting the view accordingly).
Navigation works baiscally well, there is one problem however:
When I keep navigating between routes that are based on the same viewModel, the viewModel doesn't 'refresh'. Only when navigating to a different route with a different viewModel first, and then back to the intended route, the contents are shown as expected.
It seems like the lifecycle-hooks of the component are not kicking in. Is there any way to trigger unbind() and detached() manually? Or is there a better way to do things generally?
Also the Route-Configuration seems a bit weird. When I'm taking away moduleId the app crashes, and when I'm taking away layoutViewModel the Data is not bound to the view. My Workaround for now is to assign an empty viewModel + an empty template. Am I using this wrong?
Big thanks!
configureRouter(config, Router) {
var getModelStrat = (instruction) => {
instruction.config.layoutView = "pages/templates/"+instruction.params.model+".html"
}
config.addAuthorizeStep(AuthorizeStep);
config.title = 'Aurelia';
config.map([
{
route: 'detail/:model/:id?',
name: 'detail',
moduleId: 'pages/empty',
layoutViewModel: 'pages/detail',
auth: true,
navigationStrategy: getModelStrat
},
{...}
]);
}
This is by design. Router will try to reuse existing view models.
If you need to override this per view model, then create determineActivationStrategy() method on it and return activationStrategy.replace:
import { activationStrategy } from 'aurelia-router';
export class SomeViewModel {
// ...
determineActivationStrategy() {
return activationStrategy.replace;
}
// ...
}
If you need to override this for each view model / route then take a look at Marton Sagi's answer for a similar question. Basically, all of your routes need to define activationStrategy: 'replace'.

Dynamic content loading view

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.

Loading template not showing in Ember app

I am developing an Ember-cli app with ember 2.1.
I created a templates/loading.hbs and several other templates.
My foo template has a input that I use to send a queryParam to foo/bar.
foo/bar uses the query param to find its model:
export default Ember.Route.extend({
queryParams: {
q: {refreshModel: true}
},
model: function (params) {
if (params.q) {
return this.store.query('client', { q: params.q });
} else {
return {};
}
}
});
When I go from foo to foo/bar, the model gets loaded and foo/bar gets rendered correctly, but, during the loading time, I don't get the loading template. If I hit refresh in foo/bar, then I see the loading template.
Can you help me to understand this, and how can I make sure I always get the loading template.
Thanks!
If you are using liquid-fire, the current version breaks all loading substates. I have PR'd the main repository.
https://github.com/ef4/liquid-fire/pull/384
Until then, you can add the fork to your package.json:
"liquid-fire": "git#github.com:erkie/liquid-fire.git#breaking-loading-substates"

How do I properly split apart a Controller's model?

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
}
});

How to reload correctly a route in Ember JS

I'm working with the latest release of Ember JS (RC1), and I have an architectural problem :
I have a very simple use case : a list of users, and a form to add users.
My Router:
App.Router.map(function () {
this.resource('users', function () {
this.route('new');
});
});
My Routes:
App.UsersRoute = Em.Route.extend({
model:function () {
return App.User.findAll();
}
});
My Controller:
App.UsersNewController = Em.ObjectController.extend({
saveUser:function () {
//'content' contains the user
App.User.save(this.content);
// here i want to reload the list of users, but it doesn't work
// The application goes correctly to the url /users
// But doesn't call the 'model' function
this.transitionToRoute('users');
}
});
As I say in the above comment, when I create a new User, I'd like to redirect to the list of users (that part works) AND reload the user list by calling the 'model' method of the route (that part doesn't).
I could write a method in UsersController to reload the list, but then I would have duplication between UsersRoute and UsersController.
Can someone help me on this problem ?
Thanks
P.S. : here a fiddle http://jsfiddle.net/vsxXj/
Ember Documentation on the model hook:
"A hook you can implement to convert the URL into the model for this
route."
So i do not think that this hook is right for this case. I think you should use the setupController hook for this case. Try this:
App.UsersRoute = Em.Route.extend({
setupController(controller){
controller.set("content", App.User.findAll());
}
});

Categories

Resources