Using Custom Directive in AngularJS to create reusable JavaScript components for your ASP.NET MVC app

In our previous three articles on Angular JS, we have seen how we could get started by building a small Twitter Client. We have explored the view model, Modules and Services in Angular JS and in our last article saw how to post data using the $resource Service.

Today we’ll see how to use a feature called Directives in Angular JS. Directives allow you to encapsulate custom behavior in an HTML element, attribute, classes and even comments. For example, the ng-app attribute that we use to define the scope of our Angular App is in fact a Directive, because there are no HTML5 attributes by that name! It’s Angular who interprets the attribute at runtime.

Apart from helping add custom attributes, directives can also be used to create the server side equivalent of ‘tag-libraries’ on the client. Those familiar with WebForms or JSP development will remember you could create server side components with custom Tags like <asp:GridView>…</asp:GridView> where the GridView rendering logic was encapsulated in a server component. Well, Directives allow you build such components, but on the client.

In fact today we’ll define a ‘Retweet’ button on our Tweet Reader that will enable us to encapsulate the function in a custom directive.

Doing things the ‘old-fashioned way’ first

Before we get started, let’s download the code from previous article so we can build on it. You can download the zip from here or fork it on Github. Once you’ve got the code you’ll need to put in your Twitter Consumer Key and Consumer Secret in the web.config’s twitterConsumerKey and twitterConsumerSecret appSetting Keys. With the keys setup you are ready to roll and at this point can run the app to check previous behavior.

Adding the Retweet Functionality

Adding a Retweet button is rather simple to do. All you have to do is update the markup and hook it up to a function call JavaScript. We could then use the $http resource and Post it to a Retweet action method.

The markup for this would be as follows, the new bits are highlighted.

<table class="table table-striped">
<tr ng-repeat="item in tweets">
  <td>
   <img src="{{item.ImageUrl}}" />
  </td>
  <td>
   <div>
    <strong>{{item.ScreenName}}</strong>
    <br />
    {{item.Tweet}}
   </div>
   <div>
    <button class="btn btn-mini" ng-click="retweet(item)"><i class="icon-retweet"></i> Retweet</button>
   </div>

  </td>
</tr>
</table>


Model and Controller changes

LinqToTwitter’s Retweet API uses the Status ID that Twitter generated to do the Retweet. We have not saved StatusId in our TweetViewModel.cs so first we’ll update that:

public class TweetViewModel
{
public string ImageUrl { get; set; }
public string ScreenName { get; set; }
public string MediaUrl { get; set; }
public string Tweet { get; set; }
 public string Id { get; set; }
}

Next we update the Tweet() HttpGet function in the Controller to save the Id in the Json that will be sent over to the client

friendTweets =
(from tweet in twitterCtx.Status
  where tweet.Type == StatusType.Home &&
   tweet.ScreenName == screenName &&
   tweet.IncludeEntities == true
  select new TweetViewModel
  {
   ImageUrl = tweet.User.ProfileImageUrl,
   ScreenName = tweet.User.Identifier.ScreenName,
   MediaUrl = GetTweetMediaUrl(tweet),
   Tweet = tweet.Text,
   Id = tweet.StatusID
  })
  .ToList();

Finally we add the Retweet Action method

[HttpPost]
public JsonResult Retweet(string id)
{
Authorize();
twitterCtx = new TwitterContext(auth);
try
{
  Status stat = twitterCtx.Retweet(id);
  if (stat != null)
  {
   return Json(new { success = true });
  }
  else
  {
   return Json(new { success = false, errorMessage = "Unknown Error" });
  }
}
catch (Exception ex)
{
  return Json(new { success = false, errorMessage = ex.Message });
}
}

Well if we run the application now things will just work fine and Retweet will work. This was doing things the HTML/JavaScript way. Time to get Directives into the picture.

Creating your own Directive

The idea behind creating a directive is to encapsulate the ‘Retweet’ functionality on the client side. For this example this may seem a little counterproductive because the ‘Retweet’ functionality is rather simple and re-using it, involves only a bit of markup copy-paste.

But imagine for more involved UI Component like say a Tabbed UI functionality. Having a compact directive that renders the tabbed interface would really be a welcome relief.

In our case we’ll simply create a new element called <retweet-button>…</retweet-button>.

Defining the Directive

To do this we first copy the current markup from the Index.cshtml and replace it with the following:

<retweet-button></retweet-button>

Next we simply add the following snippet to our hello-angular.js file

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict : "E",
  template : "<button class='btn btn-mini' ng-click='retweet(item)'><i class='icon-retweet'></i> Retweet</button>"
}
});


There are a few key things to note in the above snippet and how it ‘links’ to the custom element we used in the html markup.

1. The ngTwitter.directive method’s first parameter is the name of your directive. Note the naming convention, the ‘-‘ in retweet-button was removed and the next character ‘b’ was made upper case (camel case), the markup attribute ‘retweet-button’ became the directive name ‘retweetButton’. Remember this convention else your directive will not be hooked up correctly and you’ll end up with invalid HTML.

2. Next, the directive method takes an anonymous function as parameter which returns an object. In our case the object has two properties

a. The ‘restrict’ property tells Angular what type of directive it is. In our case it’s an element, hence the “E”. If you omit it, default is attribute and things will break.

b. The ‘template’ property contains a piece of the DOM (not string). As we can see we have simply copy pasted the button element from our cshtml file. This works, but it has the action method, the text both hard coded and as we know hard-coding makes components ‘brittle’ and less extensible.

Before we go further you can run the application at this point and things will still work.

‘Transclusion’ and generalizing the Button’s label

First thing we want to do is to be able to have Text that we want, instead of hard-coding it in our Directive. To do this we can add the text in our markup as follows

<retweet-button>RT</retweet-button>

Then we’ll add a ‘transclude’ property in our directive’s return object and set it to true. Finally we’ll add the ng-transclude attribute in the template as shown below.

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  transclude: true,
  template : "<button class='btn btn-mini' ng-click='retweet(item)' ng-transclude><i class='icon-retweet'></i> </button>"
}
});


Now if we run the application we’ll see that the text in our Markup is coming through.

transclude-works

Before we go further, it’s worth noting the HTML generated by Angular

replace-attribute

If we use our browser tools we’ll see that the actual button has been placed inside the <retweet-button> element. This is a little icky and can be easily fixed by using the ‘replace’ property in our directive’s return object.

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  template : "<button class='btn btn-mini' ng-click='retweet(item)' ng-transclude><i class='icon-retweet'></i> </button>"
}
});


This tells angular to replace the directive with the Template.

Now that we have generalized the text, what if we wanted to pass in the action method name too? Transcluding doesn’t help there.

Using ‘Link Function’ in our Directive

As mentioned earlier, hardcoding the click event to an action method reduces the modularity and makes our ‘component’ brittle. One way to ‘not’ hardcode is to use the Linking function while creating our Directive. The linking function is define via the Link attribute.

We update our directive definition as follows:

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  template: "<button class='btn btn-mini' ng-transclude><i class='icon-retweet'></i> </button>",
  link: function (scope, el, atts)
  {
   el.on("click", function ()
   {
    scope.retweet(scope.item);
   });
  }
}
});


If we run the app now we’ll still get routed to the retweet method. How is this working? Well the scope parameter of the input function passes the current $scope instance, the el parameter passes a jquery object of the html element and atts element contains an array of attributes that we may have added to our directive markup. Currently we haven’t used the atts we’ll see it shortly. However the el element is a disaster waiting to happen. Given a jQuery object we can simply go back to doing things the jQuery way like injecting the template etc. etc. In this case we’ve attached the click handler.
Well this works, but we can better this too and ‘angularize’ it further.

The ‘scope’ Definition for a Directive

We can add another property in the return object for Directive definition. This is the scope property. It is different from $scope as in, scope simply helps configure shortcuts that can be used in our template. For example, remember Angular uses the {{ … }} syntax for templating fields. So instead of using transclude we can use the {{ … }} expression. But what will be the template property to bind to? We can define it in scope as follows (don’t forget to remove the transclude: true property and the ng-transclude attribute.

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  replace: true,
  scope: {
   text: "@"
  },
  template: "<button class='btn btn-mini'><i class='icon-retweet'></i> {{ text }} </button>"
}
});

So what’s happening here? We’ve defined a scope property that has an object with the property text whose value is set to @. This is actually a ‘shorthand’ telling Angular to pick up the ‘text’ attribute from the markup’s list of attributes. Next, we have used the {{ text }} templating notation to tell angular that it should replace the value passed in via the scope->text in the template. The corresponding change in the markup is as follows:

<retweet-button text="Retweet"></retweet-button>

Okay, so this generalized the text in yet another way. How do we generalize the function call? Well we’ll use another shortcut in the scope for that. We update the directive script as follows first:

ngTwitter.directive("retweetButton", function ()
{
return {
  restrict: "E",
  replace: true,
  scope: {
   text: "@",
   clickevent: "&"
  },
  template: "<button class='btn btn-mini' ng-click='clickevent()'><i class='icon-retweet'></i> {{text}}</button>"
};
});


Yes, we have used another magic symbol & for getting hold of a ‘function pointer’ called clickevent. In our template we are invoking the function that this pointer is pointing to. But where did we assign the function? In our view of course

<retweet-button text="Retweet" clickevent="retweet(item)"></retweet-button>

Finally we have extracted the action method we are supposed to call on $scope out from the directive into the markup making the directive pretty generic.

In hindsight, it seems like a lot of work to reduce

<button class="btn btn-mini" ng-click="retweet(item)"><i class="icon-retweet"></i> Retweet</button>
TO
<retweet-button text="Retweet" clickevent="retweet(item)"></retweet-button>

But as a I said, this is a simple example of simply encapsulating the functionality of button. We could potentially encapsulate the functionality of, say sending a tweet or rendering a timeline etc. using the same directives functionality.

Checkout the Tab component in AngularJS’ documentation (scroll all the way down) for a more complex example.

angular-tab-component

Conclusion

We learnt today that Directives in Angular JS kind of teaches HTML to do new tricks. We were able to define a custom tag that did a custom action and ended up with a component that could be reused at other places in the App.

With that we wrap this episode of Angular JS, maybe we can do a more complex directive next time or maybe learn some more new Angular JS tricks J, so stick around!

Download the entire source code of this article (Github)

No comments:

Post a Comment