1
votes

I'm having a weird issue with the Ember.Select view when I try to bind its value to a model. Here is an abstract of what I'm doing, the complete jsbin can be found here:

Using JavaScript: http://jsbin.com/jayutuzazi/1/edit?html,js,output
Using CoffeeScript: http://jsbin.com/nutoxiravi/2/edit?html,js,output

Basically what I'm trying to do is use an attribute of a model to set an attribute of another model.

I have the Age model like this

App.Age = DS.Model.extend({
  label: DS.attr('string'),
  base: DS.attr('number')
});

And an other model named Person like this

App.Person = DS.Model.extend({
  name: DS.attr('string'),
  ageBase: DS.attr('number')
});

The template looks like this:

<!-- person/edit.hbs -->
<form>
  <p>Name {{input value=model.name}}</p>
  <p>
    Age
    {{view "select" value=model.ageBase
           content=ages
           optionValuePath="content.base"
           optionLabelPath="content.label"}}
  </p>
</form>

What I am trying to do is have a select in the Person edit form that lists the ages using base as value and label as label.

I expect the correct value to be selected when loading and to change when the selected option changes.

Has can be seen in the jsbin output, the selected is correctly populated but it sets the ageBase value of the edited person to undefined and does not select any option. The model value is correctly set when an option is selected though.

Am I doing something wrong ? Is it a bug ? What am I supposed to do to make this work ?

Thank you :)

4
Use JavaScript for your fiddles, everybody must know it and Coffee is something that some won't know. Reach the greater audience. - givanse
Ok I'll change it. Thanks - Happynoff
When does it get set to undefined? I don't see any buttons or save actions. - givanse
When the page is loaded. Before displaying the page a value is set. But when the page is displayed, the value is emptied and is only set again when an option is selected. - Happynoff

4 Answers

3
votes

You can conditionally render based on fulfilment of the ages as follows, since select doesn't handle promises (more on that below):

{{#if ages.isFulfilled}}
    {{view "select" value=ageBase
           content=ages
           optionValuePath="content.base"
           optionLabelPath="content.label"}}
{{/if}}

I updated your JsBin demonstrating it working.

I also illustrate in the JsBin how you don't have to qualify with model. in your templates since object controllers are proxies to the models they decorate. This means your view doesn't have to be concerned with if a property comes from the model or some computed property on the controller.

There is currently a PR #9468 for select views which I made a case for getting merged into Ember which addresses some issues with selection and option paths. There is also meta issue #5259 to deal with a number of select view issues including working with promises.

From issue #5259 you will find that Ember core developer, Robert Jackson, has some candidate select replacements. I cloned one into this JsBin running against latest production release version of Ember.

There is nothing at all preventing you using Roberts code as a select view replacement in your app. Asynchronous collections/promises will just work (and it is MUCH faster rendering from the benchmarks I have seen).

The template for that component is just:

{{#if prompt}}
  <option disabled>{{prompt}}</option>
{{/if}}

{{#each option in resolvedOptions}}
  <option {{bind-attr value=option.id}}>{{option.name}}</option>
{{/each}}

The js of the component is:

App.AsyncSelectComponent = Ember.Component.extend({
  tagName: 'select',
  prompt: null,

  options: null,
  initialValue: null,

  resolvedOptions: null,
  resolvedInitialValue: null,

  init: function() {
    var component = this;

    this._super.apply(this, arguments);

    Ember.RSVP.hash({
      resolvedOptions: this.options,
      resolvedInitialValue: this.initialValue
    })
      .then(function(resolvedHash){
        Ember.setProperties(component, resolvedHash);

        //Run after render to ensure the <option>s have rendered
        Ember.run.schedule('afterRender', function() {
          component.updateSelection();
        });
      });
  },

  updateSelection: function() {
    var initialValue = Ember.get(this, 'resolvedInitialValue');
    var options = Ember.get(this, 'resolvedOptions') || [];
    var initialValueIndex = options.indexOf(initialValue);
    var prompt = Ember.get(this, 'prompt');

    this.$('option').prop('selected', false);

    if (initialValueIndex > -1) {
      this.$('option:eq(' + initialValueIndex + ')').prop('selected', true);
    } else if (prompt) {
      this.$('option:eq(0)').prop('selected', true);
    }
  },

  change: function() {
    this._changeSelection();
  },

  _changeSelection: function() {
    var value = this._selectedValue();

    this.sendAction('on-change', value);
  },

  _selectedValue: function() {
    var offset = 0;
    var selectedIndex = this.$()[0].selectedIndex;

    if (this.prompt) { offset = 1; }

    return Ember.get(this, 'resolvedOptions')[selectedIndex - offset];
  }
});
0
votes

The problem is that in:

{{view "select" value=model.ageBase

When the app starts, value is undefined and model.ageBase gets synchronized to that before value is synchronized to model.ageBase. So, the workaround is to skip that initial undefined value.

See: http://jsbin.com/rimuku/1/edit?html,js,console,output

The relevant parts are:

template

{{view "select" value=selectValue }}

controller

App.IndexController = Ember.Controller.extend({

  updateModel: function() {
    var value = this.get('selectValue');
    var person = this.get('model');

    if ( value ) { // skip initial undefined value
      person.set('ageBase', value);
    }
  }.observes('selectValue'),

  selectValue: function() {
    // randomly used this one
    return this.store.find('age', 3);
  }.property()

});
0
votes

givanse's answer should work.

I don't think it's because value is undefined, but because value is just an integer (42) and not equal to any of the selects content, which are Person objects ({ id: 2, label: 'middle', base: 42 }).

You could do something similar to what givens suggests or use relationships.

Models

//
// Models
//

App.Person = DS.Model.extend({
  name: DS.attr('string'),
  ageBase: DS.belongsTo('age', { async: true })
});

App.Age = DS.Model.extend({
  label: DS.attr('string'),
  base: DS.attr('number')
});

//
// Fixtures
//

App.Person.reopenClass({
  FIXTURES: [
    { id: 1, name: 'John Doe', ageBase: 2 }
  ]
});

App.Age.reopenClass({
  FIXTURES: [
    { id: 1, label: 'young',  base: 2  },
    { id: 2, label: 'middle', base: 42 },
    { id: 3, label: 'old',    base: 82 }
  ]
});

Template:

<h1>Edit</h1>

<pre>
  name: {{model.name}}
  age: {{model.ageBase.base}}
</pre>

<form>
  <p>Name {{input value=model.name}}</p>
  <p>
    Age
    {{view "select" value=model.ageBase
           content=ages
           optionValuePath="content"
           optionLabelPath="content.label"}}
  </p>
</form>
0
votes

Ok, I found a solution that I think is more satisfying. As I thought the issue was coming from ages being a promise. The solution was to ensure that the ages list was loaded before the page was rendered.

Here is how I did it:

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return Ember.RSVP.hash({
      person: this.store.find('person', 1),
      ages: this.store.findAll('age')
    });
  }
});

That's it! All I need from there is to change the view according the new model:

{{#with model}}
  <form>
    <p>Name {{input value=person.name}}</p>
    <p>
      Age
      {{view "select" value=person.ageBase
             content=ages
             optionValuePath="content.base"
             optionLabelPath="content.label"}}
    </p>
  </form>
{{/with}}

The complete working solution can be found here: http://jsbin.com/qeriyohacu/1/edit?html,js,output

Thanks again to @givanse and @Larsaronen for your answers :)