Angularjs + TypeScript – setting up a basic application with Visual Studio 2013
Preface
No, I have not abandoned Windows (Phone) Development, and am not planning to do that. But apart from being Windows Platform MVP (as it is called since a few weeks) I actually have a day job as an employee building web applications. A few years ago I brought in the SPA concept in the company, first based on Knockout, later Angular, and after the 2014 Dutch TechDays and actually having dinner with Erich Gamma I decided it was time to take on TypeScript. And the overall team lead agreed. Provided I would give some good feedback on my experiences. Well, how about this? ;)
Although I have ascertained the combination actually works very well, I really found myself in unchartered territory and it took some time to get off the ground. I started my blog in 2007 because there were not enough complete samples in the .NET world – well, in the web world this apparently goes squared and with a few VERY notable exceptions people are quite terse when it comes to giving help. I even got told off on Stack Overflow for commenting on an answer containing typos and suggested some calls were synchronous while in fact they were not. This is a apparently a whole different world than the helpful #wpdev community. Still, I soldiered on, and decided to do this blog post – or actually series of blog posts, that’s forged from the same fire as this blog itself: of frustration about lack of helpful samples.
I am not saying this the definitive guide to AngularJS and TypeScript – but it’s how I got stuff working, how I got to understand it, or at least I think I understand it. I hereby invite anyone thinking I do things wrong to provide corrections, or write better blog posts with better samples themselves. I will gladly link to you for credits.:)
I am not going to have a discussion about why to use TypeScript. I am going to assume you want to use it, that you know that in order to use it you will need files that type JavaScript types, and that you know the basics of creating a module, class or interface. This is mostly a how-to, with some explanations on the side. This article learns you:
- The initial setup of the solution
- What initial NuGet packages to get
- How to create the base application
- How to set up your first scope, view, controller and route
Prerequisites
I used
- Visual Studio 2013 Update 2 with the Microsoft Web Developer tools selected
- Web Essentials 2013 for Update 2
The last one is optional, but recommended. It gives a few extra options, as generating JavaScript classes from C# and (when you are typing TypeScript) seeing your code converted to JavaScript on the fly.
Creating a new project
- File/New Project/Web/ASP.Net Web application (it’s the only choice you have)
- Choose a name (I chose AtsDemo) and hit OK
- Choose “Web Api” and UNSELECT “Host is the cloud”
- Go to the NuGet Package manager and update all the packages, because a lot of them will be horribly outdated. Hit “accept” or “yes” on any questions
- Delete the “Areas” and “Fonts” folder cause we won’t need them
Getting the additional NuGet packages
This is quite a list. Maybe it is too much for just a basic start, but I decided to load it all.
- Angularjs
- Angularjs.Animate
- AngularJS.Cookies
- AngularJS.Route
- AngularJS.Sanatize
- Angularjs.TypeScript.DefinitelyTyped
- Jquery.TypeScript.DefinitelyTyped
Note that this will also pull in Angularjs.Core.
The TypeScript.DefinitelyTyped--files are definitions that make it possible to use typed versions of Angular and Jquery from TypeScript.
Set up the initial application
- Add a folder “app” to the root folder of your web project
- Add a file AppBuilder.ts to the app folder with the following contents:
module App { "use strict"; export class AppBuilder { app: ng.IModule; constructor(name: string) { this.app = angular.module(name, [ // Angular modules "ngAnimate", "ngRoute", "ngSanitize", "ngResource" ]); } public start() { $(document).ready(() => { console.log("booting " + this.app.name); angular.bootstrap(document, [this.app.name]); }); } } }
This is the basic setup for a class I use to ‘construct’ my app. I don’t like to do this in the global namespace. So I create a basic ‘module’ – which I tend to think of as a .NET namespace – in the “AppBuilder” in the namespace “App”.
Then add another file to app, called “start.ts”, to create an AppBuilder instance and call “start” to bootstrap your Angular app:
/// <reference path="appbuilder.ts" /> new App.AppBuilder('atsDemo').start();
By the way, the first line indicates this uses a type defined in appbuilder.ts. It’s good practice to add these references, although mostly (but certainly not always) the compiler seems to find the references itself. You can make these references easily by dropping one file on top of the other form the solution explorer (so in this case, I dropped AppBuilder.ts on top of start.js).
Then
- right-click your web project,
- select properties,
- go to the “TypeScript Build” tab,
- select “combine Javascript into output file”
- Enter “app/app.js” in the text box
Net result: see below.
Build your project, and verify that in the app folder the files “app.js” and “app.js.map” appear. Don’t include them in your project – when working with TypeScript it’s best to think of the created JavaScript as binaries. I use this options mainly to prevent loading order issues with regards to the resulting JavaScript, but it also makes the loading of JavaScript faster – now there’s only one file to load, in stead of more – and when you use TypeScript, it becomes quite a lot of files soon. Of course, you might also go for something like AMD with requirejs but for the somewhat smaller sites I tend to write, this works pretty well.
Then, open App_Start/Bundleconfig.cs and add the following lines just before the comment line "//Set EnableOptimizations to false for debugging. For more information,"
bundles.Add(new ScriptBundle("~/bundles/angular").Include( "~/Scripts/angular.js", "~/Scripts/angular-animate.js", "~/Scripts/angular-cookies.js", "~/Scripts/angular-route.js", "~/Scripts/angular-sanitize.js", "~/Scripts/angular-resource.js" )); bundles.Add(new ScriptBundle("~/bundles/app").Include( "~/app/app.js"));
This will include Angular files, as well as the JavaScript generated from the TypeScript. Finally, the last code line says
BundleTable.EnableOptimizations = true;
Change "true" to “false”. Then go to the Views/Shared/_Layout.cshtml. Change it’s contents to this:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="container body-content"> @RenderBody() </div> @Scripts.Render("~/bundles/jquery", "~/bundles/angular", "~/bundles/app") @RenderSection("scripts", required: false) </body> </html>
This will load the all the necessary scripts. Finally visit the Views/Home/Index.cshtml file. Delete it’s contents and replace it by just this:
@{ ViewBag.Title = "Home Page"; } angulartest={{1+1}} <div data-ng-view=""></div>
The “angulartest={{1+1}}“ line is just to see if Angular works. The div below it is the place where we will inject our views (see later).
Test the setup
Run the application from Visual Studio in your browser of choice. Hit F12 as soon as the browser starts. In your console it should say “booting atsDemo”, an in your browser window it should say “angulartest=2”
If you see this, then a) Angular is correctly loaded and activated (or else your browser window most likely displays “angulartest={{1+1}}”, meaning the expression is not evaluated and replaced and b) your application written in TypeScript actually has booted. Now it does nothing yet, but that’s the next step. For people who want to reference this stage, you can download the solution for this stage here. It may be useful if you just want a starter point.
Defining a scope object and a scope
The thing about TypeScript is typing, and you can also type your scope. Now the same caveats apply as to ‘normal’ Angular – if you go into a child scope or sub scope, or whatever they may be called, you can access but not change the parent scope – unless you specifically access it. That is quite of a hassle, so it’s good practice to create an object to put on the scope, and manipulate that object – and not the scope itself. That’s basically Angular, and has nothing to do with TypeScript per se.
So I first added a folder “scope” to my app folder, and created the following object to hold person data:
module App.Scope { "use strict"; export class Person { public firstName: string public lastName: string } }And then, to use it in the scope, I define and interface telling TypeScript that my scope, apart from the usual things that Angular provides, should at least contain a person of type Person:
/// <reference path="person.ts" /> module App.Scope { "use strict"; export interface IDemoScope extends ng.IScope { person : Person } }
Important to remember is that an interface is purely aTypeScript construct. It does not exist in JavaScript. If you look into the resulting app.js file, you won't find an IDemoScope. You won't find any interface. It's just a scaffold to help you not make typos in addressing this particular scope again. It needs to extend ng.IScope, a predefined interface from the DefinitelyTyped file.
Defining a controller
Add a folder “controller” to your app folder, and add a simple controller that allows you to enter the fields but also clear them again.
/// <reference path="../scope/idemoscope.ts" /> /// <reference path="../scope/person.ts" /> module App.Controllers { "use strict"; export class DemoController { static $inject = ["$scope"]; constructor(private $scope: Scope.IDemoScope) { if (this.$scope.person === null || this.$scope.person === undefined) { this.$scope.person = new Scope.Person(); } } public clear(): void { this.$scope.person.firstName = ""; this.$scope.person.lastName = ""; } } }
As you can see, a controller is just a plain old class not extending anything special, with a pubic method to clear the fields of a person. It has a few things to note.
- It’s the first class I show with an actual constructor. In that constructor it makes sure the person in the scope is initialized if necessary.
- Note that putting “private” in front of a constructor parameter automatically creates a private class member which can be henceforth referenced by this.$scope
- There is a static $inject variable. Although you are supposed to create the controller in the AppBuilder (see later) and only then inject the scope into in, apparently to prevent minification issues you have to provide this array too.
Defining a view
Create a folder “views” in you “app” folder, then add a file “PersonView.html” with the following very complex HTML in it:
<div> <div> <input type="text" data-ng-model="person.firstName" /> </div> <div> <input type="text" data-ng-model="person.lastName" /> </div> <div>{{person.firstName}}</div> <div>{{person.lastName}}</div> <button data-ng-click="myController.clear()">Clear</button> </div>This will give you a text box to enter the fields in, some feedback text below it to show data binding actually works, and a button to clear the fields again to see we can also actually can call bind to the controller - in this case a method.
Defining the controller in the AppBuilder
Just before the closing accolade } of the constructor, add this code:this.app.controller("personController", [ "$scope", ($scope) => new App.Controllers.DemoController($scope) ]);or if you want to super safe, explicitly type the scope explicitly so the controller get's the exact type of scope it expects.
this.app.controller("personController", [ "$scope", ($scope : Scope.IDemoScope) => new App.Controllers.DemoController($scope) ]);
I find two things odd about this:
- The fact that the first code works as well, while I would expect it would not – anything that is not typed explicitly is type “any” but clearly that does not fit into the constructor
- This compiles without making a reference to the files containing IDemoScope and DemoController.
Anyway, for good measure I always add these references just to make sure. I have been running into some odd problems where references suddenly could not be found anymore.
Creating the route definition
Once again, just before the last accolade of the AppBuilder constructor, add this code:
this.app.config([ "$routeProvider", ($routeProvider: ng.route.IRouteProvider) => { $routeProvider .when("/person", { controller: "personController", controllerAs: "myController", templateUrl: "app/views/personView.html" }) .otherwise({ redirectTo: "/person" }); } ]);This defines a single route "person", which is also the default route, for which it will use the personController, the view personView.html, and what is very important and had me searching for quite some time - it defines a binding alias for the controller in the router, in code. Almost all the samples I found where controller definitions in html, e.g.
<div data-ng-controller="personController as myController"> ... </div>but it took me quite some time to find out how to do it from code. Which I had to be able to do, as I sometimes have use a different controller on the same view. Well, this is how you do that: use "controllerAs" in your router definition.
Kick start the router
Finally, to get the router working and started, add once again a piece of code just before the last accolade of the AppBuilder constructor :
this.app.run([ "$route", $route => { // Include $route to kick start the router. } ]);
And we’re done… for now
If you run the code, you should see the url in the browser go from http://localhost:3987/ to http://localhost:3987/#/person, indicating your router is now in control, and it should show the beautiful *cough* UI on the right. I typed “Joost” in the first box and “van Schaik” in the second and as you type, the line below it should change with it, indicating data binding works to the scope. And the “Clear” clears both the boxes and the display lines, showing binding to the controller works as well.
Conclusion
Setting up an Angular + TypeScript application in Visual Studio 2013 is not that hard to do, once you know how to do it. I hope this post will save other developers from the stumbling around I did. I am, by far, not finished, but what I now have are more small gotcha’s, things you need to know and things that are apparently common knowledge, self-explanatory, blindingly obviously or buried deep inside documentation everyone seems to know by heart as no-one explains or mentions them to mere mortal (web) developers such as myself ;)
The full solution, as always, can be downloaded here, so you can see for yourself how things work in it’s totality, in stead of having to hunt it down piecemal from Stack Overflow.
Thanks
Special thanks to a few of the “notable exceptions” who helped me along the way to get here (and further)