1
votes

I am re-writting my app to be compliant with Ember 2 and have run into a big problem when trying to refactor an ItemController.

Currently I have an arrayController that has an array of items. The user has a number of options for sorting the data that is displayed that can be selected. Some of the sorts depend directly on a property set on the controller (supplied by the user via the template). To do this I currently declare an item controller, which means that I can create a computed property to compute the sort value I need. This can’t be done via a computedProperty on the model as the model does not know the controller property that has been set.

How do I refactor this to not use itemControllers - I’m at a loss.

I've put some code snippets together to try to explain what I mean. I am trying to sort on the itemController property numberOfSelectedFruit the value of which for each 'person' will depend on the selectedFruit property on the controller (Picked up from the template). It's a silly example but I think it illustrates the issue.

I really appreciate any help here on what is the ember 2 way of doing this (I can't see how components can help either)

// model
APP.Person = DS.Model.extend({
  name: DS.attr('string'),
  // Not proper code, but to show the structure of the fruit field
  fruit: [{type: DS.attr(‘string’), number: DS.attr(‘number’}]
})

// person_controller
APP.PersonItemController = Ember.ObjectController.extend({
  numberOfSelectedFruit: function() {
    var parentController = this.get('parentController');

    var fruitType = parentController.selectedFruit;
    var numberOfFruit = 0;
    this.get(‘fruit’).forEach(function(aFruit) {
      if(aFruit.type === fruitType) {
        numberOfFruit++;
      }
    });
    return numberOfFruit;
  }
})

APP.PersonController = Ember.ArrayController.extend({
  itemController: 'PersonItem',
  sortingOn: 'name',
  selectedFruit: 'orange',
  sortedPeople: Ember.computed.sort('filteredContent', 'sortingOn'),

})

Edit to add template code

The template code could be something as simple as the following, with a selector to select the 'fruit' (That would set selectedFruit property on the controller.

// person.hbs
<table>
  <tbody>
      {{#each sortedPeople as |person|}}
      <tr>
        <td>{{person.name}} {person.numberOfSelectedFruit}</td>
      </tr>
      {{/each}}
  </tbody>

</table>
2
Please show your template code. - Gaurav
Edited to add simple template code. It doesn't include all the elements to select the sort field or 'fruitType' (from example), as I feel this would just confuse the example. - SimonB

2 Answers

1
votes

So as previously stated, normally you can use a component as your item controller. This is very easy and explicit, and easier for most people to understand than itemControllers:

// person.hbs
<table>
  <tbody>
      {{#each sortedPeople as |person|}}
           {{person-item person=person selectedFruit=selectedFruit}}
      {{/each}}
  </tbody>

</table>

// components/person-item.js
import Ember from "ember";

const { computed } = Ember;

export default Ember.Component.extend({
    tagName: 'tr',

    numberOfSelectedFruit: computed('selectedFruit', 'person.fruit.@each.{type,number}', function() {
        const selectedFruit = this.get('selectedFruit');
        return this.get('person.fruit')
            .filter(aFruit => aFruit.type === selectedFruit)
            .map(aFruit => aFruit.number)
            .reduce((previous, current) => previous + current);
    })
});

// templates/components/person-item.hbs

<td>{{person.name}} {{numberOfSelectedFruit}}</td>

However, you say you want to be able to sort people by the numberOfSelectedFruit. Since you no longer have an itemController, ObjectController, or ArrayController you will have to move the numberOfSelectedFruit computed property to an ObjectProxy like so:

// controllers/person.js
import Ember from "ember";
import PersonProxy from "../models/person-proxy";

export default Ember.Controller.extend({
    // ...

    proxiedPeople: computed('model.[]', function() {
        return this.get('model').map((person) => PersonProxy.create({
            content: person,
            context: this
        }));
    }),

    selectedFruit: 'orange'
    sortedPeople: computed.sort('proxiedPeople', 'sortingOn'),
    sortingOn: ['numberOfSelectedFruit:asc']
});

// models/person-proxy.js
import Ember from "ember";

const { computed } = Ember;

export default Ember.ObjectProxy.create({
    selectedFruit: computed.readOnly('context.selectedFruit')

    numberOfSelectedFruit: computed('selectedFruit', 'fruit.@each.{type,number}', function() {
        const selectedFruit = this.get('selectedFruit');
        return this.get('fruit')
            .filter(aFruit => aFruit.type === selectedFruit)
            .map(aFruit => aFruit.number)
            .reduce((previous, current) => previous + current);
    })
});

ObjectProxy is what itemController was based on in Ember 1.x. See http://emberjs.com/api/classes/Ember.ObjectProxy.html for how ObjectProxy works. Hope this helps.

0
votes

To do this without an itemController, you instead define a component (in your case perhaps called person-item). A component in effect is just a controller-template pair that is conceptually abstracted from everything else. Define a property on the component called person and assign the model to it inside your {{each}} loop.

At this point, the person-item.js file (component logic) is now what used to be your item controller, and it now has its own template (person-item.hbs).

So the code is the same (pretty much), but the template is now in a separate file.