13
votes

Let's say I have a template which iterates over a collection of items, and I want to call a function with each item which is specific to the controller, and not a model-level concern:

{{#each people as |person|}}
  icon name: {{findIconFor(person)}}
{{/each}}

I'd like to define findIconFor in the controller, because this is something specific to this particular view.

export default Ember.Controller.extend({
  findIconFor: function(person) {
    // figure out which icon to use
  }
);

But that doesn't work. The template fails to compile. Parse error: Expecting 'STRING', 'NUMBER', 'ID', 'DATA', got 'INVALID'

What is the "ember way" to do this?

3
Well, you can't do that, because you can't do that. You could put a computed property on person, then just do person.icon. - user663031

3 Answers

9
votes

I'd use a computed property in the controller:

iconPeople: Ember.computed('people.@each', function(){
  var that = this;
  return this.get('people').map(function(person){
    return {
      'person': person,
      'icon': that.findIconFor(person)
    };
  });
})

Now you could get the icon from {{person.icon}} and the name from {{person.person.name}}. You might want to improve on that (and the code is untested), but that's the general idea.

15
votes

As i spent almost entire day on a similar problem here is my solution.

Because Ember for some reason just doesn't allow you to run a controller functions directly from the template (which is ridiculous and ties your hands in some very stupid ways and i don't know who on earth decided this is a good idea ...) the thing that makes most sense to me is to create an universal custom helper, that allows you to run functions from the template :) The catch here is that you should always pass the current scope (the "this" variable) to that helper.

So the helper could be something like this:

export default Ember.Helper.helper(function([scope, fn]) {
    let args = arguments[0].slice(2);
    let res = fn.apply(scope, args);
    return res;
});

Then, you can make a function inside your controller, that you want to run, for example:

testFn: function(element){
    return element.get('name');
}

and then in your template you just call it with the custom helper:

{{#each items as |element|}}
    {{{custom-helper this testFn element}}}
{{/each}}

The first two arguments to the helper should always be "this" and the name of the function, that you want to run, and then you can pass as many extra arguments as you wish.


Edit: Anyway, every time when you think you need to do this, you should think if it will not be better to create a new component instead (it will be in 90% of the cases)

1
votes

If the icon is something associated with a person, then since the person is represented by a model, it is best to implement it as a computed property on the person model. What is your intent in trying to put it into the controller?

// person.js
export default DS.Model.extend({
  icon: function() { return "person-icon-" + this.get('name'); }.property('name')
  ..
};

Then assuming that people is an array of person:

{{#each people as |person|}}
  icon name: {{person.icon}}
{{/each}}

The alternative provided by @jnfingerle works (I assume you figured out that he is proposing that you loop over iconPeople), but it seems like a lot of extra work to go to to create a new array containing objects. Does the icon depend on anything known only to the controller? If not, as I said, why should the logic to compute it be in the controller?

Where to put things is a a matter of philosophy and preference. Some people like bare-bones models that contain nothing more than fields coming down from the server; other people compute state and intermediate results in the model. Some people puts lots of stuff in controllers, whereas others prefer light-weight controllers with more logic in "services". Personally, I'm on the side of heavier models, lighter controllers, and services. I'm not claiming that business logic, or heavy data transformations, or view preparations should go in the model, of course. But remember, the model represents an object. If there's some interesting facet to the object, whether it come down from the server or be computed somehow, to me it makes a lot of sense to put that in the model.

Remember also that controllers are part of a tightly-coupled route/controller/view nexus. If there's some model-specific thing that you compute in one controller, you might have to then add it to some other controller that happens to be handling the same model. Then before you know it you're writing controller mixins that share logic across controllers that shouldn't have been in them in the first place.

Anyway, you say your icon comes from an "unrelated data store". That sounds asynchronous. To me, that hints that maybe it's a sub-model called PersonIcon which is a belongsTo in the person model. You can make that work with the right mix of adapters and serializers for that model. The nice thing about that approach is that all the asynchronicity in retrieving the icon is going to be handled semi-magically, either when the person model is created, or when you actually need the icon (if you say async: true).

But perhaps you're not using Ember Data, or don't want to go to all that trouble. In that case, you could consider adorning the person with the icon in the route's model hook, making use of Ember's ability to handle asynchronous model resolution, by doing something like the following:

model: function() {
  return this.store.find('person') .
    then(function(people) {
      return Ember.RSVP.Promise.all(people.map(getIcon)) .
        then(function(icons) {
          people.forEach(function(person, i) {
            person.set('icon') = icons[i];
          });
          return people;
        })
      ;
    })
  ;
}

where getIcon is something like

function getIcon(person) {
  return new Ember.RSVP.Promise(function(resolve, reject) {
    $.ajax('http://icon-maker.com?' + person.get('name'), resolve);
  });
}

Or, if it is cleaner, you could break the icon stuff out into an afterModel hook:

model: function() { return this.store.find('person'); },

afterModel: function(model) {
  return Ember.RSVP.Promise.all(model.map(getIcon)) .
    then(function(icons) {
      model.forEach(function(person, i) {
        person.set('icon') = icons[i];
      });
    })
  ;
}

Now Ember will wait for the entire promise to resolve, including getting the people and their icons and sticking the icons on the people, before proceeding.

HTH.