Design pattern for Ember.Select usage with Ember Data (hasMany relationship)


#1

I’m working my way through an example to learn Ember.js + Ember Data and I’m struggling on Ember.Select’s usage with Ember Data.

On my first go, google led me to believe I had to handle the relationships manually by using a placeholder on the controller for Ember.Select’s selection property and with pushes for the child object(s) during saves. I also used the foreign controller to store the ‘lookup’ options for the Select’s content property. But this felt wrong as I was hijacking the controller and force-feeding it content. (Vs. using the foreign route to load the model.)

I’m now using a property on the local controlller to create an array for the select’s options, along with another computed property for sorting, and filling it in during the setupController on the local route. (For page refreshes if the foreign model has not been loaded into the store.)

And more importantly, I realized I could use the model’s hasMany relationship property as the selection key in my template. (Which felt much better.) This works beautifully except for the fact that it throws a big old error on my ‘New’ form and works great on my ‘Edit’ form if relationships are already assigned, until a user selects the supplied prompt (null) option. Then it throws an error:

My questions:

  1. Am I doing this somewhat correctly? Or am I off the reservation on usage?
  2. Is it ‘proper Ember’ to use the foreign controller to hold the lookup/option values during this view? (It works, but what happens if I end up displaying both items at the same time down the road.)
  3. What about the selection value? Should I use the local model’s property or a specific new property on the controller?
  4. Does anyone know of a good, simple, straight-forward Ember Data example app out there anywhere? (Preferably without mixins, etc.) There are plenty of post/comment snippets within Ember’s documentation for different functions of an app, but nothing that I’ve found that puts together a fully working multi-relationship CRUD example.
  5. Should I be posting this on stackoverflow? Or is this forum okay with general questions about concepts? (I figured this is kind of a mix.)

Error on menu-items/new route load:

Uncaught Error: Assertion Failed: The content property of DS.PromiseArray should be set before modifying it
Ember Debugger Active 

Error on menu-items/:menu-item_id/edit route when selecting prompt/null value:

"Uncaught TypeError: Cannot read property 'constructor' of undefined"

Code:

  • I’ve stripped down the files for the menu-items/new route to the Ember.Selet related stuff. Hopefully I’ve included everything needed to debug.
  • I’m using ember-cli, so I think I have the naming conventions correct (with dashes.)
  • I’ve left debugging in as this helps my understanding of when/what/where things happen.

router.js

import Ember from 'ember';

var Router = Ember.Router.extend({
  location: FoodieFoodENV.locationType
});

Router.map(function() {
	this.resource('menu-items', function(){
		this.route('new');
		this.resource('menu-item', { path: '/:menu-item_id' }, function() {
			this.route('edit');
		});
	});
	
	this.resource('food-groups', function(){
		this.route('new');
		this.resource('food-group', { path: '/:food-group_id' }, function() {
			this.route('edit');
		});
	});
});

export default Router;

models/menu-item.js

import DS from 'ember-data';

export default DS.Model.extend({
	title: DS.attr('string'),
	description: DS.attr('string'),
	foodGroups: DS.hasMany('food-group', { 'async': true })
});

models/food-group.js

import DS from 'ember-data';

export default DS.Model.extend({
	title: DS.attr('string'),
	description: DS.attr('string'),
	menuItems: DS.hasMany('menu-item')
});

routes/menu-items/new.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function(){
    console.group(this.toString() + ':model()');
    console.groupEnd();
    
    return this.store.createRecord('menu-item');
  },
  
  // afterModel: function(record, transition){
  //   console.group(this.toString() + ':afterModel()');
  //   console.log('record', record);
  //   console.groupEnd();
  //   i tried returning a promise here for filling in the controller lookup value, but from what I could tell, it happens before the controller is inited.
  // },
  
  setupController: function (controller, model) {
    console.group(this.toString() + ':setupController()');
    console.log('controller', controller);
    console.log('model', model);
    console.groupEnd();

    this._super(controller, model);
    
    controller.set('foodGroupsAll', this.store.find('food-group'));
  }
});

controllers/menu-items/new.js

import Ember from 'ember';

export default Ember.ObjectController.extend({
  foodGroupsAll: Ember.A(),
  foodGroupsSorting: ['title:asc'],
  foodGroupsLookup: Ember.computed.sort('foodGroupsAll', 'foodGroupsSorting'),
  
  // foodGroupsAllLog: function() {
  //   console.group(this.toString() + 'foodGroupsAllLog');
  //   console.log('foodGroupsAll', this.get('foodGroupsAll'));
  //   console.groupEnd();
  // }.property('foodGroupsAll'),
  
  init: function() {
    this._super();
    console.group(this.toString() + ':init()');
    console.groupEnd();
  }
});

templates/menu-items/new.js

<div class="form-group">
    <label for="input-food-groups" class="control-label">Food Group(s)</label>
    {{view Ember.Select content=foodGroupsLookup 
        selection=model.foodGroups 
        optionValuePath="content.id" 
        optionLabelPath="content.title" 
        prompt="Choose Food Group" 
        multiple="true" 
        class="form-control" 
        id="input-food-groups"}}
</div>

<dl>
    <dt>foodGroupsLookup.length</dt>
        <dd>{{foodGroupsLookup.length}}</dd>
    <dt>model.foodGroups.length</dt>
        <dd>{{model.foodGroups.length}}</dd>
</dl>

{{log ''}}
{{log 'NEW-FORM'}}
{{log 'form:foodGroupsLookup' foodGroupsLookup}}

Console output:

DEBUG: -------------------------------
DEBUG: Ember      : 1.5.1 
DEBUG: Ember Data : 1.0.0-beta.8.2a68c63a
DEBUG: Handlebars : 1.3.0
DEBUG: jQuery     : 1.11.1
DEBUG: -------------------------------
<foodie-food@route:menu-items::ember261>:model() 
<foodie-food@route:menu-items/new::ember348>:model() 
<foodie-food@route:menu-items/new::ember348>:afterModel() 
  record 
  Class {id: "1i5em", store: Class, container: Container, _changesToSync: Object, _deferredTriggers: Array[0]…}
<foodie-food@controller:menu-items/new::ember416>:init()
<foodie-food@route:menu-items/new::ember348>:setupController()
  controller 
    Class {toString: function, __ember1407354815271: "ember416", __nextSuper: undefined, __ember1407354815271_meta: Object, constructor: function…}
  model 
    Class {id: "1i5em", store: Class, container: Container, _changesToSync: Object, _deferredTriggers: Array[0]…}
Transitioned into 'menu-items.new' vendor.js:16672
NEW-FORM
form:foodGroupsLookup 
[Class, Class, Class, Class, Class, Class, Class, __nextSuper: undefined, __ember1407354815271_meta: Object, __ember1407354815271: "ember540", _super: function, nextObject: function…]

#2

I found out this is a known issue: https://github.com/emberjs/data/issues/2111

this is a known issue, use rel.content for now.

…but I don’t know what "rel.content’ is referring to? I tried all different combinations of content/rel.content/controller scopes on my select’s content attribute, but it’s still not working. Can anyone shed any light on this for me? Please?


#3

Rel == Relationship, in your case it would be foodGroups.content.

In ember data, related records are stored in a ManyArray which proxies the array and does some ED magic. When you declare aysnc: true, the ManyArray is again wrapped in a PromiseProxy. The select view doesn’t seem to play nice with PromiseProxies.


#4

Thank you Joe.

Your answer is exactly what I was hoping for. A clear/working solution and an explanation that makes complete sense. (And allows me to stop obsessing over this issue.)