27 July 2014

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

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.

imageCreating 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.

image

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”

image

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

imageIf 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)

13 comments:

J.P. said...

Here's a little trick I use:

BundleTable.EnableOptimizations = !Debugger.IsAttached;

This way you can have everything unbundled while you are running locally. No need to toggle this setting back and forth.

Yo said...

Great article. Love the typing of scope.

Greymarch said...

Hmmm. Precisely followed your instructions. Everything worked except the clear button. My clear button does nothing. What might I be missing?

Joost van Schaik said...

@Todd download the demo solution

Unknown said...

I find it very handy to create a 'references.ts' file that contains all your references, then reference that file from individual ts files. That way, when adding a new service for example, that is needed by a number of controllers you simply need to add the reference to 'references.ts' file instead of each individual controller files.

Joost van Schaik said...

@keith I agree to an extent - I have found that simply doing all references in one file does not always solve that, you can get ordering problems forcing to add reference then to individual files as well, and I also had an occasion where running in debug mode just hung the web site. So, yes: good tip, but use with caution

GoldenCat said...

Thank you very much for creating this article.

Angular is made so much easier to work with when you have a great IDE and typescript. I now enjoy writing frontend code.

Thanks again.

Anonymous said...

@Todd I know this is an old post, but since I saw no responses explaining the issue, I thought I post this for future viewers. In the sample above in the personView.html, the button has a directive "data-ngclick". This should actually read "data-ng-click". In angular, the directive name is camelCase of ngClick and then when you add it to html you have to change the camelCase to hyphen separated. Hope that helps.

Joost van Schaik said...

Dang. @Doug, you are right. In the demo solution I got it right, but apparently I have made an error putting it into text. Thanks for pointing this out.

data.baaqmd said...

Great example and thank you.

The problem I had is that:
After I built, I only got start.js and start.js.map in my app folder, there is no app.js even I specified "app/app.js" in combine Javascript output to file.

By the way, I built it using vs 2015 community version.

Joost van Schaik said...

@ata.baaqmd have your tried a VS2013 version? I haven't used this inside 2015

Unknown said...

you have used app.js in bundles, but haven't created it. how does it work??

bundles.Add(new ScriptBundle("~/bundles/app").Include(
"~/app/app.js"));

Joost van Schaik said...

@Ali, have a look at the screen below the text "Enter “app/app.js” in the text box"