Readers' Letters: Making an Ember.js component even better

18 February 2014

This is Part 3 of a mini-series on components. Here are the preceding posts:

Part 1: Convert a view into a component

Part 2: Making an Ember.js Component More Reusable


Last time I showed a way to make the star-rating component more reusable. The solution employed a useful, low-level method, Ember.addObserver and its destructive sibling, Ember.removeObserver.

A couple of my readers offered alternative solutions that, I think, make the code simpler without harming reusability of the component.

This post is going to be sort of a "Readers' Letters", showing these solutions and explaining how they are better than my original take.

Ember.defineProperty

David Chen chimed in in the comments suggesting using Ember.defineProperty instead of setting up and tearing down an observer:

 1App.StarRatingComponent = Ember.Component.extend({
 2  classNames: ['rating-panel'],
 3
 4  numStars:  Ember.computed.alias('maxRating'),
 5
 6  defineFullStars: function() {
 7    Ember.defineProperty(this, 'fullStars', Ember.computed.alias('item.' + this.get('ratingProperty')));
 8  }.on('init'),
 9  (...)
10
11});

Ember.defineProperty makes a fullStars property on the component which is an alias of item.rating (or item.score). We can concatanate 'item.' with that property name in the body of defineFullStars, something I could not get around earlier.

Finally, the on function, an extension to the Function prototype sets up a listener and gets called when the component's init method is executed.

It is better, because there is a lot less code, it is more comprehensible and there is no need for a second step, tearing down the observer.

Passing in the value rating directly

Ricardo Mendes takes my approach one step further and shows that it is unnecessary to pass in the name of the ratingProperty.

Passing in the value of the property directly takes separation of concerns to the next level. The component does not need to know about the name of the rating property, all it needs to know is its value:

 1<script type="text/x-handlebars" data-template-name="artists/songs">
 2  (...)
 3  {{#each songs}}
 4    <div class="list-group-item">
 5      {{title}}
 6      {{star-rating item=this rating=rating maxRating=5 setAction="setRating"}}
 7    </div>
 8  (...)
 9  {{#each}}
10</script>

What changed is that instead of ratingProperty="rating" (which could be ratingProperty="score"), the value of the rating itself is passed in. Note that there are no quotes around rating which establishes a binding.

The definition of the fullStars property now could not be simpler and more expressive:

1App.StarRatingComponent = Ember.Component.extend({
2  classNames: ['rating-panel'],
3
4  numStars:  Ember.computed.alias('maxRating'),
5  fullStars: Ember.computed.alias('rating'),
6  (...)
7});

Since the component does not know about the rating property, it can't set the item's rating which is a good thing since it's not its responsiblity. It just sends an action to its context with the appropriate parameters:

 1App.StarRatingComponent = Ember.Component.extend({
 2  (...)
 3  actions: {
 4    setRating: function() {
 5      var newRating = parseInt($(event.target).attr('data-rating'), 10);
 6      this.sendAction('setAction', {
 7        item: this.get('item'),
 8        rating: newRating
 9      });
10    }
11  }
12});

This action is then handled by the controller:

 1App.ArtistsSongsRoute = Ember.Route.extend({
 2  (...)
 3  actions: {
 4    setRating: function(params) {
 5      var song = params.item,
 6          rating = params.rating;
 7
 8      song.set('rating', rating);
 9      App.Adapter.ajax('/songs/' + song.get('id'), {
10        type: 'PUT',
11        context: this,
12        data: { rating: rating }
13      })
14      (...)
15    }
16  }
17});

Clear separation of concerns, less and more expressive code.

Replacing data-rating

A further simplification comes from Tom de Smet.

He rightfully pointed out that there is no need to get the rating that was clicked on via a data attribute. It is already known at template rendering time and can thus be passed to the action helper.

So this:

1<script type="text/x-handlebars" data-template-name="components/star-rating">
2  {{#each stars}}
3    <span {{bind-attr data-rating=rating}}
4      {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}}
5      {{action "setRating"}}>
6    </span>
7  {{/each}}
8</script>

becomes this:

1<script type="text/x-handlebars" data-template-name="components/star-rating">
2  {{#each stars}}
3    <span
4      {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}}
5      {{action "setRating" rating}}>
6    </span>
7  {{/each}}
8</script>

And then setRating simply receives the new rating as an argument:

 1App.StarRatingComponent = Ember.Component.extend({
 2  (...)
 3  actions: {
 4    setRating: function(newRating) {
 5      this.sendAction('setAction', {
 6        item: this.get('item'),
 7        rating: newRating
 8      });
 9    }
10  }
11});

Instead of adding an extra data-binding property, we rely on the action helper and we do not need the additional fetching (and parsing) of the property.

Give credit where credit is due

This week's post was made possible by David, Ricardo and Tom. Their insights made the star-rating component impeccable for which they deserve a huge "thank you!" from me.

Share on Twitter