Ember.js: parallel loading parent and child models - ember.js

I have these routes:
posts
index (all the posts)
single (with dynamic parameter post_id)
index (single post and it's comments view)
edit (editing post)
There are two separate requests for fetching post by ID and fetching post comments by post ID.
I want to load post and comments for posts.single.index route in parallel, because I have a post ID in route name and I do not have to wait when post will be loaded.
But Ember loads posts.single model, and only AFTER post it loads comments.
Is it possible to call child model in parallel with parent model?
I have found a solution when posts.single does not load anything, and posts.single.index calls two requests in it's own model. On the other hand, I should load post model within all posts.single child routes, such as posts.single.edit. It could be a problem, when application will be grow.

There are several techniques to load multiple resources in a model-Hook of a Route. Which ones best fits your needs or are even possible highly depends on your application. Especially the capabilities of the backend API used and if your Ember application uses Ember Data or plain fetch/ajax requests makes a big difference.
The simplest case is using Ember Data together with an REST API that follows JSON:API specification and supports inclusion of related resources:
import Route from '#ember/routing/route';
export default Route.extend({
model({ post_id }) {
return this.store.findRecord('post', post_id, { include: 'comments' });
}
});
If you are using plain fetch, you could use Promise.all() to load multiple records in parallel:
import Route from '#ember/routing/route';
export default Route.extend({
async model({ post_id }) {
let [post, comments] = await Promise.all([
fetch(`/posts/${post_id}`),
fetch(`/posts/${post_id}/comments`),
]);
return { post, comments };
}
});
If you don't like the Promise.all() syntax with array destructing you might want to have a look at RSVP.hash(). rsvp is bundled with ember by default.
If doing it with Ember Data but your API does not support side-loading, it's a little bit more tricky as you need to use a query to load the comments. It depends on your adapter configuration but I guess it would look like this:
import Route from '#ember/routing/route';
export default Route.extend({
async model({ post_id }) {
let [post, comments] = await Promise.all([
this.store.findRecord('post', post_id),
this.store.query('comment', {
filter: {
post: post_id
}
})
]);
return { post, comments };
}
});
You can't use multiple model-Hooks to load resources in parallel. model-Hooks of parent and child routes are executed in order by design. The model-Hook of a child route won't be fired before a Promise returned by it's parent model-Hook is resolved. Having that said there are still techniques to only load needed data and caching data that is shared between different child routes.
Let's take your example from the question and more detailed in the comments to this answer: Our frontend should show a post including it's comments on one route and a form to edit the same post on another route. Both routes need the same post resource but only one also needs the post's comment. The application should not load the post again if user transition from one route to the other and should not load the comments if user transitions to edit view.
A naive attempt would be a parent route that loads the post resource and two child routes, one for the view including comments and one for the edit form. I call this attempt "naive" cause it's failing in three ways:
The post and the comments are not loaded in parallel.
The resources aren't cached if a user transitions between the routes through a third one.
It mixes visual design with data loading.
The third point may be confusing. Indeed it's a common misunderstanding of nested routes in Ember. They are not meant to model data hierarchy but should be used to share visual UI between subroutes. It's not the model-Hook what count but the rendering of the child templates in parent's {{outlet}}.
All of your concerns could be easily solved by a service that caches resources client-side. This one of the main features of Ember Data. That's also one of the most hyped features of Apollo client for GraphQL. It's as simple as that: Most complex frontend applications need a client-side cache layer to prevent overfetching of resources. If you face that requirement I would recommend to use one of the existing solutions. For simple use cases you could also build you own service. A basic implementation could look like:
import Service from '#ember/service';
export default class StoreService extend Sevice({
postCache = new Map();
loadPost(id) {
let { postCache } = this;
if (postCache.has(id)) {
return postCache.get(id);
}
let request = fetch(`/posts/${id}`);
postCache.set(id, request);
return request;
}
});

Related

Multiple layouts in ember.js 2.7.0

I want to have two layouts, one for guests which can see some routes like: /, /contacts, /rules, etc... and another one for authenticated users, it means they must login before they can go to authorized routes. How can I define two layouts for different groups of routes?
there is a way to use Ember's router to your advantage to solve this problem by nesting the authenticated routes inside a route. Here's an example router:
Router.map(function() {
this.route('contacts');
this.route('rules');
this.route('authenticated', { path: '/' }, function() {
this.route('settings');
this.route('profile');
});
});
Going to /contacts and /rules wouldn't need any authentication, but going to /settings would.
Notice the path option passed to the authenticated route. Since we set it to / so it doesn't show up in the URL, it'll take the place of application.index. If this sounds strange to you, read about what an index page is in the Ember.js tutorial.
The answer is to maintain a service that saves the user's state.
// services/user-state
import Ember from 'ember';
export default Ember.Service.extend({
loggedIn: true
});
Then, depending on how you organized things, you could inject the service into a controller or a route. So - you'd have access to loggedIn
In your template, you would use handlebars/htmlbars if helper.
{{#if loggedIn}}
render logged-in stuff...
{{/else}}
render message explaining that this is only for logged in users
{{/if}}
You can also redirect the user to another route based on the current session data. You may have entire routes that are for certain roles, or you may have portions of your template that behave differently based on role.
If you weren't logged in, then you may not be able to visit your profile page. That route may be off limits entirely, or it may redirect you to a login page. On the other hand, maybe it's just a "login" button component or a portion of your template that shows if you aren't logged in vs. "hello sheriffderek" if you were.
There are also Ember addons for more robust "role" outlines. Think about a blog page, that may have an 'edit' button for the actual user, but 'flag' button for a moderator. In this case, you can't just have an entirely different route nested somewhere.
Most authentication addons / libraries are going to have a 'service' with some sort of session state. But Template wise, the basic idea is like a JS if/else statement.
if (helpful) {
this.upvote();
} else {
// something else
}
I hope this helps. : )
Have a look at the ember-simple-auth addon.
It has many useful classes all about authorization, including mixins for your usecase. If you want to make route only visible for logged-in users, simply use the AuthenticatedRouteMixin like this:
/app/routes/protectedRoute.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin);

preventing a duplicate request in Ember Data when trying to set the route model to the result of an association

I have a route where I need to fetch associated data that is not available in the parent routes, so I need to basically reload the model and in the process provide JSONAPI with the include directive to embed other models. So the route looks like this.
import Ember from 'ember';
// route for patients/1/appointments
export default Ember.Route.extend({
model: function() {
const query = {
id: this.modelFor('patient').get('id'),
include: 'appointments,appointments.practitioner'
},
success = function(patient) {
return patient.get('appointments');
};
return this.store.queryRecord('patient', query).then(success);
}
});
The success callback is fetching the appointments a second time, which maybe isn't surprising, but it should seemingly also know that it has those in the store locally. So, I'm trying to resolve a reasonable way to set the model to the set of appointment models coming back. For various reasons, we don't want the logic of this specific request to live in the adapters, since (for example) we may not always need the practitioner side loaded anytime we get the patient's appointments. Any ideas?
I think that there must be some issue with your response from server after first queryRecord.
I created simple Ember Twiddle that matches your example and is based on mockjax here. As you can see when you open Console, the mockjax is getting only two requests - first for /patient/1 and second for /patients?... (your queryRecord).
Even though I am rendering all appointments that are relationship to current patient, mockjax does not get any other query for relationships. You can check out the JSON responses I provided for mockjax. You should compare them with your API to see if there are different in structure.

Url not updating when new model is saved

router.js
this.route('claim', function() {
this.route('new');
this.route('edit', { path: '/:claim_id' });
});
claim.new -> redirect to claim.edit with an empty model. this.transitionTo('claim.edit', model);
Initially, because the model is not saved we get: index.html#/claim/null
When we save the model, it continues to display as index.html#/claim/null instead of say index.html#/claim/87
Is there a way to force the url to refresh?
The only way to force the URL to refresh is to force the route/page to refresh. You can use the refresh method in your route to do that.
To understand why it doesn't update automatically, you have to understand how routes work and how Ember is separate from Ember Data (or any data persistence library).
First, you have to remember that Ember has no knowledge of Ember Data and the specifics of its object model. It just sees plain Ember objects. Ember Data models are more important than other models to you, but Ember isn't aware of that distinction.
Second, the routing in Ember is binding/observer aware. It doesn't watch for changes and update URL state accordingly. It calculates the URL once using the serializer hook, then it leaves it as that. It's not going to recalculate if your model changes in any way. It will only recalculate when it's refreshed.
Short story long, the part of your application that changes the ID of your model and the part that calculates the URL to use aren't aware of each other. You'll have to manually sync them up. Using the refresh method is probably easiest way to do that.
UPDATE: As Kitler pointed out, you can also transition to your route using the new model. It won't fire the model hook so it won't have to do any asynchronous work, but it will still update the URL.

request process in emberjs

I am new guy to emberjs, I want to know flow of request in emberjs.
There are some query related to emberjs:
what is specific role of controller in emberjs, wherever I have seen we can create action in template.
There is standard naming convention and association between controller, routes and view, but how can to associate a controller, routes and views.
how to flow control when a request process?
a routes handover control to a controller or
a controller handover control to routes.
if we want to associate a controller and a routes manually then how to associate.
what is specific role of controller in emberjs, wherever I have seen
we can create action in template.
Controller is connecting your model with view like in MVC pattern. In Ember.JS you can use controller to keep your logic that will be used on one particular module, manage dependencies or store conditions. In templates you can use only simple conditions (without and/or), so whenever you need some more complex condition you should put in inside controller. For instance
App.PersonController = Ember.ObjectController.extend({
isPersonRich: function() {
return #get('person.money') > 1000000 && #get('person.isReal')
}.property('person.money', 'person.isReal')
});
So I person that is not fictional and have more 1 000 000 assets is rich.
{{#if isPersonRich}}
<p>This person is rich!</p>
{{/if}}
There is standard naming convention and association between
controller, routes and view, but how can to associate a controller,
routes and views.
Route usually fetch data from your backend.
App.PersonRoute = Ember.Route.extend({
model: function(params) {
this.store.find('person', params.person_id);
}
});
Each time when you enter the persons route ember is going to make call to your api (using ember data in this case) to find given person. Moreover, it's going to display loading route in this case and give you some fallback after failure.
PersonView would be the place where you can put your jQuery code that is going to be executed after template would be successfully rendered.
App.PersonView = Ember.View.extend({
didInsertElement: function() {
this.$().find('*[data-toggle=tooltip]').tooltip();
}
});
In this example I am adding bootstrap tooltip to template.
how to flow control when a request process?
Route is procceded before controller, you have even setupController method inside each route that set model to the controller by default.
if we want to associate a controller and a routes manually then how to
associate.
You can overwrite setupController method and eventually renderTemplate. There is nothing more to do. I advice you to stick to ember name conventions.
Additionaly take a look that if your controller is not going to handle fired action it is going to propagate to your route.

Delaying route change until data is loaded in Ember

In Angular the $routeProvider resolve property allows delaying of route change until data is loaded. Given the route model hook in Ember returns a promise I was wondering how that stuff was done in Ember
Here it's what I mean in angular Delaying AngularJS route change until model loaded to prevent flicker
A link with a sample code would be great
Just recently this PR introduced Async transitions to ember.js. With this change you can do all sort of things, like for example delaying a route's transition if data is still underway. A route has now all sorts of hooks available to do just want you want.
As an example (taken from the gist mentioned below) in the afterModel hook you could do something like this to only transition to the post.show route if you actually have data:
App.PostsIndexRoute = Ember.Route.extend({
afterModel: function(posts, transition) {
if (posts.length === 1) {
this.transitionTo('post.show', posts[0]);
}
}
});
Since this new features are still very young you need to use the latest master to have it available. For more info on how to use the API please see this gist.
Hope it helps