Nest v. Flat with EmberData
Ryan M Harrison 13 Oct 2016
Boston Ember.js
1
Problem
Nested routes (front-end)
!=
Nested collections/resources (back-end)
2
What’s a nested resource/collection?
Route (ember) : /parents
Route (ember) : /parents/10/children
Collection : /api/parents
Resource : /api/parents/10
Sub-Collection : /api/parents/10/children
Sub-Resource : /api/parents/10/child/1
3
Tl;DR: Happy path
Nested routes → Flat collections
(front-end) (back-end)
⇒ Must control API
⇒ Different semantic meaning
4
Bacon and aioli
5
github.com/rmharrison/example-ember-nested-routes
Nested routes
Router.map(function() {
this.route('bacons', function() {
this.route( 'bacon', {path: '/:bacon_id'}, function() {
this.route( 'aiolis', function() {
this.route( 'aioli', {path: '/:aioli_id'});
});
});
});
});
6
http://markup.su/highlighter/
To the rescue: http-mock
7
Collection
## Ember CLI
>> ember g http-mock bacons
## API collection
<origin>/api/bacons
Resource
## API resource
<origin>/api/bacons/:bacon_id
Route → Collection/Resource
# Front-end
8
# Back-end
Collection
Resource
Sub-Collection
Nested Resource/Collection
9
## API collection
<origin>/api/bacons/1/ailois
Sub-Resource
## API collection
<origin>/api/bacons/1/ailois/1
Naïve expectation: /api/bacons/1/aiolis
10Sub-Collection Sub-Resource
Wrong: Dynamic Adapter namespace?
11
# app/adapters/aiolis.js
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
namespace: 'api',
});
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
namespace: 'api/bacons/:bacon_id',
});
├── models
│ ├── aioli.js
│ └── bacon.js
Wrong: Nest the model?
# app/models
12
├── models
│ ├── bacon
│ │ └── aioli.js
│ └── bacon.js
GET <origin>/bacons/10/aiolis
GET <origin>/bacon/10/aiolis/1
Happy path: Flat
13
GET <origin>/aiolis?bacon_id=10
GET <origin>/aiolis/1
# app/routes/bacons/bacon/aiolis.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
let bacon = this.modelFor("bacons.bacon");
return this.store.query('aioli',
{ bacon_id: bacon.id });
}
});
# app/routes/bacons/bacon/aiolis.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return this.store.findAll('aioli');
}
});
Flat, but...
14
GET <origin>/bacons/10/aiolis
Collection
Query for :bacon_id=10
1 2 3
GET <origin>/aiolis?bacon_id=10
Sub-Collection
(bacon-specific aiolis)
Resource (:bacon_id = 10)
10
1 2 3
10
<origin>/bacons/10/aiolis/1
! =
<origin>/aiolis/1
Flat, but...
15
GET <origin>/aiolis/1?bacon_id=10
Not supported out-of-the-box by:
- findRecord()
- queryRecord()
Hack it…
GET <origin>/aiolis?aioli_id=1&bacon_id=10
Override adapters…
buildURL(), urlForQueryRecord() or urlForFindRecord()
Docs: http://emberjs.com/api/data/classes/DS.RESTAdapter.html
See: https://github.com/emberjs/data/issues/3596
# app/routes/bacons/bacon/aiolis.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
let bacon = this.modelFor("bacons.bacon");
return this.store.findAll( 'aioli',
{adapterOptions: {bacon_id: bacon.id}});
}
});
Nest, buildURL
16
Nest, buildURL
17
# app/adapters/aioli.js
import DS from 'ember-data';
export default DS.RESTAdapter.extend({
namespace: 'api',
buildURL: function(modelName, id, snapshot, requestType, query) {
var url = this._super(... arguments)
var url_new = url.replace('/api', '/api/bacons/' +
snapshot.adapterOptions.bacon_id)
return url_new;
}
});
Nest, buildURL, but...
<origin>/bacons/1/aiolis
18
Implicit path / hard-coded
Hack it through buildURL?
Override each urlFor method (11x)?
Nest, addon: ember-data-url-templates
19
# app/adapters/aioli.js
import DS from 'ember-data';
import UrlTemplates from 'ember-data-url-templates' ;
export default DS.RESTAdapter.extend(UrlTemplates, {
namespace: 'api',
urlTemplate: '{+host}/{+namespace}/bacons/{ baconId}/aiolis{/id}' ,
urlSegments: {
baconId: function (type, id, snapshot, query) {
return snapshot.adapterOptions.bacon_id;
},
},
});
but...
20
urlSegments scope [Stack Overflow]
Use Query for everything
queryRecordUrlTemplate: '{+host}/bacons/{bacon_id}/aiolis/{aioli_id}'
delete query.bacon_id
Service
urlSegments: {
baconId() { return this.get('session.baconId'); }, },
but...
Many thanks to @amiel for add-on ember-data-url-templates
PR still pending after two years: https://github.com/emberjs/rfcs/pull/4
Proposal (@terzicigor): Ember Igniter
21
## Proposed two years ago by @terzicigor
export default DS.RESTAdapter.extend({
url: 'bacons/:bacon_id',
namespace: 'api',
});
Nest, links
GET <origin>/bacons/1
22
## app/models/bacon.js
aiolis: DS.hasMany('aioli',
{ async: true }),
## <origin>/api/bacons/1
links: { aiolis: /bacons/1/aioli }
but...
- Read-only out of the box
- Must pass through parent model, i.e. bacon.attr(‘aiolis’)
- Requires HATEOAS-style API w/ links
23
What we did
24
Happy path.
Flattened everything.
More info: Nested routes
Gotchas
1) Not using the Ember CLI
2) Not using the Ember CLI
3) `index.hbs` vs `<route_name>.hbs`
4) Misusing `{{outlet}}`
Full post:
https://medium.com/@rmharrison/emberjs-gotcha-nested-routes-687a0a030ce7
25
Contact
Github: rmharrison
LinkedIn: harrisonrm
Twitter: @rmharrison_
26

Nest v. Flat with EmberData

  • 1.
    Nest v. Flatwith EmberData Ryan M Harrison 13 Oct 2016 Boston Ember.js 1
  • 2.
    Problem Nested routes (front-end) != Nestedcollections/resources (back-end) 2
  • 3.
    What’s a nestedresource/collection? Route (ember) : /parents Route (ember) : /parents/10/children Collection : /api/parents Resource : /api/parents/10 Sub-Collection : /api/parents/10/children Sub-Resource : /api/parents/10/child/1 3
  • 4.
    Tl;DR: Happy path Nestedroutes → Flat collections (front-end) (back-end) ⇒ Must control API ⇒ Different semantic meaning 4
  • 5.
  • 6.
    Nested routes Router.map(function() { this.route('bacons',function() { this.route( 'bacon', {path: '/:bacon_id'}, function() { this.route( 'aiolis', function() { this.route( 'aioli', {path: '/:aioli_id'}); }); }); }); }); 6 http://markup.su/highlighter/
  • 7.
    To the rescue:http-mock 7 Collection ## Ember CLI >> ember g http-mock bacons ## API collection <origin>/api/bacons Resource ## API resource <origin>/api/bacons/:bacon_id
  • 8.
    Route → Collection/Resource #Front-end 8 # Back-end Collection Resource
  • 9.
    Sub-Collection Nested Resource/Collection 9 ## APIcollection <origin>/api/bacons/1/ailois Sub-Resource ## API collection <origin>/api/bacons/1/ailois/1
  • 10.
  • 11.
    Wrong: Dynamic Adapternamespace? 11 # app/adapters/aiolis.js import DS from 'ember-data'; export default DS.RESTAdapter.extend({ namespace: 'api', }); import DS from 'ember-data'; export default DS.RESTAdapter.extend({ namespace: 'api/bacons/:bacon_id', });
  • 12.
    ├── models │ ├──aioli.js │ └── bacon.js Wrong: Nest the model? # app/models 12 ├── models │ ├── bacon │ │ └── aioli.js │ └── bacon.js
  • 13.
    GET <origin>/bacons/10/aiolis GET <origin>/bacon/10/aiolis/1 Happypath: Flat 13 GET <origin>/aiolis?bacon_id=10 GET <origin>/aiolis/1 # app/routes/bacons/bacon/aiolis.js import Ember from 'ember'; export default Ember.Route.extend({ model: function(params) { let bacon = this.modelFor("bacons.bacon"); return this.store.query('aioli', { bacon_id: bacon.id }); } }); # app/routes/bacons/bacon/aiolis.js import Ember from 'ember'; export default Ember.Route.extend({ model: function(params) { return this.store.findAll('aioli'); } });
  • 14.
    Flat, but... 14 GET <origin>/bacons/10/aiolis Collection Queryfor :bacon_id=10 1 2 3 GET <origin>/aiolis?bacon_id=10 Sub-Collection (bacon-specific aiolis) Resource (:bacon_id = 10) 10 1 2 3 10 <origin>/bacons/10/aiolis/1 ! = <origin>/aiolis/1
  • 15.
    Flat, but... 15 GET <origin>/aiolis/1?bacon_id=10 Notsupported out-of-the-box by: - findRecord() - queryRecord() Hack it… GET <origin>/aiolis?aioli_id=1&bacon_id=10 Override adapters… buildURL(), urlForQueryRecord() or urlForFindRecord() Docs: http://emberjs.com/api/data/classes/DS.RESTAdapter.html See: https://github.com/emberjs/data/issues/3596
  • 16.
    # app/routes/bacons/bacon/aiolis.js import Emberfrom 'ember'; export default Ember.Route.extend({ model: function(params) { let bacon = this.modelFor("bacons.bacon"); return this.store.findAll( 'aioli', {adapterOptions: {bacon_id: bacon.id}}); } }); Nest, buildURL 16
  • 17.
    Nest, buildURL 17 # app/adapters/aioli.js importDS from 'ember-data'; export default DS.RESTAdapter.extend({ namespace: 'api', buildURL: function(modelName, id, snapshot, requestType, query) { var url = this._super(... arguments) var url_new = url.replace('/api', '/api/bacons/' + snapshot.adapterOptions.bacon_id) return url_new; } });
  • 18.
    Nest, buildURL, but... <origin>/bacons/1/aiolis 18 Implicitpath / hard-coded Hack it through buildURL? Override each urlFor method (11x)?
  • 19.
    Nest, addon: ember-data-url-templates 19 #app/adapters/aioli.js import DS from 'ember-data'; import UrlTemplates from 'ember-data-url-templates' ; export default DS.RESTAdapter.extend(UrlTemplates, { namespace: 'api', urlTemplate: '{+host}/{+namespace}/bacons/{ baconId}/aiolis{/id}' , urlSegments: { baconId: function (type, id, snapshot, query) { return snapshot.adapterOptions.bacon_id; }, }, });
  • 20.
    but... 20 urlSegments scope [StackOverflow] Use Query for everything queryRecordUrlTemplate: '{+host}/bacons/{bacon_id}/aiolis/{aioli_id}' delete query.bacon_id Service urlSegments: { baconId() { return this.get('session.baconId'); }, },
  • 21.
    but... Many thanks to@amiel for add-on ember-data-url-templates PR still pending after two years: https://github.com/emberjs/rfcs/pull/4 Proposal (@terzicigor): Ember Igniter 21 ## Proposed two years ago by @terzicigor export default DS.RESTAdapter.extend({ url: 'bacons/:bacon_id', namespace: 'api', });
  • 22.
    Nest, links GET <origin>/bacons/1 22 ##app/models/bacon.js aiolis: DS.hasMany('aioli', { async: true }), ## <origin>/api/bacons/1 links: { aiolis: /bacons/1/aioli }
  • 23.
    but... - Read-only outof the box - Must pass through parent model, i.e. bacon.attr(‘aiolis’) - Requires HATEOAS-style API w/ links 23
  • 24.
    What we did 24 Happypath. Flattened everything.
  • 25.
    More info: Nestedroutes Gotchas 1) Not using the Ember CLI 2) Not using the Ember CLI 3) `index.hbs` vs `<route_name>.hbs` 4) Misusing `{{outlet}}` Full post: https://medium.com/@rmharrison/emberjs-gotcha-nested-routes-687a0a030ce7 25
  • 26.