UPDATE: I recently published a WintellectNow course – Getting Started with Ember.js. You should also check out Jeremy Likness’ in depth series on Angular – Mastering AngularJS. Use promo code NSTIEGLITZ-13 for a free two week trial.
Last week, I published a blog post that shows how to build a reusable Star Rating Component using Ember.js. For this blog, I will share how to build the same functionality using a custom directive in Angular.js. I’m not the first person to write a star rating component in Angular.js; this post is more about comparing the bits needed in the respective frameworks.
There are more similarities than differences in the way you build reusable controls, but as you’ll see, there are some key differences. Below is a screenshot of a demo app which uses the Star Rating component.
Angular Directives
At the highest level, Angular directives are reusable components used to manipulate the DOM. Angular comes with several built in directives like ngClick, ngRepeat, etc. It’s also pretty straight forward to write custom directives. A custom directives looks just like HTML; you can define a directive to be used as an element, attributes, CSS class, or less commonly, as a comment. For example, here is the ngClick directive (as an attribute):
<button ng-click="count = count + 1" ng-init="count=0">
.csharpcode {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
margin: 0em
}
.csharpcode .rem {
color: #008000
}
.csharpcode .kwrd {
color: #0000ff
}
.csharpcode .str {
color: #006080
}
.csharpcode .op {
color: #0000c0
}
.csharpcode .preproc {
color: #cc6633
}
.csharpcode .asp {
background-color: #ffff00
}
.csharpcode .html {
color: #800000
}
.csharpcode .attr {
color: #ff0000
}
.csharpcode .alt {
width: 100%; margin: 0em; background-color: #f4f4f4
}
.csharpcode .lnum {
color: #606060
}
Here is a an example of using a custom directive as an element:
<customer-address></customer-address>
.csharpcode {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
margin: 0em
}
.csharpcode .rem {
color: #008000
}
.csharpcode .kwrd {
color: #0000ff
}
.csharpcode .str {
color: #006080
}
.csharpcode .op {
color: #0000c0
}
.csharpcode .preproc {
color: #cc6633
}
.csharpcode .asp {
background-color: #ffff00
}
.csharpcode .html {
color: #800000
}
.csharpcode .attr {
color: #ff0000
}
.csharpcode .alt {
width: 100%; margin: 0em; background-color: #f4f4f4
}
.csharpcode .lnum {
color: #606060
}
Read more about Angular Directives here.
Defining the Directive
Just like an Ember Component, an Angular directive has a template and a corresponding script that handles the DOM manipulation and eventing. Let’s compare the templates…
Angular.js Template
<ul style="padding: 0"> <li ng-repeat="star in stars" style="color: #FFD700;" class="glyphicon" ng-class="{true: 'glyphicon-star-empty', false: 'glyphicon-star'}[star.empty]" ng-click="click(star.index)"></li> </ul>
Ember.js Template
<ul style="padding: 0"> {{#each star in stars}} <li style="color: #FFD700;" {{bind-attr class=":glyphicon star.empty:glyphicon-star-empty:glyphicon-star"}} {{action click star}}></li> {{/each}} </ul>
.csharpcode {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
margin: 0em
}
.csharpcode .rem {
color: #008000
}
.csharpcode .kwrd {
color: #0000ff
}
.csharpcode .str {
color: #006080
}
.csharpcode .op {
color: #0000c0
}
.csharpcode .preproc {
color: #cc6633
}
.csharpcode .asp {
background-color: #ffff00
}
.csharpcode .html {
color: #800000
}
.csharpcode .attr {
color: #ff0000
}
.csharpcode .alt {
width: 100%; margin: 0em; background-color: #f4f4f4
}
.csharpcode .lnum {
color: #606060
}
The Angular template is composed using HTML and other directives (both built in and custom). The <LI> element is decorated with three Angular directive attributes.
- ng-repeat will create a <LI> elements for n stars in the scope.
- ng-class applies the glyphicon-star-empty or the glyphicon-star class based on whether the star.emtpy scope property is true.
- ng-click wires up the click event.
I won’t review the Ember implementation since I did that in a previous post. One small detail (or big, depending on who you ask) is the Ember implementation uses handelbars.js {{#each}} control flow helper to imperatively loop though each object in stars, compared to Angular’s more declarative ngRepeat directive.
The script
Angular.js
notflixApp.directive('starRating', function(){ return { restrict: 'E', templateUrl: '/app/templates/starRating.html', link: function(scope){ scope.click = function(starRating) { scope.starRating = starRating; scope.ratingChanged({newRating: starRating}); }; scope.$watch('starRating', function(oldVal, newVal) { if (newVal) { scope.stars = []; var starRating = scope.starRating; for(var i = 0; i < scope.maxStarRating; i++){ scope.stars.push({empty:i >= starRating, index:i+1}); } } }); }, scope: { starRating: "=", maxStarRating: "=", ratingChanged: "&", } }; });
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/white-space: pre;/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/white-space: pre;/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/white-space: pre;/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
margin: 0em
}
.csharpcode .rem {
color: #008000
}
.csharpcode .kwrd {
color: #0000ff
}
.csharpcode .str {
color: #006080
}
.csharpcode .op {
color: #0000c0
}
.csharpcode .preproc {
color: #cc6633
}
.csharpcode .asp {
background-color: #ffff00
}
.csharpcode .html {
color: #800000
}
.csharpcode .attr {
color: #ff0000
}
.csharpcode .alt {
width: 100%; margin: 0em; background-color: #f4f4f4
}
.csharpcode .lnum {
color: #606060
}
Ember.js
App.StarRatingComponent = Ember.Component.extend({ maxStars: 0, starRating: 0, stars: [], actions: { click: function(star){ this.set('starRating', star.index); this.sendAction('action', star.index); } }, setRating: function() { var stars = []; var starRating = this.get('starRating'); for(var i = 0; i < this.get('maxStars'); i++){ stars.pushObject(Em.Object.create({empty:i >= starRating, index:i+1})); } this.set('stars', stars); }.observes('starRating').on('didInsertElement') });
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/white-space: pre;/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
In the Angular script, I am adding the custom directive to the notflix demo app. Let’s go through each member on the directive:
- restrict: You can restrict to [A]ttrirbute, [E]lement, CSS [C]lass, or [M] comment (I guess the C was taken). This one is restricted to an element, but attribute would also make sense.
- templateUrl: a reference to the template location. You can alternatively specify the template in line.
- link: This is the function responsible for DOM manipulation. The scope.click function will respond to the click event from the template and update the scope.starRating, and fire the rating changed event allowing the app’s controller to respond. The scope.$watch registers a listener callback to update the scope’s model.
- scope: is what the template will bind to. The = and & syntax indicates the respective property and function names in the template will match the name specified in the scope.
There is a bit more code in the Angular directive than in the Ember component. Wiring up the template is not necessary in Ember(because of the naming convention). That’s not to say one is better than the other, you could certainly make the case for being explicit. I have a slight preference for the Ember .observes(..).on(…) syntax vs. the Angular style, but I wouldn’t say it’s objectively better.
The difference in SLOC would become more negligible as the code become more complicated, since the actual custom logic would grow in lock-step between the frameworks.
Using the Custom Directive Compared to the Ember Component
Angular.js Custom Directive Usage
<div ng-repeat="movie in movies" class="movie-item"> <h1>{{movie.title}}</h1> <star-rating star-rating="movie.starRating" max-star-rating="movie.maxStarRating" rating-changed="update(newRating)"></star-rating> <small>Year Released </small><label> {{movie.releasedYear}}</label> <br/> <small>Critic Review </small><label> {{movie.review}}</label> </div>
.csharpcode {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
font-size: small; font-family: consolas, “Courier New”, courier, monospace; color: black; background-color: #ffffff
}
.csharpcode pre {
margin: 0em
}
.csharpcode .rem {
color: #008000
}
.csharpcode .kwrd {
color: #0000ff
}
.csharpcode .str {
color: #006080
}
.csharpcode .op {
color: #0000c0
}
.csharpcode .preproc {
color: #cc6633
}
.csharpcode .asp {
background-color: #ffff00
}
.csharpcode .html {
color: #800000
}
.csharpcode .attr {
color: #ff0000
}
.csharpcode .alt {
width: 100%; margin: 0em; background-color: #f4f4f4
}
.csharpcode .lnum {
color: #606060
}
Ember.js Component Usage
<div class="movie-item"> <h1>{{movie.title}}</h1> {{star-rating starRating=movie.starRating maxStars=movie.maxStarRating action="rateMovie"}} <small>Year Released </small><label> {{movie.releasedYear}}</label> <br/> <small>Critic Review </small><label> {{movie.review}}</label> </div>
The <star-rating> usage should feel pretty intuitive. rating-changed calls update on the controller.
Angular’s philosophy is to extends the HTML vocabulary. If you had no experience with HTML, you wouldn’t be able to look at the starRating usage and know whether it is an Angular directive or an HTML element (In fact, when you use the form element in an Angular application, it actually overrides the HTML form element).
In contrast, Ember relies on the {{ handelbars }} syntax to use the component. The action provides a hook into the controller if you need to respond to a user changing the star rating (just like the rating-changed=”…” in the Angular solution.
The usages definitely have more similarities than differences and I don’t have a particularly strong preference for one over the other.
Wrapping Up
This is a simple example showing only a sliver of the functionality available in each framework. Still, I am surprised by the similarity of the two solutions. The Ember team has said they are not afraid to borrow ideas from others, and the same is probably true for the Angular community. Perhaps both teams borrowed concepts from some other project. It’s also worth mentioning that the Angular Directives are definitely not a one to one with Ember Components. They can solve similar problems, but each can also solve other problems not discussed here.
The code for the Angular demo app is here, and the Ember code is here. Enjoy.