Ember data side loading with JSONAPI - ember.js

The request generated for my route is http://api.myApp.com/tags/123/products, but I need to do some side loading to improve performance, the desired XHR would be:
http://api.myApp.com/tags/123/products?include=sideload1,sideload2,sideload3
My router looks like this:
this.route('tags', function() {
this.route('tag', { path: ':id' }, function() {
this.route('products', function() {
});
});
});
I'll like to sideload some async models for products, currently I have:
// app/routes/tags/tag/products.js
model() {
return this.modelFor('tags.tag').get('products');
}
How would I go about adding query params in route?

I'm doing something similar in a project and store.query(type, { query }); has worked for me. http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html#toc_query-and-queryrecord
Try doing a store.query when defining your model and passing in
{include: "sideload1,sideload2,sideload3"}
Another option could be creating an adapter for your model and using buildURL to add on the query params... However this is a bit hacky and shouldn't be necessary if your API is following the JSON API standards.
App.TagsAdapter = DS.JSONAPIAdapter.extend({
buildURL: function(type, id) {
var baseURL = 'http://api.myApp.com/tags/123/products';
var method = '?include=sideload1,sideload2,sideload3';
return baseURL + method;
}
});

Related

Ember: how to add params from router to adapter

I'm trying to fetch data from the following URL structure:
${ENV.APP.API_HOST}/api/v1/customers/:customer_orgCode/sites/
I'm using an adapter to shape the request with buildURL with the following files:
// router.js
this.route('sites', { path: 'customers/:customer_orgCode/sites' }, function() {
this.route('show', { path: ':site_id' });
});
// adapters/site.js
export default ApplicationAdapter.extend({
buildURL (modelName, id, snapshot, requestType, query) {
// return `${ENV.APP.API_HOST}/api/v1/customers/${snapshot???}/sites/`;
return `${ENV.APP.API_HOST}/api/v1/customers/239/sites/`;
}
}
// routes/sites/index.js
export default Ember.Route.extend({
model: function() {
let superQuery = this._super(...arguments),
org = superQuery.customer_orgCode;
this.store.findAll('site', org);
}
});
I'm able to get the customer_orgCode on the model, but unable to pull it into the adapter. I've noted that the model isn't being populated in the Ember inspector, but the sites data is present when I make the request. Does anyone know how I can dynamically populate the buildURL with the customer_orgCode from the params on the router? And then specify sites/index to use the 'site' model?
OK, I've figured this out. I needed to use query() instead of findAll() in the route. This allows buildURL to pickup the query parameter from the route and pass it in as the 5th argument ie. - buildURL(modelName, id, snapshot, requestType, query). Then in the route, I was neglecting return as part of my model setup. So the solution is below for anyone interested.
// router.js
this.route('sites', { path: 'customers/:customer_orgCode/sites' }, function() {
this.route('show', { path: ':site_id' });
});
// adapters/site.js
export default ApplicationAdapter.extend({
buildURL (modelName, id, snapshot, requestType, query) {
let org = query.org;
return `${ENV.APP.API_HOST}/api/v1/customers/${org}/sites/`;
}
});
// routes/sites/index.js
export default Ember.Route.extend({
model: function() {
let superQuery = this._super(...arguments),
org = superQuery.customer_orgCode;
return this.store.query('site', {org:org});
}
});

"undefined" instead of ID in URL on transition in Ember and Ember Data

I have a pretty standard post model with a title and a text field. I have two routes for the model -- new route and show route. I want to create a post from new route and then transition to show route.
This is my router file
this.route('post-new', { path: '/posts/new' });
this.route('post-show', { path: '/posts/:postId' });
and submit action in post-new controller is something like this.
actions: {
submit() {
const { title, text } = this.getProperties('title', 'text');
let post = this.store.createRecord('post', {
title: title,
text: text
});
post.save().then(() => {
//success
this.transitionToRoute('post-show', post);
}, () => {
//error
});
}
}
So I am expecting this to redirect from http://localhost:4200/posts/new to something like http://localhost:4200/posts/23 (assuming 23 is id).
The save() is successful and record is created on the backend (which is rails) and I also see the post record updated in browser (it now has an ID) using Ember Inspector. But the redirection is happening to http://localhost:4200/posts/undefined.
How can I make this to redirect to something like http://localhost:4200/posts/23 after save ?
Btw, The versions are:
ember cli : 2.3.0-beta.1
ember : 2.3.0
ember data : 2.3.3
UPDATE
I was able to make it work by replacing this
this.transitionToRoute('post-show', post);
with this
this.transitionToRoute('/posts/' + post.id);
But I am hoping for a solution using the route name and not actual route path.
Try:
post.save().then(savedPost => {
//success
this.transitionToRoute('post-show', savedPost);
},
You can implement the serialize hook on your route.
serialize(model) {
return { postId: model.get('id') };
}
This will allow you to avoid calling the model hook if you already have the model. So, both of these will work as expected:
this.transitionToRoute('post-show', post); // this will NOT call model() hook
this.transitionToRoute('post-show', post.id); // this will call the model() hook
More information available in the API docs for Route.

Ember: How to cleanly replace model data and have progress indicators

I have a certain route that shows a list of projects, and it gets initial data from my RESTAdapter based on who the user is.
I am now implementing a search function that will issue a new API call so the user can get records besides the default ones for them, and the response should replace the model for that route. I have all that working, but I'm not sure how to do a loading or progress indicator (as the response from the database could potentially take 5-10 seconds depending on the amount of data). I know about loading substates, but in this case I'm not transitioning between routes. I just want to have at minimum a spinner so the user knows that it's working on something.
Would anyone that's done this before be willing to share how they handled a)replacing the model with new data, and b)keeping the user informed with a spinner or something?
Form action called when user clicks the Search button
searchProjects: function() {
var query = this.get('queryString');
if (query) {
var _this = this;
var projects = this.store.find('project', {q: query});
projects.then(function(){
_this.set('model', projects);
});
}
}
a) replacing the model with new data
You don't need to do anything. If you sideload records properly from the backend, Ember will automatically update them on the frontend.
b) keeping the user informed with a spinner or something
The loading substate is an eager transition. Ember also supports lazy transitions via the loading event.
You can use that event in order to display the spinner.
Here's an example from the docs:
App.ApplicationRoute = Ember.Route.extend({
actions: {
loading: function(transition, route) {
showSpinner();
this.router.one('didTransition', function() {
hideSpinner();
});
return true; // Bubble the loading event
}
}
});
UPD1
I need to do at least what I'm doing right? Setting the model to the response?
You need to reflect the search in the URL via query params. This will let the router automatically update the model for you.
what I would put in showSpinner to affect stuff on the page (like, can I use jQuery to show or hide a spinner element?), or show the actual loading substate.
I would set a property on that page's controller:
App.IndexRoute = Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},
model () {
return new Ember.RSVP.Promise( resolve => setTimeout(resolve, 1000));
},
actions: {
loading (transition, route) {
this.controller.set('showSpinner', true);
this.router.one('didTransition', () => {
this.controller.set('showSpinner', false);
});
return true;
}
}
});
App.IndexController = Ember.Controller.extend({
queryParams: ['search'],
search: null,
showSpinner: false,
});
Demo: http://emberjs.jsbin.com/poxika/2/edit?html,js,output
Or you could simply put the spinner into the loading template, which will hide obsolete data:
http://emberjs.jsbin.com/poxika/3/edit?html,js,output
Or you could put your spinner into the loading template:
Just in case others want to see, here's my working code based on #lolmaus's answers.
These Docs pages were helpful as well
Route's queryParams and Find method
Controller
//app/controllers/project.js
export default Ember.ArrayController.extend({
queryParams: ['q'],
q: null,
actions: {
searchProjects: function() {
var query = this.get('queryString');
if (query) {
this.set('q', query);
}
}
}
})
Route
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
if (params.q) {
return this.store.find('project', params);
} else {
return this.store.findAll('project');
}
},
queryParams: {
q: {
refreshModel: true
}
},
actions: {
loading: function(/*transition, route*/) {
var _this = this;
this.controllerFor('projects').set('showSearchSpinner', true);
this.router.one('didTransition', function() {
_this.controllerFor('projects').set('showSearchSpinner', false);
});
return true; // Bubble the loading event
}
}
});
My issue now is that when I use the parameter query, it works great, but then if I clear the query (with an action, to effectively "go back") then the records fetched by the query stay in the store, so when it does a findAll() I have both sets of records, which is not at all what I want. How do I clear out the store before doing findAll again?

Ember data single data extract through browser URL

Getting all articles is ok, but when try to retrieve just one article through route url directly, system error - undefined
var articles = Ember.$.getJSON('http://localhost/emberdata/api/articles');
will return:
[{"articles":
[
{
"id":1,
"title":"Ember is the best",
"author":"brk","excerpt":"Programming is awesome"
},
{
"id":2,
"title":"Backbone not a framework",
"author":"krb",
"excerpt":"Server-side controller sucks"
},
{
"id":3,
"title":"Javascript pwned",
"author":"rbk",
"excerpt":"I know right"
}
]
}]
API is created using PHP Slim Rest API
this Route working find, showing all the data in the handlebar template
App.ArticlesRoute = Ember.Route.extend({
model: function() {
return articles;
}
});
However for child view routing, undefined is returned
App.ArticleRoute = Ember.Route.extend({
model: function(params) {
var article = articles.findBy('id', params.article_id);
console.log(article);
return article;
}
});
Directly invoking the child view URL is not working:
http://localhost/emberdata/#/articles/1
However clicking the link of articles, those child view route works:
this.resource('article', { path: ':article_id' });
This is the error:
Ember.$.getJSON() will return a promise (see: http://emberjs.com/api/classes/Ember.PromiseProxyMixin.html). You can't call the findBy() method on a promise.
That being said, you're making it yourself very difficult. I recommend to start using the DS.RESTAdapter. In your case it would be something like this:
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'emberdata/api'
});
then clear (or remove) ArticlesRoute because you will use convention (instead of configuration):
App.ArticlesRoute = Ember.Route.extend({
});
idem for ArticleRoute. Except, if your backend doesn't support calls like /emberdata/api/article/1 use the following code:
App.ArticleRoute = Ember.Route.extend({
model: function(params) {
var article = this.store.find('article').findBy('id', params.article_id);
console.log(article);
return article;
}
});

Reload current route's model without Ember Data

Ember Data has a reload method; I'm using a more basic approach (using Ember objects) to serve up my models, though.
I'd like a user to be able to reload the model for the current route via an action when e.g. a button is clicked. Is this possible without Ember Data?
Here is a really simple example of how I do it w/ your approach (notice the "clear").
You could invoke clear+find from your route / controller / whatever. I've also added a "beforeSend" to the $.ajax in some of my bigger apps (this will invoke the clear for you before the xhr is resolved)
App.Person.reopenClass({
people: Ember.A([]),
clear: function() {
this.people = Ember.A([]);
},
add: function(hash) {
var person = App.Person.create(hash);
this.people.pushObject(person);
},
remove: function(person) {
this.people.removeObject(person);
},
find: function() {
var self = this;
$.getJSON('/api/people', function(response) {
response.forEach(function(hash) {
var person = App.Person.create(hash);
Ember.run(self.people, self.people.pushObject, person);
});
}, this);
return this.people;
}
});
The issue was the async nature of the AJAX call I was making.
This didn't work:
this.set('model', App.MyObject.findAll(value));
I needed to allow the AJAX call to return the response, then populate the model:
var that = this;
App.MyObject.findAll(value).then(function(response) {
that.set('model', response);
});