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

In the beginning of the series I had mentioned that Single Page applications (SPA) were gaining popularity and that some of the ready SPA templates that come with ASP.NET MVC are rather opinionated, giving them a steep learning curve. We started with basics of Angular JS and slowly inched up learning bits of the framework. Till the third article we were mostly doing known things (data binding, templating, posting data) that other drop in libraries could do as well). The fourth article showed us how to do UI composition in form of directives and the seeds of a Single Page App were sown.

Today, we will look at routing (on the client side) and take the next step in realizing how a full blown Single Page Application framework functions.

Routing in AngularJS

If you are familiar with MVC routing you are wondering, err… routing is a server side thing, how does it matter to a client side framework. Well you are not alone, even I had the same thoughts when I heard of Routing in JS frameworks. But as you will see, routing is real and it works rather nicely on Angular JS.

Getting started

As with the previous articles, we’ll start with the code we ended with in the last article. You can download the zip from here, or fork it on Github or clone directly if you want. Once you have downloaded the code, you are good to go as all the dependencies are already setup for you.

Breaking the functionality up into ‘Views’

The current application consists of one view with a single ng-controller. As a next step, we would like to implement a details view of the Tweet that will show us more information and if there are any embedded images, show the image as well.

Introducing the Route Provider $routeProvider

Before we introduce the status view, let’s tell Angular that we want to use the Route Provider. To do this we will take the ngTwitter application variable (in angular hello-angular.js) and add a delegate to the config function on it as follows:

// Create ngTwitter Module (roughly Module = namespace in C#)
var ngTwitter = angular.module("ngTwitter", ['ngResource']);
ngTwitter.config(function ($routeProvider)
{
$routeProvider.when(
  "/", {
  templateUrl: "timeline"
});
});

Let’s quickly review what we did here:

1. We added a delegate to the ngTwitter app and requested for the $routeProvider in the delegate.

2. Next we told the routeProvider that when the URL is ‘/’ that is the root URL, it should use a template called ‘timeline’.

Now where is the ‘timeline’ template? We don’t have it yet.

Defining the Template

To define the template, we’ll wrap the existing markup inside our ng-controller with a <script> tag. There are two things special about the script tag.

1. Type is set to type=”text/ng-template”

2. The id is set to the templateUrl value we set in our route

<div ng-app="ngTwitter">
<div ng-controller="TimelineController">
  <script type="text/ng-template" id="timeline">
<!-- the existing markup -- >

  </script>
</div>
<ng-view></ng-view>
</div>

3. Final thing we added was the <ng-view> element. This is where the Template is placed when the route match happens.

If we run the app now, we’ll see that our Twitter client brings up the last 20 tweets with the Retweet button and text area to send new Tweets. So we have silently introduced routing in our existing Application.

Note: I have been holding off on clearing the query string of values returned by Twitter for a while now. Linq2Twitter saves the info in a cookie for us already, so we don’t need to keep it around. I hacked around it by checking for Request.QueryString contents and re-directing to Index page if Query Strings are present after authentication. So the Index action method looks like the following, the new code is highlighted.

public ActionResult Index()
{
var unAuthorized = Authorize();
if (unAuthorized == null)
{
  if (Request.QueryString.Count > 0)
  {
   return RedirectToAction("Index");
  }

  return View("Index");
}
else
{
  return unAuthorized;
}
}

Removing Controller reference from the View to Route

In the markup above, we have a container that specifies the controller name for the Template. This hard-codes the Controller to the View. We can move it to the Route mechanism by updating the Route configuration as follows:

$routeProvider.when(
"/", {
  templateUrl: "timeline",
  controller: "TimelineController"
});


In the view, we simply remove the <div> that had the ng-controller attribute specified.

<div ng-app="ngTwitter">
<script type="text/ng-template" id="timeline">
  <!-- the existing markup -- >
  …
</script>
<ng-view></ng-view>
</div>

Adding a new Route,ng-Controller and a new View

Adding a new Route is easy, because the $routeProvider’s when method returns a $routeProvider so we can simply chain another when like this

$routeProvider.when(
"/", {
  templateUrl: "timeline",
  controller: "TimelineController"
}).when(
  "/status/:id", {
  templateUrl: "status",
  controller: "StatusController"
});


What the new route is telling us is, the path /status comes with a parameter called id and the template to use is name ‘status’ and it should use the StatusController to get relevant details.

Next in the Index.cshtml, we’ll add an anchor tag and style it like a bootstrap button. We’ll also set the href to point to the status page and pass the status ID to it. This is accomplished with the following markup:

<a class="btn btn-primary btn-mini" href="#/status/{{item.Id}}">Details</a>

Note the #/ notation for URL. This is a part of HTML spec where URL starting with # doesn’t cause a postback, instead the browser looks for the anchor in the same page. Running the App will give us a result as shown below.

adding-details-button

Now if you click on “Details”, even though we haven’t defined the view, the app will navigate to the new URL. Note, it does not do a post back, simply navigates.

routing-to-empty-status

Setting up the new ng-Controller

Now let’s setup the StatusController in the hello-angular.js file.

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

As we can see in the delegate, we have a new parameter getting injected called $routeParams. Thanks to our Route definition which specified the parameter as id and the URL in the View that sets up the id value, Angular sets up $routeParams as follows:

{ id: 1234567898 }

In the controller, we setup a status object to be used as the view Model in $scope. Value of $scope.status is populated by our Server’s Status action method. We are revisiting $http service here so we have to use promise to wait for Success callback to be called before we can set the value to $scope.status.

Adding new action method in HomeController to get Status from Twitter

In the HomeController, we’ll add a new Status Action method. But before we do that, we’ll add a couple of properties to our TweetViewModel class. We’ll add FavoritedCount, RetweetedCount and HasMedia (a Boolean).

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; }
public string FavoriteCount { get; set; }
public string RetweetCount { get; set; }
public bool HasMedia { get; set; }

}

Next we refactor the translation of Status object into TweetViewModel from the Linq query to a helper method GetTweetViewModel

private TweetViewModel GetTweetViewModel(Status tweet)
{
var tvm = new TweetViewModel
{
  ImageUrl = tweet.User.ProfileImageUrl,
  ScreenName = tweet.User.Identifier.ScreenName,
  MediaUrl = GetTweetMediaUrl(tweet),
  Tweet = tweet.Text,
  Id = tweet.StatusID,
  FavoriteCount = tweet.FavoriteCount.ToString(),
  RetweetCount = tweet.RetweetCount.ToString(),
};
tvm.HasMedia = !string.IsNullOrEmpty(tvm.MediaUrl);
return tvm;
}


Finally we add the Status action method to call Twitter using Linq2Twitter

[HttpGet]
public JsonResult Status(string id)
{
Authorize();
string screenName = ViewBag.User;
IEnumerable<TweetViewModel> friendTweets = new List<TweetViewModel>();     


if (string.IsNullOrEmpty(screenName))
{
  return Json(friendTweets, JsonRequestBehavior.AllowGet);
}

twitterCtx = new TwitterContext(auth);
friendTweets =
  (from tweet in twitterCtx.Status
   where tweet.Type == StatusType.Show &&
    tweet.ID == id
   select GetTweetViewModel(tweet))
    .ToList();
   if (friendTweets.Count() > 0)
    return Json(friendTweets.ElementAt(0), JsonRequestBehavior.AllowGet);
   else
    return Json(new TweetViewModel { Tweet = "Requested Status Not Found" },
     JsonRequestBehavior.AllowGet);
}

Adding the ‘status’ View

In the Index.cshtml we add to following markup that will constitute the Status view.

<script type="text/ng-template" id="status">
<table class="table table-striped">
  <tr>
   <td>
    <img src="{{status.ImageUrl}}" />
   </td>
   <td>
    <div>
     <strong>{{status.ScreenName}}</strong>
     <br />
     {{status.Tweet}}
    </div>
   </td>
  </tr>
  <tr>
   <td>
    <div>
     <retweet-button text="Retweet" clickevent="retweet(status)"></retweet-button>
    </div>
   </td>
   <td>
    <span class="label">Retweeted:</span> <span class="badge badge-success">{{status.RetweetCount}}</span>
    <span class="label">Favorited:</span> <span class="badge badge-warning">{{status.FavoriteCount}}</span>
   </td>
  </tr>
</table>
<div ng-show="status.HasMedia">
  <img src="{{status.MediaUrl}}" />
</div>
</script>

Most of the markup is easy to understand. We are using the typ=”text/ng-template” directive to declare this snippet as a template and tying it up with our route using the id=status.

If you remember, in the Client Side controller, we had added the data to $scope.status hence status is the name of our view model object, so while binding values, we use status.*

Towards the end, we have a div with an ng-show directive with the value set to ‘status.HasMedia’.

This is a directive we are using to dynamically show/hide any attached image that a tweet may have.

Point to note is that value of ng-show is not escaped using {{ … }} impliying ng-show needs the binding expression from which to get the value and not the value itself.

All done.

Run the application now and click on the details button to navigate to the status page. As we can see below we have a rather amusing image tweeted by the user Fascinatingpics.

status-page

Broken Retweet functionality

The hawk-eyed will note that the Retweet functionality is broken. This is because, while trying to show how to use a second controller, I left out the retweet method in our TimelineController. Actually the status functionality doesn’t need a separate controller of its own. To fix this we do the following:

1. Update status route to point to TimelineController

"/status/:id", {
    templateUrl: "status",
    controller: "TimelineController"
})


2. Update our TwitterService with a new status function, this will also require us to use the $http service that we’ll simply request Angular to inject. The status function will do the $http.get to retrieve the Status.

ngTwitter.factory("TwitterService", function ($resource, $http)
{
return {
  timeline: $resource("/Home/Tweet"),
  status: function (id)
  {
   return $http.get("/Home/Status/" + id);
  }

}
});


3. Next in the TimelineController, we’ll request for the $routeParams service and put an if condition to check whether $routeParams has the id property and if so, calls the status method on the service and waits for the promise to return successfully.

ngTwitter.controller("TimelineController", function ($scope, $http, $routeParams, TwitterService)
{
if ($routeParams.id)
{
  var statusPromise = TwitterService.status($routeParams.id);
  statusPromise.success(function (data)
  {
   $scope.status = data;
  });
}
else
{
  $scope.tweets = TwitterService.timeline.query({});
}
// rest of the code remains the same

}


That pretty much covers it and now our status view also uses the TimelineController. Thus now the retweet function will work perfectly!

retweet-working-angularjs
Super sweet, we just verified a use-case for custom directives! We added the Retweet button in a new view and things just worked!

That’s a wrap for the day.

Conclusion

With that we conclude today’s post. If we think back, we’ll realize that our app is slowly but sure turning into a nice, on browser, Single Paged Application. Now the SPA templates in ASP.NET MVC will begin to make a little more sense. If not, don’t worry we’ll continue towards generalizing our application to ultimately ending up with an clear understanding of SPAs and ASP.NET MVC SPA templates.

For today, we conclude with the knowledge that we have tried out routing, multiple controllers and reused a custom directive created previously in AngularJS.

Download the entire source code of this article (Github)




No comments: