Build survey app with Ember.js - 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.

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

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.

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.

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