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:
1 2 3 4 5 6 7 8 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:
<script type="text/x-handlebars" data-template-name="artists/songs">
(...)
{{#each songs}}
<div class="list-group-item">
{{title}}
{{star-rating item=this rating=rating maxRating=5 setAction="setRating"}}
</div>
(...)
{{#each}}
</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:
1 2 3 4 5 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:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
This action is then handled by the controller:
1 2 3 4 5 6 7 8 9 10 11 12 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:
<script type="text/x-handlebars" data-template-name="components/star-rating">
{{#each stars}}
<span {{bind-attr data-rating=rating}}
{{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}}
{{action "setRating"}}>
</span>
{{/each}}
</script>
becomes this:
<script type="text/x-handlebars" data-template-name="components/star-rating">
{{#each stars}}
<span
{{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}}
{{action "setRating" rating}}>
</span>
{{/each}}
</script>
And then setRating
simply receives the new rating as an argument:
1 2 3 4 5 6 7 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.