JSON with AngularJS

By Mike Gledhill

Pages:

Our next task is to see how easy it is to get our JSON Serices working with AngularJS.
Our example will use JSON data from the following two URLs:

http://www.iNorthwind.com/Service1.svc/getAllCustomers
http://www.iNorthwind.com/Service1.svc/getBasketsForCustomer/ANATR

So, before we start looking at some code, here's a live demonstration of what the final result will look like.
Try choosing a different customer from the following list, and marvel at how smoothly that customer's list of orders gets displayed.

Please select a customer:
ID of selected customer: {{selectedCustomer}}

This customer has {{listOfOrders.length}} orders.
  • Order # {{order.OrderID}} ( {{order.OrderDate}} )
    • {{product.Quantity}} x {{product.ProductName}} @ {{product.UnitPrice | currency}} = {{product.Quantity * product.UnitPrice | currency}}
{{errorMessage}}

The good news is that you can achieve this with very little code.
The bad news is that AngularJS introduces a few issues and problems you need to get around.

Disclaimer

Remember, this is a JSON tutorial, and in the same way as its not meant to teach you how to write an iPhone app, I'm also not about to teach you AngularJS from scratch. There are already plenty of excellent tutorials and books out there.

What I will do is show you an example of how easily AngularJS can consume and display data from our JSON Web Service.

By the way, this is quite a "tame" simple first example of using AngularJS.
In a later tutorial, we'll use exactly the same JSON services, but get AngularJS to make our data look really cool, showing it as a Master-Detail view.

So, if you want to skip the basics (or you find this tutorial too basic), go and have a read here !

AngularJS working with 'GET' web services

The first problem we'll hit is that AngularJS tends to call web services using OPTION, rather than a GET or POST.
If we don't fix this issue, our web services will receive such an 'OPTION' web request, and have no idea how to handle it.

Here's how to fix this issue. First, we must tell our web page that we'll be using AngularJS, with a particular app:

<html ng-app='myApp'>

And in our AngularJS JavaScript file:

// Make sure AngularJS calls our WCF Service as a "GET", rather than as an "OPTION"
var myApp = angular.module('myApp', []);
myApp.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.defaults.useXDomain = true;
    delete $httpProvider.defaults.headers.common['X-Requested-With'];
}]);

This is a vital piece of code to get Angular working with our services, and full credit for this gem goes to these sites:
http://better-inter.net/enabling-cors-in-angular-js/
http://stackoverflow.com/questions/12312659/how-to-prevent-angular-js-http-object-from-sending-x-requested-with-header


Next, we also need to add a few lines to our REST Web Service's web.config file:

<configuration>
  ...
  <system.webServer>
    <httpErrors errorMode="Detailed"/>
    <validation validateIntegratedModeConfiguration="false"/>
    <!-- We need the following 6 lines, to let AngularJS call our REST web services -->
    <httpProtocol>
        <customHeaders>
            <add name="Access-Control-Allow-Origin" value="*"/>
            <add name="Access-Control-Allow-Headers" value="Content-Type"/>
        </customHeaders>
    </httpProtocol>

  </system.webServer>
  ...
</configuration>

If you don't make this change to your web.config file, then your web services will continue to work beautifully when you call them from your browser, but will throw all kinds of "Access-Control-Allow-Origin" errors when you try to call them from your AngularJS code.

Trust me, I've just saved you a day of desperate Googling.
Don't forget to email me a beer.

With these two fixes in place, we're ready to write an Angular controller file to load in our web service data, then get our HTML page to display it.

The AngularJS 'Controller' file

Next, we need a Controller file for our AngularJS code.
I'll explain the two parts of this JavaScript in a minute, but for now, here's the code:

myApp.controller('MasterDetailCtrl',
  function ($scope, $http) {

    // When the user selects a Customer from the DropDownList, the following variable will get set.
    $scope.selectedCustomer = null;

    // And we'll get Angular to populate this Customer's list of Orders (and the Products in them) into this variable
    $scope.listOfOrders = null;

    // When we first load our Controller, we'll call our first web service, which fetchs a list of all Customer records.
    $http.get('http://www.iNorthwind.com/Service1.svc/getAllCustomers')
        .success(function (data) {
            $scope.listOfCustomers = data.GetAllCustomersResult;

            if ($scope.listOfCustomers.length > 0) {
                // If we managed to load more than one Customer record, then select the first record by default.
                // This line of code also prevents AngularJS from adding a "blank" <option> record in our drop down list
                // (to cater for the blank value it'd find in the "selectedCustomer" variable)

                $scope.selectedCustomer = $scope.listOfCustomers[0].CustomerID;

                // Load the list of Orders, and their Products, that this Customer has ever made.
                $scope.loadOrders();
            }
        })
        .error(function (data, status, headers, config) {
            $scope.errorMessage = "Couldn't load the list of customers, error # " + status;
        });

     // When the user selects a new Customer from our list, we'll call this function, which calls our second web service,
    // which loads the list of Orders that this Customer has made.

    $scope.loadOrders = function () {
        $http.get('http://www.iNorthwind.com/Service1.svc/getBasketsForCustomer/' + $scope.selectedCustomer)
                .success(function (data) {
                    $scope.listOfOrders = data.GetBasketsForCustomerResult;
                })
                .error(function (data, status, headers, config) {
                    $scope.errorMessage = "Couldn't load the list of Orders, error # " + status;
                });
    }
});

You can view/download the entire AngularJS code by clicking here:

MasterDetailCtrl

AngularJS in our HTML file

And here's what we need to add to our webpage.
This is copy of the HTML/Angular used in the live demonstration above.

<span style="width: 200px">Please select a customer:</span>
<select ng-model="selectedCustomer" ng-change="loadOrders();" ng-options="customer.CustomerID as customer.CompanyName for customer in listOfCustomers" style="width:350px;"></select>
<br />
<span style="width: 200px">ID of selected customer: </span>
<span style="width:80px;border:1px solid #B0B0B0;">{{selectedCustomer}}</span>
<br />
<br />
This customer has {{listOfOrders.length}} orders.
<br />
<ul class="cssListOfOrders">
    <li ng-repeat='order in listOfOrders'>
        <span class="cssOrderHeader">Order # {{order.OrderID}} ( {{order.OrderDate}} )</span>
        <ul>
                <li ng-repeat='product in order.ProductsInBasket'>
                    <span style="width:25px"> {{product.Quantity}}</span>
                    <span style="width:15px">x</span>
                    <span style="width:230px"> {{product.ProductName}} </span>
                    <span style="width:30px">@</span>
                    <span style="width:60px"> {{product.UnitPrice | currency}} </span>
                    <span style="width:30px">=</span>
                    <span style="width:100px"> {{product.Quantity * product.UnitPrice | currency}} </span>

                </li>
        </ul>
    </li>
</ul>
<span style="color:Red;">{{errorMessage}}</span>

How it works

As I said earlier, this example uses two WCF Web Services which return JSON results.
 

1. Loading the list of all customers

Our first WCF Web Service, which uses the following URL, simply returns a list of all Customer records:

http://www.iNorthwind.com/Service1.svc/getAllCustomers

The JSON data which gets returned looks like this:

  { "GetAllCustomersResult": [
     {
        City: "Berlin",
        CompanyName: "Alfreds Futterkiste",
        CustomerID: "ALFKI"
     },
     {
        City: "México D.F.",
        CompanyName: "Ana Trujillo Emparedados y helados",
        CustomerID: "ANATR"
     },

In our AngularJS controller file, once this JSON has been loaded, we set our listOfCustomers variable to contain the array of customer records.

But notice that our JSON results (shown above) actually only contains one root element, "GetAllCustomersResult", and it's this that contains our array, so we need to set listOfCustomers to the GetAllCustomersResult element:

    $http.get('http://www.iNorthwind.com/Service1.svc/getAllCustomers')
        .success(function (data) {
            $scope.listOfCustomers = data.GetAllCustomersResult;

Now, at this point, if we were just using JQuery, we would need to write some JavaScript to iterate through our JSON records, and create one <option> for each of them:

$.each(data.GetBasketsForCustomerResult, function () {
    $("#listOfCustomers").append($("<option />").val(this.CustomerID).text(this.CompanyName));
});

But with AngularJS, we just need to describe what to do with any data it finds in the $scope.listOfCustomers variable:

<select ng-model="selectedCustomer"
    ng-change="loadOrders();"
    ng-options="customer.CustomerID as customer.CompanyName for customer in listOfCustomers"
    style="width:350px;">
</select>

There's actually a lot going on in the ng-options part of this select statement.

ng-options="customer.CustomerID as customer.CompanyName for customer in listOfCustomers"

You could read like this:

foreach (Customer in listOfCustomers)
{
    // Create a new <option /> DOM element, with Customer.CompanyName as the text
    // When the user chooses a particular <option>,
    //     - set the selectedCustomer variable to contain the Customer.CustomerID value.
    //     - call the loadOrders(); function.

}

Phew !

If you wanted to use more than just the CustomerID value when the user selected an option, you could decide to set the selectedCustomer variable to the entire Customer record, then use the parts you wanted.

For example, we could get our webpage to display the Customer's ID and City values by changing the <select> to look like this:

<span style="width: 200px">Please select a customer:</span>
<select ng-model="selectedCustomer" ng-change="loadOrders();" ng-options="customer as customer.CompanyName for customer in listOfCustomers" style="width:350px;"></select>
<br />
<span style="width: 200px">ID of selected customer: </span>
<span style="width:80px;border:1px solid #B0B0B0;">{{selectedCustomer.CustomerID}}</span>
<br />
<span style="width: 200px">City of selected customer: </span>
<span style="width:200px;border:1px solid #B0B0B0;">{{selectedCustomer.City}}</span>

Which would give us HTML like this:

 

2. Loading the Orders for one customer

Next, we call our WCF Web Service to load the list of Order records for one of our customers.
And each Order might have several Product records in it.

A typical URL would look like this:

http://www.iNorthwind.com/Service1.svc/getBasketsForCustomer/ANATR

And here's what the JSON (for just one Order) might look like:

{
    "GetBasketsForCustomerResult" : [
    {
        "OrderDate":"9\/18\/1996",
        "OrderID":10308,
        "ProductsInBasket":
        [
            {
                "ProductID":69,
                "ProductName":"Gudbrandsdalsost",
                "Quantity":1,
                "UnitPrice":36.0000
            },
            {
                "ProductID":70,
                "ProductName":"Outback Lager",
                "Quantity":5,
                "UnitPrice":15.0000
            }
        ]
    }

Here's the code we need in our AngularJS controller file:

$scope.loadOrders = function () {
  // The user has selected a Customer from our Drop Down List. Let's load this Customer's records.
  $http.get('http://inorthwind.azurewebsites.net/Service1.svc/getBasketsForCustomer/' + $scope.selectedCustomer)
      .success(function (data) {
           $scope.listOfOrders = data.GetBasketsForCustomerResult;
      })
      .error(function (data, status, headers, config) {
           $scope.errorMessage = "Couldn't load the list of Orders, error # " + status;
      });
}

Blimey.
Not much to see there, is there ?

All it does is call our web service, and sets one variable.

$scope.listOfOrders = data.GetBasketsForCustomerResult;

The magic happens in our HTML, which binds to various parts of this listOfOrders variable, and creates a friendly looking list of Products in each of the Orders.

In particular, notice how easily we can iterate through the listOfOrders.ProductsInBasket array.

<span style="width: 200px">Please select a customer:</span>
<select ng-model="selectedCustomer" ng-change="loadOrders();" ng-options="customer.CustomerID as customer.CompanyName for customer in listOfCustomers" style="width:350px;"></select>
<br />
<span style="width: 200px">ID of selected customer: </span>
<span style="width:80px;border:1px solid #B0B0B0;">{{selectedCustomer}}</span>
<br />
<br />
This customer has {{listOfOrders.length}} orders.
<br />
<ul class="cssListOfOrders">
    <li ng-repeat='order in listOfOrders'>
        <span class="cssOrderHeader">Order # {{order.OrderID}} ( {{order.OrderDate}} )</span>
        <ul>
                <li ng-repeat='product in order.ProductsInBasket'>
                    <span style="width:25px"> {{product.Quantity}}</span>
                    <span style="width:15px">x</span>
                    <span style="width:230px"> {{product.ProductName}} </span>
                    <span style="width:30px">@</span>
                    <span style="width:60px"> {{product.UnitPrice | currency}} </span>
                    <span style="width:30px">=</span>
                    <span style="width:100px"> {{product.Quantity * product.UnitPrice | currency}} </span>

                </li>
        </ul>
    </li>
</ul>
<span style="color:Red;">{{errorMessage}}</span>

Wow. That's pretty cool.

Troubleshooting

One issue you might get with AngularJS (and JavaScript itself) is that sometimes you might load some JSON from your web service, but nothing appears on your page. What can you do ?

Here's what I'd recommend when this happens:

1. Run your website in Google Chrome, hit F12 to open the Developer Panel, and refresh your webpage.
2. Select the Console tab, and check for JavaScript errors. This is the most common reason why your JavaScript/AngularJS might've stopped.
3. If there weren't any errors, select the Network tab, refresh your page again, check that your webservice was called, and click on the URL to view the data which was returned from it.


4. If your web service was called successfully and did return some data, then perhaps you have a binding problem.
To check this, find out which variable you're attempting to bind to (which is listOfCustomers in the example below)...

$http.get('http://www.iNorthwind.com/Service1.svc/getAllCustomers')
    .success(function (data) {
        $scope.listOfCustomers = data.GetAllCustomersResult;

.. and simply add one line of code to your webpage (inside whichever DOM element which you've assigned ng-controller to) to display the contents of this variable:
<pre>{{ listOfCustomers | json }}</pre>

If you refresh your page now, then it should display the full JSON data that your website returned.
From here, you should be able to work out what's gone wrong in your code.

Final comment

As you can see, AngularJS beautifully separates your controller from your HTML page, and allows you to minimize the amount of coding you need to do.

But you do need to be careful that you - prepare yourself folks - comment what you are doing. Miss out comments in your code, and you'll have just written some beautiful JavaScript, which is wonderfully concise, which looks great to the users, but which is hopelessly unmaintainable to anyone else in your development team.

Even in this simple example, imagine you'd just picked up this HTML & JavaScript code, and been asked to find a bug in this:

<li ng-repeat='order in listOfOrders'>
    <span class="cssOrderHeader">Order # {{order.OrderID}} ( {{order.OrderDate}} )</span>
    <ul>
            <li ng-repeat='product in order.ProductsInBasket'>
                <span style="width:25px"> {{product.Quantity}}</span>

You would look at this HTML, and be wondering:

  • Where has that listOfOrders thing come from ?
  • I can see there's an order.OrderID and order.OrderDate... but what other fields from the Order record am I able to use ?
  • What is the trigger for loading the list of Order records for a particular Customer ?

And so on.


For many developers, just mentioning the words commenting or maintainability results in them rolling their eyes and claiming their code is obviously readable. After all, they've used AngularJS, so there's less of it now !!

The reality is that, even after a few months, beautiful code that you write today will make less and less sense tomorrow.

So, after patting yourself on the back for writing such beautiful AngularJS code, go back and spend a few minutes just adding a few lines of comments. Explain when your functions would be triggered, give a one-line summary of what that $scope.selectedCustomer variable is going to be used for, and perhaps even give an example of a typical JSON record which your function is about to bind to (this is a lifesaver to other developers).

Further reading

I've only scratched the service of AngularJS in this page. I didn't even mention Modules, Directives or Testing.

The best book I've found on the subject (and which I use myself) is AngularJS: Up and Running:



< Previous Page
Next Page >


Comments

blog comments powered by Disqus