RESTAdapter vs Localstorage. Why should I switch to RESTAdapter? - ember.js

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.

Related

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.

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.

How do I access the URL that Ember Data will PUT to?

I'm adapting an old JS (no framework) + Rails app as an Ember learning exercise. The idea of the application is that I'm producing a pdf from some data input. In the initial version, there was no user persistence - you could modify the data provided to you in the tables, and then download the PDF of it.
As part of this, I decided to run with a decidedly non-standard ember framework - I'm essentially using Ember Data to load the initial value of the tables. Ember has been a really natural fit for the models I have on the Rails side, and it's made a lot of the more complicated calculations a lot easier. The issue I have is that my initial idea was that when I came to download the PDF, I'd respond to the "save" action on Ember Data with binary data (with an application/pdf header), which I could then use something like FileSaver.js to serve up to the client. Then, I found that EmberData needs JSON return value.
So I base64 encoded my PDF response and fired it back..but it didn't fit the model schema. I thought I'd then do a manual AJAX save -
CalculateYourTV.RostersShowController = Ember.ObjectController.extend({
actions:{
download: function(){
var roster = this.get("model");
var team = roster.get('team');
return this.ajax('*URL GOES HERE*', record.toJSON(), "PUT").then(function(data) {
console.log('called')
console.log(data)
});
},
}
})
And this is where I'm currently stuck. Is there any way to access the URL that EmberData is posting to? I could hard-code a route in the Rails side of things, but I don't like hardcoding routes in here, and I'd like to keep it as reusable as possible (I'm planning to eventually allow data persistance).
Just open chrome dev tools (or firebug) and monitor what's going on in the network tab. You should see any ajax request your application sends out, including the EmberData's save request.
You can change the URL that a specific model will hit by creating custom Ember Data adapters per model.
For example, say you have a person model that needs to not hit the default /persons URL.
App.PersonAdapter = App.ApplicationAdapter.extend({
pathForType: 'special/custom/endpoint/for/folks'
});
That said, Ember Data may not be the best tool here for your PDF "model". You can always use Ember Data for the majority of your models, but use a quick $.ajax for other stuff that doesn't fit your definition of a true model.
You can ask the adapter to build a URL for you -
adapter = #store.adapterFor('application')
url = adapter.buildURL(type, id)
where type is the name of the model, and id is its id.
If want to look up the adapter directly in the container it is
#container.lookup('adapter:application')

Build survey app with Ember.js

I'm learning (and enjoying a lot) Ember.js. I'm trying to figure out the best way to create a survey: my idea is to use a different route for each of my question pages, so when you answer, you transition to the next route.
App.Router.map(function() {
this.route("question1", { path: "/q1" });
this.route("question2", { path: "/q2" });
this.route("question3", { path: "/q3" });
this.route("questionN", { path: "/qN" });
});
Of course, any route has its controller and model. Problem is: when the survey reaches the last page and I must send data to the server, if the data is tied to the single route, how do I collect anything for sending? Is there any way to store the data outside the current route? How?
And: is there any way to prevent the user from entering /qN path if he has not answered all the previous questions?
Thanks in advance!
You could create a model or a ember Object to storage all the data that you need and then submit that data to the server through Ajax.

How to make the Ember Local Storage Adapter (LSAdapter) work with the Ember REST Adapter?

I would like to take advantage of both the Ember Local Storage Adapter (LSAdapter) and the Ember REST Adapter (RESTAdapter), so that there is a centralized database for all the users while avoiding sending an Ajax request for each user action.
Specifically, I would like to:
Populate the LSAdapter with data from the server.
All changes users make are updated to the LSAdapter. (I know how to do this step)
(In preparation for #3, I can have a model that saves a log in LSAdapter of all the updates)
Once in n minutes, the LSAdapter update data are sent back to the server.
My backend server is NOT Rails, so please make the answers generic.
Is it possible to use BOTH LSAdapter and RESTAdapter in an Ember app? If so, please provide code sample of how to set it up.
I would also appreciate if you provide code sample for steps 1 and 3, basically, how a database can talk to the local storage and vice versa.
If it's not possible to have both LSADapter and RESTAdapter, what can I do to accomplish steps 1 and 3?
The only get around I can think of is to set up the Ember app store as a RESTAdapter, but then call Web Storage localstorage directly in my app, not calling it from LSAdapter at all. Let me know if there is an easier or built-in way.
After reading Ember data's DS.Store comments, it looks like it might be possible to use 2 adapters at once:
You can retrieve models from the store in several ways. To retrieve a record
for a specific id, use DS.Model's find() method:
var person = App.Person.find(123);
If your application has multiple DS.Store instances (an unusual case), you can
specify which store should be used:
var person = store.find(App.Person, 123);
I will update this answer if I try it out and get it working.
Update 1:
Check out the code in UPDATED for Additional question where both FixtureAdapter and RestAdapter are called.
//define your main adapter as usual
App.Store = DS.Store.extend({
revision: 13,
adapter: DS.RESTAdapter.create({
url: app.apiUrl,
namespace: app.apiNamespace
})
});
//App.Product for example will use the rest adapter
App.Product = DS.Model.extend({
name: DS.attr('string'),
});
App.Somemodel = DS.Model.extend({
name: DS.attr('string'),
});
//App.Somemodel for example will use the fixture adapter
App.Store.registerAdapter('App.Somemodel', DS.FixtureAdapter);