bsmldrs bsmldrs - 1 month ago 21
Javascript Question

EmberJS Custom adapter using ember-data-url-templates

I'm once again struggling with the adapters in EmberJS. This time it's related to nested api requests with the usage of ember-data-url-templates.

First of all, the relevant code:

// /app/application/adapter.js
import DS from 'ember-data';

var AppAdapter = DS.JSONAPIAdapter.extend({
host: 'http://coursedev.api'
});

export default AppAdapter;

// /app/router.js
import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
location: config.locationType
});

Router.map(function() {
this.route('courses');
this.route('course', { path: '/course/:course_id' });
this.route('lesson', { path: '/course/:course_id/lesson/:lesson_id' });
});

export default Router;

// app/course/model.js
import DS from 'ember-data';

export default DS.Model.extend({
order: DS.attr('number'),
title: DS.attr('string'),
body: DS.attr('string'),
lessons: DS.hasMany('lesson')
});

// app/lesson/model.js
import DS from 'ember-data';

export default DS.Model.extend({
order: DS.attr('number'),
title: DS.attr('string'),
body: DS.attr('string'),
course: DS.belongsTo('course'),
questions: DS.hasMany('question')
});

// app/lesson/adapter.js
import AppAdapter from '../application/adapter';
import UrlTemplates from "ember-data-url-templates";

export default AppAdapter.extend(UrlTemplates, {
urlTemplate: '{+host}/courses/{courseId}/lesson/{id}',

urlSegments: {
host: function () {
return this.get('host');
},
courseId: function(type, id, snapshot, query) {
return snapshot.belongsTo('course', { id: true });
}
}
});

// app/lesson/route.js
import Ember from 'ember';

export default Ember.Route.extend({
model(params) {
return this.store.findRecord('lesson', params.lesson_id );
}
});

// app/lesson/template.hbs
<h1>{{model.title}}</h1>

{{model.body}}
<br>
This lesson is part of {{#link-to 'course' model.course.id}}{{model.course.title}}{{/link-to}}


The idea is that a course consists of several questions. The api provides data in a nested way:

/courses > all the courses
/courses/{courseId} > detailed course info
/courses/{courseId}/lesson/{lessonId}/ > detailed lesson info


This code is working fine when navigating 'inside' the application, as in, not accessing the route directly. It then shows the appropriate title, body and which course it is part of. But, when navigating to /courses/1/lesson/3 directly, the adapter isn't able to find the courseId from the snapshot since the request is pointed to http://coursedev.api/courses//lesson/3. Note the lack of the courseId. This URL doesn't exist, since a courseId should be provided.

My first question is, is the approach above right to handle nested api urls? And if that is the case, then how should the courseId be gathered to do a proper request to the api?

A massive thanks again!

Answer

Ok, yes. I understand your confusion.

What you have is working fine when you already have a lesson loaded with a course. When navigating directly to the lesson, you'll need to get the course id from the url and somehow pass it to the adapter. There is no obviously better solution to this; here are a few options, in order of preference:

1. Don't use nested urls in your API

If you have control over the structure of your API, I recommend not nesting your resources in this way. It might make sense to load a collection sub-resource in a nested way (such as /courses/:course_id/lessons), but /lessions/:lesson_id is preferred over /courses/:course_id/lessons/:lesson_id.

However, you may not have control over your API, which is exactly why ember-data-url-templates was created...

2. Use queryRecord

You can use queryRecord to load one record and pass more information that just an id.

// adapter
export default AppAdapter.extend(UrlTemplates, {
  queryRecordUrlTemplate: '{+host}/courses/{course_id}/lesson/{lesson_id}',
});

// route
this.store.queryRecord('lesson', params);

3. Use a service

Another way to pass information to your adapter is using a service. This approach does not look appropriate to your situation, but I'll include it here in case it helps another similar issue. I frequently use a service to hold general session information (such as a user id). Here's how you could accomplish the same thing as above with a session service:

// adapter
export default AppAdapter.extend(UrlTemplates, {
  session: Ember.inject.service(),
  findUrlTemplate: '{+host}/courses/{courseId}/lesson/{id}',

  urlSegments: {
    courseId() { return this.get('session.courseId'); },
  },
});


// route model hook
this.set('session.courseId', params.course_id);
this.store.findRecord('lesson', params.lesson_id);

I hope this helps :)