Introduction
In my
previous article, we’ve set up the backbone of our Angular 2 Application. As promised, we will continue to explore Angular 2 within ASP.NET Core by using Web API to work with the Server-side data.
If you have stumbled upon this article, then I presume you are new to Angular 2 and would like to get your hands dirty with the practical examples. Just like you, I am also pretty much new to Angular 2 and that’s why I am writing this series of articles as a reference to other ASP.NET developers who might want to jump on Angular 2.
Luckily, I had the opportunity to review a book on ASP.NET Web API and Angular 2 by Valerio De Sanctis. So, it’s a good time to incorporate the things that I’ve learned from the book into this series of articles. If you are looking for in-depth information about Angular 2 with ASP.NET Web API, I would highly recommend you to grab yourself a copy of the book
here.
In this series of articles, we will learn how to build a data-driven Angular 2 app from the scratch.
What You Will Learn
In this particular series, you will learn the following:
- Overview of Angular 2 RC and Core Web API 2.
- Upgrading from Angular 2 Beta to RC4.
- Enable Serving Static Files and Diagnostics
- Adding the typings.json file
- Updating the package.json file
- Updating the tsconfig.json file
- Modifying the Existing Angular 2 components
- Switching to Gulp
- Adding the systemjs.config.js file
- Updating the index.html
- Testing the App
- Overview of Web Request and Response Flow
- The Backpacker’s Lounge App
- Restructuring the App
- Integrating NewtonSoft.JSON
- Creating the ViewModels
- Creating the Web API Controllers
- Creating the Client-Side ViewModels
- Creating the Client-Side Service
- Creating the Angular 2 Components
- Enabling Client-Side Routing
- Rewriting
- Running the App
- Tips
Before you go any further, though it’s not required, I would suggest you to read my previous article about Getting Started with Angular 2 in ASP.NET Core,
since I will not be covering the details about setting up Angular 2 in ASP.NET Core in this series.
Overview
Before we move further, let’s talk about a brief recap about each framework:
Core Web API
ASP.NET Core Web API is a framework built on top of the .NET Core. It’s made specifically for building RESTful services, which can serve a massive range of the clients including Web Browsers, mobile devices and more. ASP.NET Core has a built-in support for MVC building Web APIs. Unifying the two frameworks makes it simpler to build the apps.
Angular 2
Angular 2 is the second major installment of AngularJS and is entirely written in TypeScript. For the developers who are working with Angular 1.x, you might find a big change in Angular 2, as it is entirely component based and an object orientation is much easier with enhanced DI capability.
We can think of Web API as our data transfer gateway that composes a set of Server-side interfaces/endpoints. This set processes request-response messages, typically expressed in a form of JSON or XML. To put it in other words, our Web API will serve as our central gateway to handle the request from the client, perform the Server-side data operations, and then send back the response to the client (the caller). Angular can be described as a modern client-side library, which provides rich features, which gives the
Browser; the ability to bind the input/output parts of a Web page to a flexible, reusable and easily testable JavaScript model.
In this part, we will see the Client-Server capabilities of both the frameworks and how they interact with each other. In other words, we need to understand, how Angular 2 retrieve and transfer the data from ASP.NET Core Web API. We will be using the Angular 2 RC 4 and ASP.NET Core 1.0 RTM to build the Application.
Upgrading to Angular2 RC 4
In my previous article, we were using Angular 2 Beta version. Recently, Angular 2 RC version was released to align with the current version, I think it’s a good idea to upgrade.
Important: The current release of Angular 2 requires at least node v4.x.x and npm 3.x.x. Verify that you are running the versions mentioned, by running node -v and npm -v in a terminal/console window. Older versions produce errors.
Before making any file changes, make sure, that you have the supported versions of Node and NPM. If you have the older versions , uninstall node, download and install the required version here: Node.js
NPM is already integrated in the Node installer, so you don’t have to worry about manually upgrading it.
After the installation, make sure to restart your machine to ensure the updates will take effect. In my case, I have the following versions installed:
- Node v6.3.1 (the current release as of this writing).
- NPM 3.10.3 (latest version as of this writing).
Let’s Start Modifying!
Once you’re all set, we can now start modifying our existing Angular 2 App. If you haven’t referred my previous article and would like to jump on this series, make sure to create the files required, if they do not exist in your project.
Enable Serving Static Files and Diagnostics
As a recap, we need to add the StaticFiles dependency, so our Application can serve static files such as JavaScripts, CSS and HTML. Also, it might be a good idea to enable the diagnostics to capture the Application errors for development purposes. To do this, add the following as dependency in your project.json file:
- "Microsoft.AspNetCore.StaticFiles": "1.0.0",
- "Microsoft.AspNetCore.Diagnostics": "1.0.0"
Save the file to install the needed packages. Once installed, modify your Configure() method within Startup.cs and it will look like:
- public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
- loggerFactory.AddConsole(Configuration.GetSection("Logging"));
- loggerFactory.AddDebug();
- app.UseDefaultFiles();
- app.UseStaticFiles();
- if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
- app.UseMvc();
- }
There’s nothing much to say about the code, mentioned above. We just added a call to UseDefaultStaticFIiles() to enable the wwwroot to the Server static files and call the UseDeveloperExceptionPage() to allow us to see the error message in the page, when something wrong is going to happen in our code.
Adding the typings.json File
Based on the official Angular 2 Quickstart demo, we need to create a new file for creating typings containing the node, jasmine and core-js typings. To do this, just right-click on your project root and then add a new.json file. In my case, I have selected the “NPM Config File” and just rename it as “typings.json”. Replace the default generated content with the following:
- {
- "globalDependencies": {
- "core-js": "registry:dt/core-js#0.0.0+20160602141332",
- "jasmine": "registry:dt/jasmine#2.2.0+20160621224255",
- "node": "registry:dt/node#6.0.0+20160621231320"
- }
- }
Note: The core-js line in the typings.json file is the only required one. We’ve also taken the chance to add the Jasmine and node typings: you may need them in the near future, if you want to use the Jasmine test framework and/or use code that references objects in the nodejs environment. Keep them there for now, they won’t hurt your project.
Updating the package.json File
Open the package.json file and replace everything in it with the following:
- {
- "version": "1.0.0",
- "name": "AngularJS2Demo",
- "author": "ProudMonkey",
- "description": "A simple DEMO on Angular 2 RC4 within ASP.NET Core 1.0 RTM",
- "scripts": {
- "postinstall": "typings install",
- "typings": "typings"
- },
- "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",
- "systemjs": "0.19.27",
- "core-js": "^2.4.0",
- "reflect-metadata": "^0.1.3",
- "rxjs": "5.0.0-beta.6",
- "tsd": "^0.6.5",
- "zone.js": "^0.6.12"
- },
- "devDependencies": {
- "typescript": "^1.8.10",
- "gulp": "^3.9.1",
- "gulp-clean": "^0.3.2",
- "gulp-concat": "^2.6.0",
- "gulp-sourcemaps": "^1.6.0",
- "gulp-typescript": "^2.13.6",
- "gulp-uglify": "^1.5.3",
- "typings": "^1.3.1",
- "gulp-tsc": "^1.2.0"
- }
- }
If you have noticed, we’ve added the scripts section to install the typings. Typings manages and installs the TypeScript definitions.
The packages with the @ symbol is a part of the new Angular 2 bundle: the other ones are loading libraries, helper tools and polyfills for older Browser support. The current version of Angular 2, as of this writing is 2.0.0-rc.4. You can check for the updates here. You may also have noticed that, we’ve replaced the dependencies to Grunt with Gulp ~ I’ll explain, why later in this article. You may remove the existing gruntfile.js file, just like, what I did if you’d like, but if you prefer using Grunt, feel free do so. No one is going to fire you for using Grunt .
Now, save the file to restore the needed packages for the Application. Once installed, you should be able to see something like this:
Figure 1: Application Dependencies
Tip: If Typings didn’t install successfully on Save, try to use the “Restore Package” option by right-clicking on the Dependencies node. Another way is to use the command line to run the typings explicitly. To do this, just navigate to the root folder of your app and do a CTRL+SHIFT, select “Open command window here”. In the command line, type the following command:
npm run typings install
If successful, you should be able to see something like this:
Figure 2: Command Line
Now, switch back to Visual Studio and you should be able to see the typings folder within the root of your app, as shown in the figure, given below:
Figure 3: Solution Explorer
As a recap, all Angular 2 dependencies will be installed in the following location in your local drive:
../src/<YourProjectName>/node_modules
Updating the tsconfig.json File
Now, open your tsconfig.json file. If you do not have it, you need to create one. Here’s the updated TypeScript JSON Configuration file:
- {
- "compileOnSave": true,
- "compilerOptions": {
- "target": "es5",
- "module": "system",
- "moduleResolution": "node",
- "experimentalDecorators": true,
- "emitDecoratorMetadata": true,
- "noImplicitAny": false,
- "suppressImplicitAnyIndexErrors": true,
- "noEmitOnError": true,
- "removeComments": false,
- "sourceMap": true
- },
- "exclude": ["node_modules", "wwwroot", "Scripts/app"]
- }
The compileOnSave signals the IDE to generate all the files for a given tsconfig.json upon saving. CompilerOptions configuration will influence, how the Intellisense and our external TypeScript compiler will work. By excluding the “Scripts/app” folder in the config, we tell the built-in TypeScript compiler, provided by Visual Studio 2015 to disable compiling the external TypeScript files within the location.
Changes on Angular 2 Components
Since we are upgrading to RC version, we also need to update the components, which have references to the previous beta versions of Angular. We are going to split each component update for you to easily follow.
The app.component.ts- import { Component } from '@angular/core';
- @Component({
- selector: 'angularjs2demo',
- template: `
- <h1>AngularJS 2 Demo</h1>
- <div>Hello ASP.NET Core! Greetings from AngularJS 2.</div>
- `
- })
-
- export class AppComponent {}
There’ s not much to say about the update above.We just changed the import reference to the new name which is @angular/core.
The boot.ts
-
- import { bootstrap } from '@angular/platform-browser-dynamic';
- import { AppComponent } from './app.component';
- import 'rxjs/Rx';
-
- bootstrap(AppComponent);
Just like in the app.component.ts file, we changed the reference to the new Angular bundle. Notice, we also change the typings reference.
Switching to Gulp
This time, we will be using Gulp as the JavaScript Task Runner to automate our client-side scripts. I’d like to note that switching to Gulp does not mean Grunt is a bad tool. Of course Grunt is still a great tool and used by many. If you prefer, you can always use it to automate tasks for you. I just preferred to use Gulp because of its conciseness and easy task writing. It also uses node.js’ streams, and executes faster, since it does not open/close files, or create intermediary copies all the time. But then again, every developer has a preference, so it’s all up to you which JS task runners you would use .
Now let’s add a new file for our Gulp configuration. Go ahead and right click on the project solution and then select Add > New Item. Under Client-side template, select “Gulp Configuration File” as shown in the figure below:
Figure 4: Adding Gulp File
Click Add to generate the file, and then replace the default generated configuration with the following code below:
- var gulp = require('gulp'),
- gp_concat = require('gulp-concat'),
- gp_sourcemaps = require('gulp-sourcemaps'),
- gp_typescript = require('gulp-typescript'),
- gp_uglify = require('gulp-uglify'),
- gp_clean = require('gulp-clean');
-
- var srcPaths = {
- app: ['Scripts/app/boot.ts', 'Scripts/app/**/*.ts'],
- js: ['Scripts/js/**/*.js',
- 'node_modules/core-js/client/shim.min.js',
- 'node_modules/zone.js/dist/zone.js',
- 'node_modules/reflect-metadata/Reflect.js',
- 'node_modules/systemjs/dist/system.src.js',
- 'node_modules/typescript/lib/typescript.js',
- 'node_modules/ng2-bootstrap/bundles/ng2-bootstrap.min.js',
- 'node_modules/moment/moment.js'],
- js_angular: ['node_modules/@angular/**'],
- js_rxjs: ['node_modules/rxjs/**']
- };
-
- var destPaths = {
- app: 'wwwroot/app/',
- js: 'wwwroot/js/',
- js_angular: 'wwwroot/js/@angular/',
- js_rxjs: 'wwwroot/js/rxjs/'
- };
-
- gulp.task('clean', function() {
- return gulp.src(destPaths).pipe(clean());
- });
-
-
-
- gulp.task('app', function() {
- return gulp.src(srcPaths.app)
- .pipe(gp_sourcemaps.init())
- .pipe(gp_typescript(require('./tsconfig.json').compilerOptions))
- .pipe(gp_uglify())
- .pipe(gp_sourcemaps.write('/'))
- .pipe(gulp.dest(destPaths.app));
- });
-
-
- gulp.task('js', function() {
- gulp.src(srcPaths.js_angular).pipe(gulp.dest(destPaths.js_angular));
- gulp.src(srcPaths.js_rxjs).pipe(gulp.dest(destPaths.js_rxjs));
- return gulp.src(srcPaths.js).pipe(gulp.dest(destPaths.js));
- });
-
-
- gulp.task('watch', function() {
- gulp.watch([srcPaths.app, srcPaths.js], ['app', 'js']);
- });
-
-
- gulp.task('default', ['app', 'js', 'watch']);
The code above contains three (3) main variables:
- gulp - initializes each Gulp plugins, which we are going to need for running the tasks.
- srcPaths - holds an array of the sources, which we want to copy and transpile.
- destPaths - holds an array of the specific location within wwwroot. This is basically where we dump the scripts we defined from the srcPaths.
It also contains five (5) main tasks:
- clean - This task deletes the existing files from the destination folders we defined.
- app - This task compiles, uglify and create sourcemaps for all TypeScript files and place them to wwwroot/app folder, together with their js.map files.
- js - This task will copy all JavaScript files from the external libraries, which are located within the node_modules folder and place them to wwwroot/js folder.
- watch - This task watches the files, defined in app and JS tasks, which are changed.
- default - Define the default task, so it will launch all the other tasks.
Adding the systemjs.config.js File
Now, let’s add the systemjs configuration file. Right-click on the wwwroot folder and select Add > New Item. Under Client-side templates, select “JavaScript File” just like in the figure, given below:
Figure 5: Add New Item Dialog
Name the file as “systemjs.config.js” and copy the code, given below:
- (function(global) {
-
- var map = {
- 'app': 'app',
- '@angular': 'js/@angular',
- 'rxjs': 'js/rxjs'
- };
-
-
- var packages = {
- 'app': {
- main: 'boot.js',
- defaultExtension: 'js'
- },
- 'rxjs': {
- defaultExtension: 'js'
- }
- };
-
- var ngPackageNames = ['common', 'compiler', 'core', 'forms',
- 'http', 'platform-browser', 'platform-browser-dynamic',
- 'router', 'router-deprecated', 'upgrade', ];
-
- function packIndex(pkgName) {
- packages['@angular/' + pkgName] = {
- main: 'index.js',
- defaultExtension: 'js'
- };
- }
-
- function packUmd(pkgName) {
- packages['@angular/' + pkgName] = {
- main: '/bundles/' + pkgName + '.umd.js',
- defaultExtension: 'js'
- };
- };
-
- var setPackageConfig = System.packageWithIndex ? packIndex : packUmd;
- ngPackageNames.forEach(setPackageConfig);
- var config = {
- map: map,
- packages: packages
- }
- System.config(config);
- })(this);
The SystemJS.config file will load the Application and library modules. We are going to need this loader to build our Angular 2 app. For more details about SystemJS config, read on:
SystemJS
Updating the index.html
Since we need the SystemJS config to load our app modules and components, we need to update our index.html to include the new configuration. Replace your index.html file, so it will look something like:
- <html>
-
- <head>
- <base href="/">
- <title>ASP.NET Core with Angular 2 RC Demo</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <!-- Step 1. Load libraries -->
- <!-- Polyfill(s) for older browsers -->
- <script src="js/shim.min.js"></script>
- <script src="js/zone.js"></script>
- <script src="js/Reflect.js"></script>
- <script src="js/system.src.js"></script>
- <!-- Step 2. Configure SystemJS -->
- <script src="systemjs.config.js"></script>
- <script>
- System.import('app').catch(function(err) {
- console.error(err);
- });
- </script>
- </head>
- <!-- Step 3. Display the application -->
-
- <body>
- <!-- Application PlaceHolder -->
- <angularjs2demo>Please wait...</angularjs2demo>
- </body>
-
- </html>
Testing the App
Clean, Build your solution and make sure it has no errors. If it builds successfully, right click on the gulpfile.js and select “Task Runner Explorer”.
Make sure to click the "Refresh" button to load the task, as shown in the figure, given below:
Figure 6: Task Runner Explorer
Now, right-click to the default task and hit Run. You should be able to see something like this, when successful:
Figure 7: Task Runner Results
After successfully running the Task Runner, you should also see that the “app” and “js” folders are generated within your “wwwroot” folder , as shown in the figure, given below:
Figure 8: The wwwroot
Running the Application should result in something like:
Figure 9: The Output
If you’ve made it this far, congrats! You have now an Angular 2 RC4 running and now ready to work with the data, using Core Web API.
Tips:
- If you are getting the following Typescript Build errors:
“TS2664 Invalid module name in augmentation, module '../../Observable' cannot be found.”
Just follow the solution provided here: https://github.com/Microsoft/TypeScript/issues/8518#issuecomment-229506507
In my case, since I’m using VStudio 2015 Update 3, the fix was to replace the typescriptService.js with the updated version mentioned from the link above.
- If you are getting the errors during compilation that says something like these:
TS1005 Build:'=' expected.
TS1005 Build:';' expected.
Just make sure, you exclude the wwwroot in your ts.config. If it doesn’t work, you can try the following options for the fix:
Option 1: Filtering out the location, which contains the *.d.ts files from compiling in your ts.config file.
Option 2: Upgrade to TypeScript 1.9.x or greater Beta versions by running the following command within your project root:
npm install typescript@next
Creating the Application
Before we get our hands dirty, let’s have a quick overview on what happens, when a request is issued by the client and how the Server sends out a JSON Response by looking at the figure, given below:
Figure 10: Web Request and Response Flow
Async Data Requests are any user interactions, as long as it needs to retrieve data from the server, a very typical example includes (yet is not limited to): clicking a button to show data or to edit/delete something, navigating to another view, submitting a form and so on. And in order to respond to any client-issued Async Data Request, we need to build a server-side Web API Controller that would handle the client request (CRUD). In other words, the API will process the request, perform database CRUD operations and return a serialized ViewModel to the client as a Response.
To understand how Angular and Web API connects to each other in real scenarios then let’s go ahead and start building our app.
The Backpacker’s Lounge App
For the entire series, we will be creating an application that covers a bunch of CRUD operations. I love to travel and explore the wonders of nature, so to make this app different and interesting for starters, I have decided to build a simple Backpackers Lounge App.
To make it more clear, we will be creating something like this:
Figure 11: The Main App Layout
In this part of the series, we are going to focus on creating the Home content. Meaning, we are going to create the components for displaying the latest discussion, latest places added and the most viewed places. We are also going to setup the client-side routing and see how the navigation works in Angular 2. Adding to that, we will implement a simple master-detail navigation to see how we are going to pass information between components. Finally, we are going to see the basics on how the two-way data-binding will work in Angular2.
Looking at the figure above, we are going to need the following sets of API calls:
- api/Lounge/GetLatestDiscussion
- api/Place/GetLatestEntries
- api/Place/GetMostViewed
The GetLatestDiscussion will return a list of items for discussion. This API method will be hosted within the LoungeController class.
The GetLatestEntries will return a list of items for new places added. The GetMostViewed will return a list of items for places that has the top page views. Both of these API methods will be hosted within the PlaceController class as the API URI defined above suggests.
Let’s Get Started!
Before we move further, let’s revamp our application structure so it would be more maintainable and to value the
separation of concerns.
Restructuring the App
Looking at the picture shown in figure 11, we need to add some folders to our existing “app” folder and then move some existing files. Let’s start by adding the following folders below under Scripts/app folder:
- components
- services
- viewmodels
The components folder is where we store all TypeScript files for Angular 2 related components.
The services folder is where we store our TypeScript based services for communicating with ASP.NET Core Web API.
The viewmodels folder is where we store our TypeScript based strongly-typed view models.
Now, under the components folder, add the following sub-folders:
- home
- lounge
- about
- account
- explore
If you have noticed, the folders above matched with our navigation menu shown in figure 11. Note that the folders don’t really need to be the same name as the menus. You can name the folders to whatever you like, but for this demo, let’s stick to that for easy reference. Each folder is where we store the corresponding components for specific features in the website.
Once you have all of that folders set, move the “app.component.ts” to the components folder. Your project structure should now look something like this:
Figure 12: The Components Folder
Since we moved the location of our app.component.ts file, then we also need to update the import reference of that file in our boot.ts file:- import {AppComponent} from './components/app.component';
Integrating NewtonSoft.JSON
If you’re working with ASP.NET and you’ve never heard about Newtonsoft’s Json.NET, you most certainly missed something that could’ve eased your job. Big time. We’re talking of one of the finest libraries – and most useful tools – ever developed for .NET, at least for the writer: a very efficient (and thus very popular), high-performance JSON serializer, deserializer and all-around framework for .NET, which also happens to be completely Open Source.
To add the Newtonsoft.JSON in our project, go ahead and right click on the project root and then select Manage NuGet Packages. Under “Browse” tab, enter “newtonsoft” in the search bar and it should display the package just like in the following:
Figure 13: Manage Nuget Packages
The latest version of Newtonsoft.Json is 9.01 as of this writing. Click Install to add the dependency in our project. After successful installation, you should be able to see the Newstonsoft.Json added in our project references.
Creating the ViewModels
To provide you a quick recap, ViewModels are just classes that house some properties that we only need in the View/UI. From this moment, we are going to use ViewModels as our data transfer objects: sending data from client to server and/or vice-versa.
In this part, we will not be using any database at this point for us to test something out. We’ll just put together some static test data in order to understand how to pass them back and forth by using a well-structured and highly-configurable interface. One of the great things in building a native Web App using ASP.NET and Angular2 is that we can start writing our code without worrying much about the data source: they will come later, and only after we’re sure about what we really need.
Now, it’s time to create the server-side ViewModels.
Create a new folder at the root of your project and name it as “Data” and under it, create a sub-folder and name it as “ViewModels”. Within that folder, create the following class:
- LoungeViewModel.cs
- PlaceViewModel.cs
We should now have the following structure:
Figure 14: View Models Folder
Here are the codes for each class:
The LoungeViewModel
Update the default code generated so it would look similar to the following code below: - using System;
- using Newtonsoft.Json;
- namespace AngularJS2Demo.ViewModels {
-
- [JsonObject(MemberSerialization.OptOut)]
- public class LoungeViewModel {
- public int ID { get; set; }
- public string Subject { get; set; }
- public string Message { get; set; }
- [JsonIgnore]
- public int ViewCount { get; set; }
- public DateTime CreatedDate { get; set; }
- public DateTime LastModifiedDate { get; set; }
- }
-
- }
The PlaceViewModel
And here’s the code for the PlaceViewModel class:- using System;
- using Newtonsoft.Json;
- namespace AngularJS2Demo.ViewModels {
-
- [JsonObject(MemberSerialization.OptOut)]
- public class PlaceViewModel {
- public int ID { get; set; }
- public string Name { get; set; }
- public string Location { get; set; }
- [JsonIgnore]
- public int ViewCount { get; set; }
- public DateTime CreatedDate { get; set; }
- public DateTime LastModifiedDate { get; set; }
- }
-
- }
Both ViewModels above are nothing but just classes that house some properties. Each class is decorated with the [JsonObject(MemberSerialization.OptOut)] attribute that causes the properties to be serialized into JSON unless being decorated by an explicit [JsonIgnore]. We’re making this choice because we’re going to need most of our ViewModel’s properties serialized, as we’ll be seeing soon enough.
We are also limiting the properties defined in our ViewModels at this point. Eventually, we will be adding more to them once we integrate Authorizan, Authentication and Image for places. So for now let’s keep rolling.
Creating the Web API Controllers
Now, let’s create the needed API Controllers in our app.
The LoungeController
Create a new class under “Controllers” folder and name it as “LoungeController”. To do that, just right click on the “Controllers” folder and then select Add > New Item. Under ASP.NET template, select “Web API Controller Class”. Replace the default generated code with the following:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Mvc;
- using AngularJS2Demo.Data.ViewModels;
- using Newtonsoft.Json;
-
- namespace AngularJS2Demo.Controllers {
-
- [Route("api/[controller]")]
- public class LoungeController: Controller {
-
- [HttpGet("{id}")]
- public IActionResult Get(int id) {
- return new JsonResult(GetTestData().Where(i => i.ID == id), DefaultJsonSettings);
- }
-
- [HttpGet("GetLatestDiscussion")]
- public IActionResult GetLatestDiscussion() {
- return GetLatestDiscussion(DefaultNumberOfItems);
- }
-
- [HttpGet("GetLatestDiscussion/{n}")]
- public IActionResult GetLatestDiscussion(int n) {
- var data = GetTestData().OrderByDescending(i => i.CreatedDate).Take(n);
- return new JsonResult(data, DefaultJsonSettings);
- }
-
- private List < LoungeViewModel > GetTestData(int num = 99) {
- List < LoungeViewModel > list = new List < LoungeViewModel > ();
- DateTime date = DateTime.Now.AddDays(-num);
- for (int id = 1; id <= num; id++) {
- list.Add(new LoungeViewModel() {
- ID = id,
- Subject = String.Format("Discussion {0} Subject", id),
- Message = String.Format("This is a sample message for Discussion {0} Subject: It's more fun in the Philippines", id),
- CreatedDate = date.AddDays(id),
- LastModifiedDate = date.AddDays(id),
- ViewCount = num - id
- });
- }
- return list;
- }
-
- private JsonSerializerSettings DefaultJsonSettings {
- get {
- return new JsonSerializerSettings() {
- Formatting = Formatting.Indented
- };
- }
- }
-
- private int DefaultNumberOfItems {
- get {
- return 10;
- }
- }
-
- private int MaxNumberOfItems {
- get {
- return 50;
- }
- }
- }
- }
The class above make use of Attribute Routing to determine that the class is an API by decorating with this attribute: [Route("api/[controller]")].
The class above contains the following three (3) main API action methods:
- Get() – This method returns a single set of data based on a given ID. This method can be invoked using: /api/lounge/<id> where id is a variable
- GetLatestDiscussion() – This method represents a RESTFUL API method, and just calls it’s overload method to actually process the request. This method can be invoked using: /api/lounge/getlatestdiscussion
- GetLatestDiscussion(int n) – This method is the overload of GetLatestDiscussion() which takes an int parameter. It use the LINQ OrderByDescending() and Take operators to get the latest records from our test data. This method can be invoked using: /api/lounge/getlatestdiscussion/<n> where n is a variable, representing a number.
It also contains the following private methods:
- GetTestData(int num = 99) - A test method that returns a static list of items. We will be using this method to generate a list of LoungViewModel just for us to test something out quickly.
- DefaultJsonSettings - A property that gets the default JSON format settings
- DefaultNumberOfItems - A property that returns the defaul number of items to be returned in the UI.
- MaxNumberOfItems - A property that returns the default maximum number of items to be returned to the UI.
We are going to use the aforementioned private members for displaying a test data in our app. We will split and decouple our data access from within our Controller in next part of this series, but for now, let’s just keep rolling.
The PlaceController
Create another Web API Controller Class and name it as “PlaceController”. Again, replace the default code with the following:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Mvc;
- using AngularJS2Demo.Data.ViewModels;
- using Newtonsoft.Json;
-
- namespace AngularJS2Demo.Controllers
- {
- [Route("api/[controller]")]
- public class PlaceController : Controller
- {
- [HttpGet("{id}")]
- public IActionResult Get(int id)
- {
- return new JsonResult(GetTestData().Where(i => i.ID == id), DefaultJsonSettings);
- }
-
- [HttpGet("GetLatestEntries")]
- public IActionResult GetLatestEntries()
- {
- return GetLatestEntries(DefaultNumberOfItems);
- }
-
- [HttpGet("GetLatestEntries/{n}")]
- public IActionResult GetLatestEntries(int n)
- {
- var data = GetTestData().OrderByDescending(i => i.CreatedDate).Take(n);
- return new JsonResult(data, DefaultJsonSettings);
- }
-
- [HttpGet("GetMostViewed")]
- public IActionResult GetMostViewed()
- {
- return GetMostViewed(DefaultNumberOfItems);
- }
-
- [HttpGet("GetMostViewed/{n}")]
- public IActionResult GetMostViewed(int n)
- {
- if (n > MaxNumberOfItems) n = MaxNumberOfItems;
- var data = GetTestData().OrderByDescending(i => i.ViewCount).Take(n);
- return new JsonResult(data, DefaultJsonSettings);
- }
-
- private List<PlaceViewModel> GetTestData(int num = 999)
- {
- List<PlaceViewModel> list = new List<PlaceViewModel>();
- DateTime date = DateTime.Now.AddDays(-num);
- for (int id = 1; id <= num; id++)
- {
- list.Add(new PlaceViewModel()
- {
- ID = id,
- Name = String.Format("Place {0} Name", id),
- Location = String.Format("Place {0} Location", id),
- CreatedDate = date.AddDays(id),
- LastModifiedDate = date.AddDays(id),
- ViewCount = num - id
- });
- }
- return list;
- }
-
- private JsonSerializerSettings DefaultJsonSettings
- {
- get
- {
- return new JsonSerializerSettings()
- {
- Formatting = Formatting.Indented
- };
- }
- }
-
- private int DefaultNumberOfItems
- {
- get
- {
- return 5;
- }
- }
-
- private int MaxNumberOfItems
- {
- get
- {
- return 100;
- }
- }
- }
- }
You may have noticed that both classes have the pretty much similar implementation. We could probably merge it in one Controller, but I have decided to split it with different class to value the separation of concerns.
You may also have noticed that both classes have the same private members. We could minimize that by creating a separate class, and implement the code there. But for now, let’s just have it on separate class for easy reference.
Creating the Client-Side ViewModels
We will be using TypeScript to define a set of class for us to work with type definitions. In other words, we will not be dealing with raw JSON data and anonymous objects; instead we will be using typed objects: an actual instance of classes.
Under Scripts/app/viewmodels, add a new TypeScript file and name it as “lounge.ts”. Then copy the following code below:
- export class Lounge {
- constructor(
- public ID: number,
- public Subject: string,
- public Message: string
- ) { }
- }
Add another TypeScript file and name it as “place.ts”. Copy the following code below:
- export class Place {
- constructor(
- public ID: number,
- public Name: string,
- public Location: string
- ) { }
- }
Notice that we’re not adding all the properties that are present in our server-side ViewModel class: as a general rule of the thumb, we’ll be keeping these classes as lightweight as possible, defining only what we need in the UI: we can always add more properties later, as soon as we need them.
These ViewModels will be used as a client-side, TypeScript class to properly map our JSON-serialized server-side ViewModels that is returned from the Web API controller.
Note: The property names should match with the property names you defined in your server-side ViewModels including the casing.
Creating the Client-Side Service
Now, we need to setup a client-service to fetch the required data from the Web API: we’ll do that by issuing a request to the API Controllers we built earlier. We are going to use the Angular Http client to communicate via XMLHttpRequest (XHR), which is a rather complex HTTP-based API that provides client functionality for transferring data between a client and a server.
Create another TypeScript file under “Scripts/app/services” folder and name it as “app.service.ts”. Copy the following code below:
The AppService Class- import { Injectable } from "@angular/core";
- import { Http,Response } from "@angular/http";
- import { Lounge } from "../viewmodels/lounge";
- import { Observable } from "rxjs/Observable";
-
- @Injectable()
- export class AppService {
-
- constructor(private http: Http) { }
-
- private loungeBaseUrl = 'api/lounge/';
- private placeBaseUrl = 'api/place/';
-
- getLatestDiscussion(num?: number) {
- var url = this.loungeBaseUrl + "GetLatestDiscussion/";
- if (num != null) url += num;
- return this.http.get(url)
- .map(response => response.json())
- .catch(this.handleError);
- }
-
- getDiscussion(id: number) {
- if (id == null) throw new Error("id is required.");
- var url = this.loungeBaseUrl + id;
- return this.http.get(url)
- .map(response => <Lounge>response.json())
- .catch(this.handleError);
- }
-
- getLatestEntries(num?: number) {
- var url = this.placeBaseUrl + "GetLatestEntries/";
- if (num != null) url += num;
- return this.http.get(url)
- .map(response => response.json())
- .catch(this.handleError);
- }
-
- getMostViewed(num?: number) {
- var url = this.placeBaseUrl + "GetMostViewed/";
- if (num != null) url += num;
- return this.http.get(url)
- .map(response => response.json())
- .catch(this.handleError);
- }
-
- getPlace(id: number) {
- if (id == null) throw new Error("id is required.");
- var url = this.placeBaseUrl + id;
- return this.http.get(url)
- .map(response => <Lounge>response.json())
- .catch(this.handleError);
- }
-
- private handleError(error: Response) {
- console.error(error);
- return Observable.throw(error.json().error || "Server error");
- }
-
- }
Notice that the code above pretty much resembles our Web API Controllers methods, except that we have merged all the calls to our API within it. This will be the class that our client will use to fetch the data from our WebAPI Controller itself.
Keep in mind that we make use of the Injectable decorator, declaring that the service is an Injectable class: doing that will attach to our class a set of meta data that will be consumed by the DI system upon instantiation. Basically what we’re doing here is telling the DI injector that the constructor parameter(s) should be instantiated by using their declared type(s). The TypeScript code allows a very fluent syntax to achieve this result at constructor level, as it can be seen in the following line:
- constructor(private http: Http) { }
Creating the Angular 2 Components
The next thing that we are going to do is to create a dedicated component for serving different contents. Let’s start by implementing the “Latest Discussion” master-detail components.
The Lounge List Component
Create a new TypeScript file under “Scripts/app/components/lounge” and name the file as “lounge-list.component.ts”. Copy the following code below within that file:
- import {Component, OnInit} from "@angular/core";
- import {HTTP_PROVIDERS} from "@angular/http";
- import {Router} from "@angular/router";
- import {Lounge} from "../../viewmodels/lounge";
- import {AppService} from "../../services/app.service";
- import {LoungeDetailComponent} from "./lounge-detail.component";
-
- @Component({
- selector: "lounge-list",
- template: `
- <h2>{{title}}</h2>
- <ul class="items">
- <li *ngFor="let item of items"
- [class.selected]="item === selectedItem"
- (click)="onSelect(item)">
- <span>{{item.Subject}}</span>
- </li>
- </ul>
- `,
- styles: [`
- ul.items li {
- cursor: pointer;
- }
- ul.items li:hover {
- background-color: #E8FAEC;
- }
- `],
- directives: [LoungeDetailComponent],
- providers: [
- HTTP_PROVIDERS,
- AppService
- ]
- })
-
- export class LoungeListComponent implements OnInit {
- title: string;
- selectedItem: Lounge;
- items: Lounge[];
- errorMessage: string;
-
- constructor(private AppService: AppService, private router: Router) { }
-
- ngOnInit() {
- this.title = "The Lounge";
- var service = this.AppService.getLatestDiscussion();
-
- service.subscribe(
- items => this.items = items,
- error => this.errorMessage = <any>error
- );
-
- }
-
- onSelect(item: Lounge) {
- this.selectedItem = item;
- var link = ['/lounge', this.selectedItem.ID];
- this.router.navigate(link);
- }
- }
The
LoungeListComponent will display the latest list of discussions from our mocked data, which is returned from the Web API call. Let’s talk a bit about what we did there:
At the top of the file, we have imported the Angular classes that we need: since we’re creating a Component, we need the Component base class by referencing the "@angular/core", and we also need to implement the OnInit interface because our component needs to execute something upon its initialization. As for the HTTP_PROVIDERS, these are a set of injectables that are required any time we need to use the Angular built-in Http service. We have referenced the Angular Router to make use of client-side navigation because we are going to need it for navigating to the details component. We have also referenced the service that we have created earlier to communicate with our server to get some data. Finally, we have imported the lounge-detail component for displaying the details content which we will be creating soon enough.
The
@component block is where we setup the UI for our Component, including the selector, template and styles. Notice that we used a bit of Angular 2 Template Syntax there, in order to get the job done. Specifically, we used a master template, a ngFor directive, a property binding and an event binding. Note that we can also decouple the template and styles in a separate file by defining the templateUrl and styleUrls. It is also where we define the directives and providers that the Component needs.
The
LoungeListComponent is a class written in TypeScript. This class contains some properties, a constructor which makes use of DI to instantiate the
AppService and
Router objects. It also composed of methods that will be used in the component itself, specifically in the Angular 2 template manipulation, and of course in the client-side routing. The
ngOnInit() method is where we get the data from the service which fires on initialization of the component. The
onSelect() method takes a Lounge object as the parameter. This is where we get the selected item and pass it to the details component by making use of Angular 2 routing. For information about Angular 2 Templates, read on:
Template Syntax
The LoungeDetail Component
Create another TypeScript component within “Scripts/app/components/lounge” and name the file as “lounge-detail.component.ts”. Now copy the following code below within that file:
- import {Component, OnInit, OnDestroy} from "@angular/core";
- import {HTTP_PROVIDERS} from "@angular/http";
- import {Router, ActivatedRoute} from "@angular/router";
- import {AppService} from "../../services/app.service";
- import {Lounge} from "../../viewmodels/lounge";
-
- @Component({
- selector: "lounge-detail",
- template: `
- <div *ngIf="item" class="item-details">
- <h2>{{item.Subject}} - Detail View</h2>
- <ul>
- <li>
- <label>Subject:</label>
- <input [(ngModel)]="item.Subject" placeholder="Insert the title..."/>
- </li>
- <li>
- <label>Message:</label>
- <textarea [(ngModel)]="item.Message" placeholder="Insert a message..."></textarea>
- </li>
- </ul>
- </div>
-
- <div>
- <button (click)='onBack()'>Back to Home</button>
- </div>
- `,
- providers: [
- HTTP_PROVIDERS,
- AppService
- ],
- styles: [`
- .item-details {
- margin: 5px;
- padding: 5px 10px;
- border: 1px solid 9BCCE0;
- background-color: #DDF0D5;
- width: 500px;
- }
- .item-details * {
- vertical-align: middle;
- }
- .item-details ul li {
- padding: 5px 0;
- }
- `]
- })
-
- export class LoungeDetailComponent implements OnInit {
- item: Lounge;
- sub: any;
-
- constructor(private AppService: AppService,
- private router: Router,
- private route: ActivatedRoute) { }
-
- ngOnInit() {
- this.sub = this.route.params.subscribe(params => {
- var id = +params['id'];
- console.log("selected id " + id);
- this.AppService.getDiscussion(id).subscribe(item => this.item = item[0]);
- });
- }
-
- ngOnDestroy() {
- this.sub.unsubscribe();
- }
-
- onBack(): void {
- this.router.navigate(['/home']);
- }
- }
Just like what we did in the lounge-list.component.ts file, we are importing what we need for this specific component. Within template, noticed that we've used the Angular 2
ngIf directive to hide the DOM elements when the property item is null or no data associated on it. We also used the Angular 2
ngModel directive to implement a two-way data binding syntax for textarea element. This could simply mean that any changes made in the binded element will automatically update the model itself and vice-versa.
We also applied some simple styles to our HTML by defining the @component's styles attribute just to beautify a bit our rendered markup.
The
LoungeDetailComponent class implements the logic for this particular component. We have defined a few properties and make use of constructor injection (DI) to initialize objects what we need for the class implementation. This class takes the id parameter from the params observable in the
ActivatedRoute service and use the AppService to fetch the data with that id. You should be able to see how we did that under the
ngOnInit() method. The
ngOnDestroy() method is responsible for cleaning up the params subscription. Finally, we have implemented an
onBack() method to let user navigate back to the Home component view. This event is attached in the Button element defined in our HTML template using Angular 2 event binding.
At this point, we’re done with our basic Lounge master-detail navigation and data binding. Now, let’s do the same to implement the “What’s New?” and “Top Places to Visit” master-detail components.
The PlaceList Component
Create a new TypeScript file under “Scripts/app/components/explore” and name the file as “place-list.component.ts”. Copy the following code below within that file:
- import {Component, Input, OnInit} from "@angular/core";
- import {HTTP_PROVIDERS} from "@angular/http";
- import {Router} from "@angular/router";
- import {Place} from "../../viewmodels/place";
- import {AppService} from "../../services/app.service";
- import {PlaceDetailComponent} from "./place-detail.component";
-
- @Component({
- selector: "place-list",
- template: `
- <h2>{{title}}</h2>
- <ul class="items">
- <li *ngFor="let item of items"
- (click)="onSelect(item)">
- <span>{{item.Name}}</span>
- </li>
- </ul>
- `,
- styles: [`
- ul.items li {
- cursor: pointer;
- }
- ul.items li:hover {
- background-color: #E8FAEC;
- }
- `],
- directives: [PlaceDetailComponent],
- providers: [
- HTTP_PROVIDERS,
- AppService
- ]
- })
-
- export class PlaceListComponent implements OnInit {
- @Input() class: string;
- title: string;
- items: Place[];
- errorMessage: string;
-
- constructor(private AppService: AppService, private router: Router) { }
-
- ngOnInit() {
-
- var service = null;
- switch (this.class) {
- case "latest":
- default:
- this.title = "What's New?";
- service = this.AppService.getLatestEntries();
- break;
- case "most-viewed":
- this.title = "Top Places to Visit";
- service = this.AppService.getMostViewed();
- break;
- }
-
- service.subscribe(
- items => this.items = items,
- error => this.errorMessage = <any>error
- );
- }
-
- onSelect(item: Place) {
- var link = ['/explore', item.ID];
- this.router.navigate(link);
- }
-
- }
The code above has similar implementation with our LoungeListComponent except that we did something different in the ngOnInit() method. We have implemented a switch statement to determine which data to load in the component by passing the class variable as the parameter. The class variable is defined using an @Input() decorator function (@Input() class: string;), that will add the required metadata to make this property available for property binding. We need to do that because we expect this property to be populated from a binding expression within a parent component ~ the Home Component. We did it like that to make this component more reusable and maintainable since the data for “What’s New?” and “Top Places to Visit” components are coming from the same data source.
The PlaceDetail Component
The details for “What’s New?” and “Top Places to Visit” components will also share the same Detail-View component. Let’s go ahead and create it. Add a new TypeScript file on the same folder and name it as “place-detail.component.ts”. Now copy the following code below:
- import {Component, OnInit, OnDestroy} from "@angular/core";
- import {HTTP_PROVIDERS} from "@angular/http";
- import {Router, ActivatedRoute} from "@angular/router";
- import {AppService} from "../../services/app.service";
- import {Place} from "../../viewmodels/place";
-
-
- @Component({
- selector: "place-detail",
- template: `
- <div *ngIf="item" class="item-details">
- <h2>{{item.Name}} - Detail View</h2>
- <ul>
- <li>
- <label>Subject:</label>
- <input [(ngModel)]="item.Name" placeholder="Insert the name..."/>
- </li>
- <li>
- <label>Message:</label>
- <textarea [(ngModel)]="item.Location" placeholder="Insert a location..."></textarea>
- </li>
- </ul>
- </div>
-
- <div>
- <button (click)='onBack()'>Back to Home</button>
- </div>
- `,
- providers: [
- HTTP_PROVIDERS,
- AppService
- ],
- styles: [`
- .item-details {
- margin: 5px;
- padding: 5px 10px;
- border: 1px solid 9BCCE0;
- background-color: #DDF0D5;
- width: 500px;
- }
- .item-details * {
- vertical-align: middle;
- }
- .item-details ul li {
- padding: 5px 0;
- }
- `]
- })
-
- export class PlaceDetailComponent implements OnInit {
- item: Place;
- sub: any;
-
- constructor(private AppService: AppService,
- private router: Router,
- private route: ActivatedRoute) { }
-
- ngOnInit() {
- this.sub = this.route.params.subscribe(params => {
- var id = +params['id'];
- this.AppService.getPlace(id).subscribe(item => this.item = item[0]);
- });
- }
-
- ngOnDestroy() {
- this.sub.unsubscribe();
- }
-
- onBack(): void {
- this.router.navigate(['/home']);
- }
- }
The code above is pretty much the same as what we did for implementing the LoungeDetailComponent, so there’s really not much to talk about.
The Home Component
Create a new TypeScript file under Scripts/app/components/home and name the file as “home.component.ts”. Copy the following code below within that file:
- import {Component} from "@angular/core";
- import {LoungeListComponent} from "../lounge/lounge-list.component";
- import {PlaceListComponent} from "../explore/place-list.component";
-
- @Component({
- selector: "home",
- template:`
- <div class="div-wrapper">
- <div class="div-lounge">
- <div>
- <lounge-list class="discussion"></lounge-list>
- </div>
- </div>
- <div class="div-explore">
- <div class="top">
- <place-list class="latest"></place-list>
- </div>
- <div class="bot">
- <place-list class="most-viewed"></place-list>
- </div>
- </div>
- <div class="div-clear"></div>
- </div>
- `,
- directives: [
- LoungeListComponent,
- PlaceListComponent
- ],
- styles: [`
- .div-wrapper {
- margin-right: 300px;
- }
- .div-lounge {
- float: left;
- width: 100%;
-
- }
- .div-lounge div{
- margin:0 0 10px 0;
- border: 1px solid #9BCCE0;
- background-color: #DDF0D5;
- }
- .div-explore {
- float: right;
- width: 300px;
- margin-right: -300px;
- }
- .div-explore .top{
- margin:0 10px 10px 10px;
- border: 1px solid #9BCCE0;
- background-color: #DDF0D5;
- }
- .div-explore .bot{
- margin:10px 10px 10px 10px;
- border: 1px solid #9BCCE0;
- background-color: #DDF0D5;
- }
- .div-clear {
- clear: both;
- }
-
- `]
- })
-
- export class HomeComponent {}
The
HomeComponent will serve as our master page for displaying the list of data from various components. Notice that we've imported the
LoungeListComponent and
PlaceListComponent files so we can integrate those components within our Home component. We added the
<lounge-list> element to display the "Latest Discussion”. Also, we’ve added the
<place-list> element to display the “What’s New?” and “Top Places to Visit” list with a standard class attribute to uniquely identify each one of them. The class attribute defined in the element will be used as the target of a property binding: We were referring to the
@Input() decorator attribute which we defined in the
PlaceListComponent.
We also use the class attribute to uniquely define a sets of styles for each elements: As we can see from the Angular 2 template's styles section above.
Enabling Client-Side Routing
At this point, we’ve done implementing the needed components for our Angular 2 application yet it has a few major issues. The navigation we’ve setup in our master-detail components will not work since the URL routes defined from our components doesn’t exist yet. Also, we need to configure our app to enable client-side routing. It’s time for us to connect the dots in the picture to achieve what we expect.
Creating the App.Routes
Create a new TypeScript file under “/Scripts/app” folder and name it as “app.routes.ts”. Copy the following code below:
- import { provideRouter, RouterConfig } from "@angular/router";
- import { HomeComponent } from "./components/home/home.component";
- import { LoungeDetailComponent } from "./components/lounge/lounge-detail.component";
- import { PlaceDetailComponent } from "./components/explore/place-detail.component";
-
- const routes: RouterConfig = [
- {
- path: '',
- redirectTo: '/home',
- pathMatch: 'full'
- },
- {
- path: 'home',
- component: HomeComponent
- },
- {
- path: 'lounge/:id',
- component: LoungeDetailComponent
- },
- {
- path: 'explore/:id',
- component: PlaceDetailComponent
- }
- ];
-
- export const appRouterProviders = [
- provideRouter(routes)
- ];
Let’s see what we just did there.
Just like the other regular components, we need to import the module/directives that the component needs. It this case, we’ve imported the “@angular/router” to make use of the Angular 2 router interface: Specifically, we need to use and declare routes by defining a RouterConfig and define an export to add the router to our bootstrap.
The
RouterConfig is an array of route definitions. A route is typically composed of two main parts: The path and component. From the code above, we have defined some routes that we need for this particular series. The first route indicates our default component that is targeting to our Home component. The rest of the routes are pointing to our components we created earlier.
Making the Routes Available
Let’s modify the boot.ts file to make the routes available to any components that needs it.
-
- import {AppComponent} from "./components/app.component";
- import {bootstrap} from "@angular/platform-browser-dynamic";
- import { appRouterProviders } from "./app.routes";
- import 'rxjs/Rx';
-
- bootstrap(AppComponent, [appRouterProviders]);
What we did there is we imported the appRouterProviders that we have defined in our “app.routes.ts” which contains our configured router and make it available to the application by adding it to the bootstrap array.
Modifying the App Component
Now, we need to add an anchor tag to the template which, when clicked, triggers navigation to the Components. To do that, open "app.component.ts" file and update the code so it would look something like this:
- import {Component} from '@angular/core';
- import {ROUTER_DIRECTIVES} from '@angular/router';
- import {HomeComponent} from './home/home.component';
-
- @Component({
- selector: "angularjs2demo",
- template: `
- <h1>{{title}}</h1>
- <h3>{{subTitle}}</h3>
- <div class="menu">
- <a class="home" [routerLink]="['/home']">Home</a> |
- </div>
- <router-outlet></router-outlet>
- `,
- directives: [
- ROUTER_DIRECTIVES,
- HomeComponent
- ]
- })
-
-
- export class AppComponent {
- title = "The Backpackers' Lounge";
- subTitle = "For geeks who want to explore nature beyond limits.";
- }
At this point, we only need to import the Home component. The [routerLink] binding in the anchor tag tells the router where to navigate when a user clicks a specific link. The <router-outlet> tag acts as the placeholder element for the component that will be injected into the page DOM by the routing engine whenever the associated route is visited. Notice that we placed it right after our new navigation menu, so it will keep its position as the page content changes.
Adding the base Tag
Open “wwwroot/index.html” and add
<base href="/"> at the very top within the
<head> section. Our updated index.html file should now look something like this:
- <html>
-
- <head>
- <base href="/">
- <title>ASP.NET Core RTM with Angular 2 RC4 Demo</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
-
-
- <script src="js/shim.min.js"></script>
- <script src="js/zone.js"></script>
- <script src="js/Reflect.js"></script>
- <script src="js/system.src.js"></script>
-
- <script src="systemjs.config.js"></script>
- <script>
- System.import('app').catch(function(err) {
- console.error(err);
- });
- </script>
- </head>
-
-
- <body>
-
- <angularjs2demo>Please wait...</angularjs2demo>
- </body>
-
- </html>
We need to set the base tag as it will tell the routing engine how to compose all of the upcoming navigation URLs our app will eventually have. For details, see:
Router Base HREF
Rewriting
The last and final step is to handle rewrites in our web.config file. We need to tell the Web Server to rewrite all routing URLs, including the root one, to the index.html file by adding the following lines to the
<system.webServer> section of web.config:
- <rewrite>
- <rules>
- <rule name="Angular2 pushState routing" stopProcessing="true">
- <match url=".*" />
- <conditions logicalGrouping="MatchAll">
- <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
- <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
- <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" />
- <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" /> </conditions>
- <action type="Rewrite" url="/index.html" /> </rule>
- </rules>
- </rewrite>
By implementing the rules above, we’re basically asking our Web Server to re-address any incoming request to the /index.html file, with the sole exception of those pointing to:
- Any existing file (to preserve references to actual .js, .css, .pdf, image files & more).
- Any existing folder (to preserve references to actual, potentially-browsable and/or Angular-unrelated subfolders).
- Anything within the /api/ folder (to preserve any call to our Web API Controllers).
Running the App
Now try to compile and rebuild your project and make sure that your gulp task is running. Hit F5 and you should be able to see something like this as the output:
Notice that the URL changes after clicking an item from the list and routes it to the corresponding details view. You can also see that the data changes automatically as you type: That’s how the two-way data binding works in Angular 2.
Tips
If your changes do not reflect on the browser, then it’s probably a caching issue. To resolve that, add the following configuration below under <system.webServer> element in your web.config file:
- <caching enabled="false"/>
For ASP.NET Core applications in VStudio 2015, configuring TypeScript is a bit challenging. The tsconfig.json will not be honored because the project file (.xproj) takes precedence. So if you are getting the following TypeScript error or warnings below:
- TS1219 Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option to remove this warning
- TS2307 Cannot find module '@angular/core'.
Until the tooling around TypeScript improves, you can configure TypeScript manually:
Step 1: Right-click project, and Unload Project
Step 2: Right-click the unloaded project, and Edit the .xproj file
Step 3: Add a PropertyGroup node, under the Project node:
- <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
- <TypeScriptTarget>ES5</TypeScriptTarget>
- <TypeScriptJSXEmit>None</TypeScriptJSXEmit>
- <TypeScriptCompileOnSaveEnabled>True</TypeScriptCompileOnSaveEnabled>
- <TypeScriptNoImplicitAny>False</TypeScriptNoImplicitAny>
- <TypeScriptModuleKind>System</TypeScriptModuleKind>
- <TypeScriptRemoveComments>False</TypeScriptRemoveComments>
- <TypeScriptOutFile />
- <TypeScriptOutDir />
- <TypeScriptGeneratesDeclarations>False</TypeScriptGeneratesDeclarations>
- <TypeScriptNoEmitOnError>True</TypeScriptNoEmitOnError>
- <TypeScriptSourceMap>True</TypeScriptSourceMap>
- <TypeScriptMapRoot />
- <TypeScriptSourceRoot />
- <TypeScriptExperimentalDecorators>True</TypeScriptExperimentalDecorators>
- <TypeScriptEmitDecoratorMetadata>True</TypeScriptEmitDecoratorMetadata>
- </PropertyGroup>
Step 4: Right-click the unloaded project and Reload Project
Step 5: Re-build project
Summary
We’ve learned a lot of things in this part of the series, starting from upgrading to Angular 2 RTM down to creating a data-driven Angular2 app from scratch within the context of ASP.NET Core. We learned how to create and communicate with Web API in our Angular 2 app. We’ve also learned the basics of Angular 2 Master-Detail implementation, Two-Way Data binding and Routing.
Stay tuned for my next article.