Where to put global state in Ember.js? - ember.js

In my Ember.js application users can have multiple projects. Since only one project can be viewed at a time, the active project is selected via the navigation bar. This is basically a global state (which is also reflected in the URL /projects/xyz).
Since multiple components depend on the project selection, where do I put this information? And in what form do I save it (instance or id)?
About my status quo: I have a route that intercepts the call to setupController for /projects/:project_id and uses App.set("projectId", model) to place the instance in the global namespace. This seems bad, doesn't it?

Whenever you have global state that is reflected in the url you can use the ember router to manage that state.
For example, let's say you have a tasks resource nested under project like:
App.Router.map(function() {
this.resource('project', { path: '/projects/:project_id' }, function() {
this.route('edit');
this.resource('tasks', function() {
this.route('new');
});
});
});
Ember will use the project_id url segment to find your model and set it as the content of ProjectController. To access the currently selected project from another controller, declare a dependency using the needs array and access it via the controllers property.
App.TasksController = Ember.ArrayController.extend({
needs: ['project']
});
// tasks/index.hbs
Project name: {{controllers.project.name}}
See controllers-needs-explained

Related

Ember.js: parallel loading parent and child models

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

Ember 2.5 Application Controller not present?

This is a very simple and probably easily resolved one.
I have created an emberjs application controller via ember generate controller application from which I want to return some basic computed properties based on the current path to higher level controllers and components. Basically, something like this:
export default Ember.Controller.extend({
entity: Ember.computed('currentPath', () => {
return this.get('currentPath').split('.')[0];
})
});
Oddly enough, I cannot access these computed properties anywhere (they turn out undefined, even if I replace them with a debug string), and in the Ember Inspector's view tree, the application controller is apparently not even present:
I have an older Ember 1.13.0 app, where I'm using the application controller with no difficulty. Have I missed a deprecation here? Or do I need to register the application controller in a specific location?
okay, I solved this differently using injection of the routing service directly into the component (see Component to be notified on route change in EmberJS 2 for details)

How do you update a controller from another controller in ember.js?

In my application I have common header that outlets into the main application layout. In that header you can select a site. I need that site selection to update another template that is rendered in the application layout. Having struggled with this for a few days I think the correct way to solve this is to use a shared service, to have the header controller observe the site selection and set that value in the shared service, then to have the index controller use a Ember.computed.alias on the value in the service controller. The following is an example of my code:
controllers/header.js
import Ember from 'ember';
export default Ember.Controller.extend({
sessionService: Ember.inject.service(),
currentSiteChanged: Ember.observer('session.current_site', function(){
var current_site = this.get('session.current_site');
console.log('currentSiteObserver', current_site);
this.get('sessionService').set('currentSite', current_site);
}),
});
controllers/index.js
import Ember from 'ember';
export default Ember.Controller.extend({
sessionService: Ember.inject.service(),
currentSite: Ember.computed.alias('sessionService.currentSite'),
dashboardData: function(){
var currentSite = this.get('currentSite');
console.log("in dashboardData", currentSite);
//other code that uses the currentSite
}.property('currentSite'),
});
services/session-service.js
import Ember from 'ember';
export default Ember.Service.extend({
currentSite: null,
setCurrentSite: function(){
var currentSite = this.get('session.current_site');
this.set('currentSite', currentSite );
}.on('init'),
});
I think this should allow someone to select a site in the header and have the dashboardData property in index update to use that selection. When the page initially loads the header defaults to the first site and the index renders it has the correct site value that it must have gotten from the session-service, however if you select another site the index does not get updated. Via the console.logs and debugging I can see that the header is observing the change and setting the value on the session-service.
Additionally I have tried solving this other ways (injecting the header service into the index and observing a property, injecting the index in the header and directly setting the value, sending and listening to events,etc) but I am willing to try anything or to be corrected that this isn't the correct way to solve the problem.
I am using ember 1.13.8 and moving to 2 isn't an option at the moment.
I don't think a service is an appropriate solution to this problem.
You want your application to have a good RESTful url design (respect for urls is a corner-stone of the Ember framework), so try to capture your application state in the URL.
Consider that if a user were to select a site, and then hit refresh they would lose their selection unless you stored it somehow in a cookie or localStorage.
I would recommend using either routes or query parameters to solve your problem.
Routes
Using routes is fairly straightforward (http://whatever.com/sites/pet-hampsters).
Query Params
You can also use query params, something like this http://whatever.com/?site=pet%20hampsters.
To do this you would write an action that bubbles up to your application controller and sets the value of the 'site' queryParam. Any of your sub-controllers on the currently active route can then read the site value with the new Ember.inject syntax. This is the conventional way to manage dependencies between controllers.

RESTAdapter vs Localstorage. Why should I switch to RESTAdapter?

I've been working with Ember for several months now. Our project had to run offline so we used App Cache and Ember Localstorage Adapter. I've become very comfortable with this approach. We load all data at once from the server and then use it throughout the project's Routes.
Currently we must build the Admin Panel for our project. It is not required to work offline. I thought I might give Ember's RESTAdapter a try since it seems to be the most popular way of working with data.
At first impression it seems to me that it makes things more complicated.
For example (and I'll oversimplify things for the sake of the example), I have 2 entities:
App.User = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
phoneNumber: DS.attr('string'),
studies: DS.hasMany('study')
});
App.Study = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
});
And the rotues
App.UsersRoute = Ember.Route.extend({
model: function(){
return this.store.find('user');
}
});
App.UserRoute = Ember.Route.extend({
setupController: function (controller, model) {
var userId = model.get('id');
this._super.apply(this, arguments);
this.store.find('user', userId).then(function (user) {
controller.set('user', user);
});
}
});
On the /users route I show only the users basic info.
On the /user route I show his info and the studies to which he is assigned to.
This means the if the user moves from /users to /user route using a transitionTo, the user template will receive the user model from the in memory data store. Still, there are no studies loaded yet and won't be able to show them. Eventually, we must sideload the studies models once with the users.
If the /user route is accessed directly from the browser's URL or a page refresh occurs then the server must provide the user model with the studies sideloaded once again.
So on the server side we must implement two controller methods which must return something like this.
/users route:
`{
"users":[{/*user1*/},{/*user2*/}],
"studies":[{/*study1*/}, {/*study2*/}, {/*study3*/}]
}`
/user route -
{
"user":{/*userdata*/},
"studies":[/*and array of studies*/]
}
Considering that we don't use big sets of data. Supposingly up to 20 users and up to 20 studies, why shouldn't I load them at once and store them using the localstorage adapter (in order to keep the data safe from page refresh) and use RESTAdapter?
It seems to me that by using RESTAdapter, we must write much more code on the server side and most importantly the user experience will be affected. Most of the time when a route will be accessed there will be a delay of a few seconds while the request makes its roundtrip to the server and back.
Please share your opinion. Why use RESTAdapter if there are no large data sets? How big should the data set be in order to be forced to use RESTAdapter? Are there any other advantages over localstorage?
You can always push everything you need into the store (using push, pushMany or pushPayload), in this case you don't have a big amount of data, so its not a big deal, but you will still have to fetch it at least one from the server, which is your source of truth.
But since this is the admin panel, I guess you are not only displaying data, but also updating and probably deleting, so you will have to still implemenet the methods to handle that on the server side, and REST is the standar approach to do that, but ember does not force you to use it, if you want you can alwasy do $.post (not recommended).
You also don't need to side load everything, you could define your relationshipt async
studies: DS.hasMany('study', {async:true} )
And then ember would first hit /user and then /studies/{id} for each study related to that user.
See the guide for more information.

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.