Last night, I was playing with the latest .NET Core tooling in Visual Studio 2015 and decided to create an Anuglar2 application. It is not a straight-forward process. Additionally, most tutorials that you will see floating around don’t deal with Angular2 RC4 (v.Latest). And the few that do either aren’t Visual Studio 2015 specific or aren’t targeting ES5 as required by IE11 or older.
One of the reasons I started with .NET Core is because the templates provided already have the Bower and Node hooks. You only have to provide the configuration.
Diving right in, let’s create an ASP.NET Core Web Application.
UPDATE: Be sure to check out my other blog posts that reflect RC/Final versions of Angular2
https://long2know.com/2016/07/angular2-basic-routing/
https://long2know.com/2016/09/angular2-basic-routing-demo-updated-to-final/
I use the Web Applicaiton template rather than the Empty template because I want all of the built-in styles. I find this more illustrative of a real project.
Here’s our shiny new project.
Since our target is an Angular2 app, we have to add a package.json file to the project. This will allow us to bring in all of external JavaScript libraries. We’ll select “Add New Item” from Solution Explorer and create the default package.json.
For Angular2 RC4, here are all of the libraries we want to bring in:
{ "version": "1.0.0", "name": "asp.net", "private": true, "devDependencies": { "gulp": "3.9.1", "gulp-concat": "2.6.0", "gulp-cssmin": "0.1.7", "gulp-uglify": "1.5.3", "rimraf": "2.5.2", "del": "2.2.0", "typings": "^1.3.1", "typescript": "^1.8.10" }, "dependencies": { "@angular/common": "2.0.0-rc.4", "@angular/compiler": "2.0.0-rc.4", "@angular/core": "2.0.0-rc.4", "@angular/forms": "^0.2.0", "@angular/http": "2.0.0-rc.4", "@angular/platform-browser": "2.0.0-rc.4", "@angular/platform-browser-dynamic": "2.0.0-rc.4", "@angular/router": "3.0.0-beta.1", "@angular/router-deprecated": "2.0.0-rc.2", "@angular/upgrade": "2.0.0-rc.4", "angular2-in-memory-web-api": "0.0.9", "core-js": "^2.4.0", "reflect-metadata": "^0.1.3", "rxjs": "5.0.0-beta.6", "systemjs": "0.19.27", "zone.js": "^0.6.12" } }
Note that we have a devDependency on gulp. We’ll use gulp to copy all of our output files to the wwwwroot folder. So, add a Gulp Configuration File:
My gulp file adds some appropriate tasks. Note that I’m eventually going to use Typescript and plan to put my files inside of an “app” folder. This coincides with the “copy” task referenced.
/// <binding AfterBuild='default' Clean='clean' /> /* This file in the main entry point for defining Gulp tasks and using Gulp plugins. Click here to learn more. http://go.microsoft.com/fwlink/?LinkId=518007 */ var gulp = require('gulp'); var del = require('del'); gulp.task('default', function () { // place code for your default task here }); var paths = { webroot: "wwwroot/", npmSrc: "./node_modules/", npmLibs: "wwwroot/lib/npmlibs/", scripts: ['scripts/**/*.js', 'scripts/**/*.ts', 'scripts/**/*.map'] }; gulp.task('clean', function () { return del([paths.webroot + '/scripts/**/*']); }); gulp.task("lib", function () { gulp.src(paths.npmSrc + '/core-js/**/*.js') .pipe(gulp.dest(paths.npmLibs + '/core-js/')); gulp.src(paths.npmSrc + '/systemjs/dist/**/*.*', { base: paths.npmSrc + '/systemjs/dist/' }) .pipe(gulp.dest(paths.npmLibs + '/systemjs/')); gulp.src('./node_modules/reflect-metadata/**/*.js') .pipe(gulp.dest(paths.npmLibs + '/reflect-metadata')); gulp.src(paths.npmSrc + '/@angular/**', { base: paths.npmSrc + '/@angular/' }) .pipe(gulp.dest(paths.npmLibs + '/@angular/')); gulp.src(paths.npmSrc + '/zone.js/**/*.js', { base: paths.npmSrc + '/zone.js/' }) .pipe(gulp.dest(paths.npmLibs + '/zone.js/')); gulp.src(paths.npmSrc + '/es6-shim/es6-sh*', { base: paths.npmSrc + '/es6-shim/' }) .pipe(gulp.dest(paths.npmLibs + '/es6-shim/')); gulp.src(paths.npmSrc + '/es6-promise/dist/**/*.*', { base: paths.npmSrc + '/es6-promise/dist/' }) .pipe(gulp.dest(paths.npmLibs + '/es6-promise/')); gulp.src(paths.npmSrc + '/rxjs/**', { base: paths.npmSrc + '/rxjs/' }) .pipe(gulp.dest(paths.npmLibs + '/rxjs/')); }); gulp.task('copy', function () { gulp.src('./app/**/*.js') .pipe(gulp.dest(paths.webroot + '/appScripts')); }); gulp.task('default', ['lib', 'copy'], function () { gulp.src(paths.scripts).pipe(gulp.dest('wwwroot/scripts')) }); gulp.task('watch', function () { gulp.watch('./app/**/*.*', ['copy']); });
Next, we have to set up Typescript’s compiler options. Add a TypeScript JSON Configuration File:
{ "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "target": "es5", "module": "commonjs", "moduleResolution": "node", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "outDir": "./wwwroot/appScripts/", "rootDir": "./app" }, "exclude": [ "node_modules", "jspm_packages", "wwwroot", "typings/main", "typings/main.d.ts" ] }
You’ll note that I have some explicit paths for copying my outputs. I also want to target ES5 because this application needs to work on IE11 as well as Chrome. To start my Angular2 app, I’m going to create (2) TypeScript files and a startup.js file and put them under the folder structure “./app/test.” It should look like this:
The main.ts is going to be out TypeScript file that defines our main application shell with a single selector and template. The tempalte is very simple and only displays “Hello, World.”
import { Component } from "@angular/core"; @Component({ selector: "app-shell", template: ` <div> <br/> Hello, {{title}} </div> ` }) export class Main { title: string; constructor() { this.title = "World!"; } }
Next, we need a bootstrapper.ts that actually bootstraps our application main.
import {Main} from "./main"; import { bootstrap } from "@angular/platform-browser-dynamic"; import {ROUTER_DIRECTIVES} from "@angular/router"; bootstrap(Main, [ROUTER_DIRECTIVES]);
The startup.js defines a function that brings together everything to work and startup.
(function (global) { // map tells the System loader where to look for things var map = { 'app': 'appScripts', '@angular': 'lib/npmlibs/@angular', 'rxjs': 'lib/npmlibs/rxjs' }; // packages tells the System loader how to load when no filename and/or no extension var packages = { 'app': { main: 'test/bootstrapper.js', defaultExtension: 'js' }, 'rxjs': { defaultExtension: 'js' } }; var ngPackageNames = [ 'common', 'compiler', 'core', 'http', 'platform-browser', 'platform-browser-dynamic', 'router', 'router-deprecated', 'upgrade', ]; // Individual files (~300 requests): function packIndex(pkgName) { packages['@angular/' + pkgName] = { main: 'index.js', defaultExtension: 'js' }; } // Bundled (~40 requests): function packUmd(pkgName) { packages['@angular/' + pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' }; } // Most environments should use UMD; some (Karma) need the individual index files var setPackageConfig = System.packageWithIndex ? packIndex : packUmd; // Add package entries for angular packages ngPackageNames.forEach(setPackageConfig); var config = { map: map, packages: packages } System.config(config); System.import('app').catch(function (err) { console.error(err); }); })(this);
Finally, Index.cshtml is modified to serve up our application with appropriate script references and such. You’ll note the reference to our single app-shell element to which our Angular2 application will be bootstrapped.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Angular 2</title> </head> <body> <app-shell></app-shell> <script src="~/lib/npmlibs/core-js/client/shim.js"></script> <script src="~/lib/npmlibs/zone.js/dist/zone.js"></script> <script src="~/lib/npmlibs/reflect-metadata/Reflect.js"></script> <script src="~/lib/npmlibs/systemjs/system.src.js"></script> <script src="~/lib/npmlibs/systemjs/system.src.js"></script> <script src="~/appScripts/test/startup.js"></script> <script> System.import('appScripts/test/bootstrapper.js').then(null, console.error.bind(console)); </script> </body> </html>
If you hit F5 at this point, you’ll get build errors.
This is the problem I ran into immediately when following various guides. The problem with older guides is that the type definitions that are required by TypeScript were removed from Anuglar2 RC4. You have to import typings from npm that include an es6-shim. This was not really straight-forward to figure out in VS2015.
We need a few things. We need a typings.json configuration file. It should look similar to this and be placed in the root project folder.
{ "globalDependencies": { "body-parser": "registry:dt/body-parser#0.0.0+20160317120654", "compression": "registry:dt/compression#0.0.0+20160501162003", "cookie-parser": "registry:dt/cookie-parser#1.3.4+20160316155526", "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", "express": "registry:dt/express#4.0.0+20160317120654", "express-serve-static-core": "registry:dt/express-serve-static-core#0.0.0+20160322035842", "mime": "registry:dt/mime#0.0.0+20160316155526", "node": "registry:dt/node#4.0.0+20160412142033", "serve-static": "registry:dt/serve-static#0.0.0+20160317120654" } }
With the typings configuration in place, we need to modify our package.json to include a typings “postinstall” step under a member named “scripts:”
"scripts": { "typings": "typings", "postinstall": "typings install" }
With that in place, when we save package.json, you’ll notice that a “typings” folder is created in the project:
For TypeScript to compile properly, targetting ES5, we need a reference in our main.ts to the index.d.ts within the typings folder. So, place this at the head of main.ts:
/// <reference path="../../typings/globals/es6-shim/index.d.ts" />
Now that all of our boiler plate code is in place, our “Hello World” Angular2 app launches fine when we hit F5:
Note that the screen shot is depicting Chrome. It should also work fine in IE11 since we’re targetting ES5 (and it does):
Hopefully this small tutorial will help you get started using Angular2 RC4 and .NET Core in Visual Studio 2015. Getting a working project was a painful process, honestly. The test project is on Github:
Nice try, but it just doesn’t work. Neither following th instructions nor downloading your test project from git produces a test app that runs…
What didn’t work? I cloned the git project from scratch, rebuilt the solution, then hit F5 to debug and it worked without a hitch. I also cleared my browser cache to ensure there were no artifacts giving me a false positive.
For me, this worked just fine, the project compiles and runs. I did have to run npm install from the command line. It did not run well from inside Visual Studio.
Good deal! I did find that performance was pretty bad also during the general build process. I’ll have to locate whatever is causing the performance hit.
Followed your instructions but ran into a problem: when the “typings” directory was created, it had just 1 member in it…doesn’t look like yours at all. I was able to download your solution, unzip it and run in VS2015…but I had to change the version of .net core to a more recent one I had installed. Would like to know why it didn’t work following your instructions though…any ideas?
There’s nothing that comes to mind. If you have a “typings.json” with the references I listed and the modification to the package.json file with the typings postinstall, the typings folder should get populated as the screen shot depicts. I don’t recall doing anything beyond what I documented.
However, this post is pretty old (Angular2 RC4 especially) and VS2015 and the current ASP.NET Core templates could be substantially different. If it’s possible to upgrade to VS2017, I would recommend looking at some of the templates I have posted about for it using Angular4, .NET Core, and VS2017.
Otherwise, you could also use a compare tool like kdiff3 to see what differs between the github repo and the project you’ve created.