In this post I will show how to use TypeScript together with AngularJS and ASP.NET Web API to write a simple web application that implements a CRUD scenario. TypeScript provides a set of features that enable developers to structure the code and write maintanable JavaScript applications more easily. It can also integrate with existing third party libraries as shown in the following demo. The simple HTML application will allow users to add, delete and retrieve products from a HTTP service backed by ASP.NET Web API.
The main highlights are as follows:
- use of TypeScript to create AngularJS controllers,
- communication with Web API services from TypeScript using AngularJS ajax functionality,
- use of strongly typed TypeScript declarations for AngularJS objects.
I am using Visual Studio 2012/Sublime Text 2 as my IDE in this example. You are fine using any text editor, and if you want to have syntax highlighting / auto-completion features you will need to download packages separately.
For Visual Studio plugin and windows compiler binary (tsc.exe, part of installer package) visit http://www.typescriptlang.org/#Download. If you prefer to use a different editor (eg. Sublime Text or Vim) get the goodies from http://aka.ms/qwe1qu. The only thing that you really need is TypeScript compiler binary.
The services are implemented using ASP.NET Web API (.NET framework for building HTTP services, which i personally like very much), but you can use any other technology as long as it produces valid JSON.
Http services using ASP.NET Web API
For this example I've chosen to deal with a simple model consisting of one entity - Product.
public abstract class Entity { public Guid Id { get; set; } } public class Product : Entity { public string Name { get; set; } public decimal Price { get; set; } }
We will need a persistence mechanism to store the entities. I am using in-memory storage (thread-safe collection) and a repository pattern. Feel free to change it to anything that suits you.
public interface IRepository<TEntity> where TEntity : Entity { TEntity Add(TEntity entity); TEntity Delete(Guid id); TEntity Get(Guid id); TEntity Update(TEntity entity); IQueryable<TEntity> Items { get; } }
public class InMemoryRepository<TEntity> : IRepository<TEntity> where TEntity : Entity { private readonly ConcurrentDictionary<Guid, TEntity> _concurrentDictionary = new ConcurrentDictionary<Guid, TEntity>(); public TEntity Add(TEntity entity) { if (entity == null) { //we dont want to store nulls in our collection throw new ArgumentNullException("entity"); } if (entity.Id == Guid.Empty) { //we assume no Guid collisions will occur entity.Id = Guid.NewGuid(); } if (_concurrentDictionary.ContainsKey(entity.Id)) { return null; } bool result = _concurrentDictionary.TryAdd(entity.Id, entity); if (result == false) { return null; } return entity; } public TEntity Delete(Guid id) { TEntity removed; if (!_concurrentDictionary.ContainsKey(id)) { return null; } bool result = _concurrentDictionary.TryRemove(id, out removed); if (!result) { return null; } return removed; } public TEntity Get(Guid id) { if (!_concurrentDictionary.ContainsKey(id)) { return null; } TEntity entity; bool result = _concurrentDictionary.TryGetValue(id, out entity); if (!result) { return null; } return entity; } public TEntity Update(TEntity entity) { if (entity == null) { throw new ArgumentNullException("entity"); } if (!_concurrentDictionary.ContainsKey(entity.Id)) { return null; } _concurrentDictionary[entity.Id] = entity; return entity; } public IQueryable<TEntity> Items { get { return _concurrentDictionary.Values.AsQueryable(); } } }
Once we have a persistence mechanism in place, we can create a HTTP service that will expose a basic set of operations. Because I am using ASP.NET Web API this means I need to create a new controller.
public class ProductsController : ApiController { public static IRepository<Product> ProductRepository = new InMemoryRepository<Product>(); public IEnumerable<Product> Get() { return ProductRepository.Items.ToArray(); } public Product Get(Guid id) { Product entity = ProductRepository.Get(id); if (entity == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return entity; } public HttpResponseMessage Post(Product value) { var result = ProductRepository.Add(value); if (result == null) { // the entity with this key already exists throw new HttpResponseException(HttpStatusCode.Conflict); } var response = Request.CreateResponse<Product>(HttpStatusCode.Created, value); string uri = Url.Link("DefaultApi", new { id = value.Id }); response.Headers.Location = new Uri(uri); return response; } public HttpResponseMessage Put(Guid id, Product value) { value.Id = id; var result = ProductRepository.Update(value); if (result == null) { // entity does not exist throw new HttpResponseException(HttpStatusCode.NotFound); } return Request.CreateResponse(HttpStatusCode.NoContent); } public HttpResponseMessage Delete(Guid id) { var result = ProductRepository.Delete(id); if (result == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return Request.CreateResponse(HttpStatusCode.NoContent); } }
We are trying to adhere to HTTP standard, hence additional logic to handle responses. After this step we should have a fully functional CRUD HTTP service.
Starting with AngularJS and TypeScript
With services ready for action, we can continue with creating the actual website. This will be plain HTML/CSS/JavaScript (TypeScript is compiled to JavaScript). The template I've started with looks like this:
<!DOCTYPE html> <html ng-app> <head> <title>Product list</title> <link rel="stylesheet" href='http://piotrwalat.net/Content/bootstrap.css' /> <script type="text/javascript" src='http://piotrwalat.net/Scripts/bootstrap.js' ></script> <script type="text/javascript" src='http://piotrwalat.net/Scripts/angular.js' ></script> <script type="text/javascript" src='http://piotrwalat.net/Scripts/Controllers/ProductsController.js' ></script> </head> <body> <div ng-controller="Products.Controller"> </div> </body> </html>
Please note that ProductsController.js file will be generated from ProductsController.ts TypeScript source using tsc.exe compiler (I am using command line for compilation step). Let's create the file that will contain AngularJS controller for our page.
module Products { export interface Scope { greetingText: string; } export class Controller { constructor ($scope: Scope) { $scope.greetingText = "Hello from TypeScript + AngularJS"; } } }
The Products module will be compiled into a JavaScript namespace and our controller will be available as Products.Controller. Now we can bind greetingText in our page. Please note that in order to leverage TypeScript's features we define contract for $scope object in the form of Scope interface. TypeScript would let us use any type instead as well, but personally I prefer more strict approach as it can help you catch errors at compile time (VS2012 will even underline errors in red as you edit your code, which is very nice). Now we have to compile ProductsController.ts, reference ProductsController.js in html and modify the view to display the message.
<div ng-controller="Products.Controller"> <p>{{greetingText}}</p> </div>
With AngularJS controller stub in place, let's move on and add some additional contract declarations.
Creating model module
Let's create a module called Model that will contain Product class used as a DTO (serialized to JSON) with our HTTP services.
module Model { export class Product { Id: string; Name: string; Price: number; } }
This simple module contains a definition of one type that is exported and ready to use in page controller class.
Ambient declarations
In order to leverage ajax functionality provided by AngularJS we will use $http service passed in to controller constructor:
class Controller { private httpService: any; constructor ($scope: Scope, $http: any) { this.httpService = $http; //... } //... }
Because we declared httpService as any type, the compiler will not be able to help us catch potential errors in compile time. To address this we can use ambient declarations. Ambient declarations are used to tell the compiler about elements that will be introduced to the program by external means (in our case through AngularJS) and no JavaScript code will be emitted from them. In other words think of them as a contract for 3rd party libraries. Declaration source files (.d.ts extension) are restricted to contain ambient declarations only. Here is angular.d.ts file that defines two interfaces used by us to call HTTP services.
declare module Angular { export interface HttpPromise { success(callback: Function) : HttpPromise; error(callback: Function) : HttpPromise; } export interface Http { get(url: string): HttpPromise; post(url: string, data: any): HttpPromise; delete(url: string): HttpPromise; } }
Declare keyword is optional, as it is implicitly inferred in all .d.ts files.
TypeScript and AngularJS
In order for compiler to know about newly introduced modules (Model and Angular) we need to add two reference statements to ProductsController.ts. Moreover we want to define Scope interface to include all properties and functions used by the view.
The page will consist of an input form for adding a new Product (name, price textboxes and a button) and will also display a list of all Products with the ability to delete individual entities. For brevity I am skipping, update scenario, but once we have other operations in place update implementation is really easy.
The Scope interface that provides this can look like this.
/// <reference path='angular.d.ts' /> /// <reference path='model.ts' /> module Products { export interface Scope { newProductName: string; newProductPrice: number; products: Model.Product[]; addNewProduct: Function; deleteProduct: Function; } // ... }
Any time a product is added or deleted we want to refresh the list by getting all products from the server. Thanks to declarations introduced earlier we can use strongly typed Angular.Http and Angular.HttpPromise interfaces.
The controller will contain private methods to communicate with our web service (getAllProducts, addProduct and deleteProduct).
export class Controller { private httpService: any; constructor ($scope: Scope, $http: any) { this.httpService = $http; this.refreshProducts($scope); var controller = this; $scope.addNewProduct = function () { var newProduct = new Model.Product(); newProduct.Name = $scope.newProductName; newProduct.Price = $scope.newProductPrice; controller.addProduct(newProduct, function () { controller.getAllProducts(function (data) { $scope.products = data; }); }); }; $scope.deleteProduct = function (productId) { controller.deleteProduct(productId, function () { controller.getAllProducts(function (data) { $scope.products = data; }); }); } } getAllProducts(successCallback: Function): void{ this.httpService.get('/api/products').success(function (data, status) { successCallback(data); }); } addProduct(product: Model.Product, successCallback: Function): void { this.httpService.post('/api/products', product).success(function () { successCallback(); }); } deleteProduct(productId: string, successCallback: Function): void { this.httpService.delete('/api/products/'+productId).success(function () { successCallback(); }); } refreshProducts(scope: Scope) { this.getAllProducts(function (data) { scope.products = data; }); } }
What's really nice is that we don't have to introduce any custom serialization logic. When retrieving products we treat data returned by $http service as a collection of strongly typed Products. Same applies to add operation - we simply pass a Product, it gets serialized and consumed by service in the end.
Creating the view
As a last step we need to create the view that will leverage new controller features. I am using bootstrap to make it a little bit easier.
<!DOCTYPE html> <html ng-app> <head> <title>Product list</title> <link rel="stylesheet" href='http://piotrwalat.net/Content/bootstrap.css' /> <script type="text/javascript" src='http://piotrwalat.net/Scripts/angular.js' ></script> <script type="text/javascript" src='http://piotrwalat.net/Scripts/Controllers/model.js' ></script> <script type="text/javascript" src='http://piotrwalat.net/Scripts/Controllers/productsController.js' ></script> </head> <body> <div ng-controller="Products.Controller"> <form class="form-horizontal" ng-submit="addNewProduct()"> <input type="text" ng-model="newProductName" size="30" placeholder="product name"> <input type="text" ng-model="newProductPrice" size="5" placeholder="product price"> <button class="btn" type="submit" value="add"> <i class="icon-plus"></i> </button> </form> <table class="table table-striped table-hover" style="width: 500px;"> <thead> <tr> <th>Name</th> <th>Price</th> <th></th> </tr> </thead> <tbody> <tr ng-repeat="product in products"> <td>{{product.Name}}</td> <td>${{product.Price}}</td> <td> <button class="btn-small" ng-click="deleteProduct(product.Id)"> <i class="icon-trash"></i> </button> </td> </tr> </tbody> </table> </div> </body> </html>
The end result should look like this
Now our page should be functional and communication with HTTP service should work as expected.
As usually code is available to browse and download on bitbucket.
Edit: It seems that Wordpress Android app managed to somehow overwrite this post with an old, incomplete version. Sorry for that as well as for reposting.