Angular JS – Making Directives self-contained in our ASP.NET MVC Twitter App

This article is sixth in the series, I strongly recommend you go through the previous five if you are new to AngularJS:

Part 1 - Hello AngularJS – Building a Tweet Reader

Part 2 - Using AngularJS Modules and Services Feature in an ASP.NET MVC application

Part 3 - AngularJS – Post data using the $resource Service in an ASP.NET MVC app

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

Part 5: Angular JS: Routing to a SPA mode using ASP.NET MVC

Today we will revisit Directives in an attempt to make them more self-contained and re-usable. As with the previous articles, we start off where we left off so please download the code of Part 5 from Github and follow along.

Revisiting Directives

In our previous Routing article, we had created a new separate Controller for our Status page called the StatusController. But unfortunately we found out that our Retweet Directive was broken in the Details page because the button click handler that was doing the actual Retweet action was in the TimelineController. This led us to abandon the StatusController and stuff everything back into TimelineController. That was an ugly hack. After all we want the Retweet functionality to be self-contained and reusable. Let’s see how we can do this.

Fortunately there is one concept about Directives that we haven’t visited yet and that is Directive specific controllers. Yup, directives can have their own controllers as well. Today we’ll see how to use this concept of modularize our Retweet button Directive.

Custom Controllers Directives

Currently our Retweet Directive is defined as follows:

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>"
};
});


It uses a local scope that is limited to the properties defined in here (that is text and clickevent properties). This scope overrides the global $scope. This is a key point to keep in mind when using Controllers for Directives.

We can update the above Directive as follows to have its own Controller

ngTwitter.directive("retweetButton", function ($http, $routeParams)
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  controller: function ($scope, $element)
  {
    // do what it takes to retweet
  },
  template: "<button class='btn btn-mini' ng-transclude><i class='icon-retweet'></i></button>"
};
});

Notice a few significant things:

1. We have let go of the custom scope, because we want access to the $scope.

2. As a result of the above, we have given up on the {{ text }} template in our HTML template for the button.

3. We’ve also removed the ng-click attribute from the button template and put the ng-transclude attribute back.

4. In the controller function, we have $element which is an instance of the button from the template.

Moving the Retweet functionality into the Directive’s Controller

Well we’ll need to move the Retweet functionality from the TimelineController into the retweetButton directive’s own controller. We add the highlighted code below to our Directive.

ngTwitter.directive("retweetButton", function ($http, $routeParams)
{
return {
  restrict: "E",
  replace: true,
  transclude: true,
  controller: function ($scope, $element)
  {
   $element.on("click", function ()
   {
    var resultPromise = $http.post("/Home/Retweet/", $scope.status);
    resultPromise.success(function (data)
    {
     if (data.success)
     {
       alert("Retweeted successfully");
     }
     else
     {
      alert("ERROR: Retweeted failed! " + data.errorMessage);
     }
    });
   });
  },
  template: "<button class='btn btn-mini' ng-transclude><i class='icon-retweet'></i></button>"
};
});

Let’s see what this does line by line:

1. We assign a click event handler to the $element which is essentially the button defined in the template.

2. When the click event fires, we use the $http object to do an http post and pass it the $scope.status object to this.

3. What does this $scope.status contain? Well that depends on the controller. As things are defined now, if we are in the Status Controller, it provides us with the status object that is the current tweet as we can see from the controller below.

ngTwitter.controller("StatusController", function ($scope, $http, $routeParams,
TwitterService)
{
var resultPromise = $http.get("/Home/Status/" + $routeParams.id);
resultPromise.success(function (data)
{
  $scope.status = data;
});
});


4. However if we are in the TimelineController, this will return a undefined because there is no status object in the TimelineController. Instead we have the tweets object. When looping through the tweet object we use the following markup and template in the index.cshtml

<tr ng-repeat="item in tweets">
     <td>
      <img src="{{item.ImageUrl}}" />
     </td>
     <td>
      <div>
       <strong>{{item.ScreenName}}</strong>
       <br />
       {{item.Tweet}}
      </div>
      <div>
       <retweet-button text="Retweet">Retweet</retweet-button>
       <a class="btn btn-primary btn-mini" href="#/status/{{status.Id}}">Details</a>
      </div>
     </td>
    </tr>


Note we are using the ‘item’ instance from the tweets collection. If we rename the item instance to status, we are done and the $scope.status in the Directive’s controller will be valid.

5. Once we have the $scope.status object sorted out, rest of the code is about handling the return value from the server and displaying a success or failure notice.

We can now remove the $scope.retweet function from the TimelineController.

If we run the application now, we’ll see that the ‘Retweet’ text has gone missing. This is because we are now transcluding text from the Directive markup into the Template. So we’ll have to update the Directive markup as follows in two places (the timeline and status templates)

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

With this change done, we are good to go.

AngularJS App Demo Time

Run the application and put a breakpoint in the click event handler of the directive’s controller.

timeline-page-angular

Click on Retweet for something you would like to share and it should use the new Directive Controller’s click event handler

angularjs-directive-controller

Now navigate to the Details page of a Tweet

details-page

Click retweet, you should see the same breakpoint being hit again. Voila!

directive-controller-from-details-page

That’s a wrap for the day.

Conclusion

We were able to modularize our Retweet Button Directive and make it self-contained using its own controller. You may argue that now we can’t attach a different click handler or that the click handler is now a part of the directive. Given the scope of our directive, being able to Retweet a given tweet from anywhere I say this is an acceptable design choice. It’s like customizing a control to work with your own data set but the control will be used at multiple places in the same project (and thus has access to this custom dataset).

Key takeaway however is that we can have isolated controllers for our custom directives for common tasks that the Directive may need to either exchange data or render itself.

Download the entire source code of this article (Github)




1 comment:

Nikhil @ MobileJury.com said...

These codes are very much helpful to create something awesome as a Twitter App. Thanks for sharing! :-)