Typescript

Identity Server: Calling Secured API from Angular

This post is a continuation of a series of posts that follow my initial looking into using IdentityServer4 in ASP.NET Core with an API and an Angular front end. The following are the related posts.

Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup
Identity Server: Interactive Login using MVC
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data
Identity Server: Usage from Angular

This post is going to take the solution from last week, the code can be found here, and add an example of the Client Application (Angular) calling an endpoint on the API Application that requires a user with permissions.

API Application

To provide an endpoint to call with minimal changes this example just moves the SampleDataController from the Client Application to the API Application. The following is the full class.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace WebApplicationBasic.Controllers
{
    [Route("api/[controller]")]
    [Authorize]
    public class SampleDataController : Controller
    {
        private static string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", 
            "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        [HttpGet("[action]")]
        public IEnumerable<WeatherForecast> WeatherForecasts()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5)
                             .Select(index => new WeatherForecast
            {
                DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            });
        }

        public class WeatherForecast
        {
            public string DateFormatted { get; set; }
            public int TemperatureC { get; set; }
            public string Summary { get; set; }

            public int TemperatureF
            {
                get
                {
                    return 32 + (int)(TemperatureC / 0.5556);
                }
            }
        }
    }
}

Make special note that this class now has the Authorize attribute applied which is the only change that was made when moving the file from the Client Application. This attribute is what will require an authorized user for all the routes this controller services.

Client Application

In the Client Application, the first step is to remove the SampleDataController since it is now in the API Application.

Next, in the app.module.client.ts file, add a new provider which can be used to supply the URL of the API to the rest of the Client Application. Don’t take this as best practices for injecting configuration data it is just an easy way to handle it in this application. The following is the full class without the imports (which haven’t changed) the new item is the API_URL.

@NgModule({
    bootstrap: sharedConfig.bootstrap,
    declarations: sharedConfig.declarations,
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        ...sharedConfig.imports
    ],
    providers: [
        { provide: 'ORIGIN_URL', useValue: location.origin },
        { provide: 'API_URL', useValue: "http://localhost:5001/api/" },
        AuthService, AuthGuardService, GlobalEventsManager
    ]
})
export class AppModule {
}

Now for the changes that need to be made to the FetchDataComponent which is the class that will call the new API endpoint. First, add an import for the AuthService.

import { AuthService } from '../services/auth.service';

Next, there are a couple of changes to the signature of the constructor. The first is to use ‘API_URL’ instead of ‘ORIGIN_URL’. The second is to provide for injection of the AuthService. The following is a comparison between the version of the constructor signature.

Before:
constructor(http: Http, @Inject('ORIGIN_URL') originUrl: string)

After:
constructor(http: Http, @Inject('API_URL') apiUrl: string, authService: AuthService)

The final change is to use authService.AuthGet with the new URL instead of http.get.

Before:
http.get(originUrl + 
         '/api/SampleData/WeatherForecasts').subscribe(result => {
    this.forecasts = result.json() as WeatherForecast[];
});

After:
authService.AuthGet(apiUrl + 
                    '/SampleData/WeatherForecasts').subscribe(result => {
    this.forecasts = result.json() as WeatherForecast[];
});

With the above changes, the user has to be logged in or the API will respond with not authorized for the weather forecasts end point. The Client Application doesn’t have anything to provide the user with the fact they aren’t authorized at the moment, but that is outside the scope of this entry.

So far we haven’t look at the code in the AuthService class, but I do want to explain what the AuthGet function is doing and the related functions for put, delete, and post. These calls are wrappers around the standard Angular HTTP library calls that add authorization headers based on the logged in user. The following is the code of the AuthGet as well as two helper functions the class uses to add the headers.

AuthGet(url: string, options?: RequestOptions): Observable<Response> {
    if (options) {
      options = this._setRequestOptions(options);
    }
    else {
      options = this._setRequestOptions();
    }
    return this.http.get(url, options);
}

private _setAuthHeaders(user: User) {
    this._authHeaders = new Headers();
    this._authHeaders.append('Authorization', 
                             user.token_type + " " + user.access_token);
    this._authHeaders.append('Content-Type', 'application/json');
}

private _setRequestOptions(options?: RequestOptions) {
    if (options) {
      options.headers.append(this._authHeaders.keys[0],
                             this._authHeaders.values[0]);
    }
    else {
      //setting default authentication headers
      this._setAuthHeaders(this._currentUser);
      options = new RequestOptions({ headers: this._authHeaders, 
                                     body: "" });
    }
    return options;
}

Wrapping up

It feels like this application is finally getting to the point where other development could happen if it were more than a demo, which is exciting. My thought on how this could be used for real applications is the Identity Application would stand on its own and be used by many clients. The Client Application with a few more tweaks could be used as a template for Angular applications. The completed code can be found here.

This post finishes up the core of what I set out to learn about IdentityServer, but there could be more related posts as I continue to add some polish to the current implementation of the sample solution.

Identity Server: Calling Secured API from Angular Read More »

Identity Server: Usage from Angular

This post is a continuation of a series of posts that follow my initial looking into using IdentityServer4 in ASP.NET Core with an API and an Angular front end. The following are the related posts.

Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup
Identity Server: Interactive Login using MVC
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data
Identity Server: Usage from Angular (this post)

This post is finally going to add login from Angular in the Client Application. It has been a long time coming and will be a starting point, based on a few examples I found which I will list at the end. The starting point of the code can be found here.

API Application

In order for the Client Application to be able to call the API Application, there are some changes needed to allow cross-origin resource sharing. For more details check out this post only the basics will be covered here. First, add the following NuGet package.

  • Microsoft.AspNetCore.Cors

Next, in the ConfigureServices function of the Startup class add AddCors before AddMvc. The following is the full function. This allows calls to the API Application from the Client Application which is running on localhost on port 5002.

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy("default", policy =>
        {
            policy.WithOrigins("http://localhost:5002")
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
    });

    services.AddMvc();
}

Then, in the Configure function add app.UseCors(“default”); to make sure the default policy defined above is enforced. The following is the full function.

public void Configure(IApplicationBuilder app, 
                      IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseCors("default");

    app
      .UseIdentityServerAuthentication(
      new IdentityServerAuthenticationOptions
    {
        Authority = "http://localhost:5000",
        RequireHttpsMetadata = false,
        ApiName = "apiApp"
    });

    app.UseMvc();
}

Identity Application

The Identity Application doesn’t have a lot of changes, but some of the configuration between it and the Client Application is what took me the bulk of time getting the code for this post setup and going.

If you are keeping up with the series then last you will know last week all the configuration data was moved to a database using Entity Framework Core. This is a bit of a problem now that I need to add a new client and the configuration data doesn’t any associated UI. To work around this I just added the new client to the Config class in the GetClients function and then deleted the existing database and let Entity Framework recreate it based on the new seed data. Not optimal, but I didn’t want to complicate things by adding a UI for the client setup. The following is the new client.

new Client
{
    ClientId = "ng",
    ClientName = "Angular Client",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowAccessTokensViaBrowser = true,
    RequireConsent = true,

    RedirectUris = { "http://localhost:5002/callback" },
    PostLogoutRedirectUris = { "http://localhost:5002/home" },
    AllowedCorsOrigins = { "http://localhost:5002" },

    AllowedScopes =
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "apiApp"
    },

}

There are a few things in this configuration that took me some time to get right. First of all best, I have been able to tell with this style of application implicit flow is the way to go which is handled by using a GrantTypes.Implicit for the AllowedGrantTypes.

The next issues I ran was a cross-origin resource sharing issue. Thankfully IdentityServer makes it easy to specify what origins should be allowed using the AllowedCorsOrigins property. In this example, we want to allow requests from the URL of our Client Application which is http://localhost:5002.

The last issue I had on was with the URIs I had set. The configuration in IdentityServer needs to exactly match the setup in the Client Application or you will have issues. I also had trouble trying to use the raw base address (http://localhost:5002) as the PostLogoutRedirectUris so look out for that as well.

Client Application

In the client application open the package.json file and add the following the dependencies section.

"oidc-client": "1.3.0",
"babel-polyfill": "6.23.0"

I also updated the typescript version to 2.3.4. Be cautious when changing the version of typescript as there is an issue with Angular and typescript 2.4.x at the moment.

At this point in the process, I had to find some resources on how to continue. The following are the ones I leaned on most.

ANGULAR OPENID CONNECT IMPLICIT FLOW WITH IDENTITYSERVER4
ASP.NET Core & Angular2 + OpenID Connect using Visual Studio Code
Repo for the previous link
Repo for with example Angular OidcClient

Getting this part of the application working involved a lot of changes and instead of going in depth on everything I am going to recommend just copying in the following files for the finished example code and dig more into them after you get an example working. Here is the list of files.

  • ClientApp/ClientApp/app/components/callback/callback.component.ts
  • ClientApp/ClientApp/app/components/services/ – whole directory
  • ClientApp/ClientApp/boot-server.ts – related to a typescript error only if needed

With the above files in place, we will now focus on using the functionality they provide to log in and protect routes. To begin  app.module.client.ts, app.module.server.ts and app.module.shared.ts all need the next set of changes. I haven’t tried it yet, but I bet this change could just be made in the shared file and used in the other two. Add the following imports.

import { AuthService } from './components/services/auth.service';
import { GlobalEventsManager } from './components/services/global.events.manager';
import { AuthGuardService } from './components/services/auth-guard.service';

Next, add the same three items to the array of providers (or add one if it doesn’t exist). The following is an example from the shared file.

 @NgModule({
     bootstrap: sharedConfig.bootstrap,		     
     declarations: sharedConfig.declarations,		     
     imports: [
         ServerModule,
         ...sharedConfig.imports
     ],
     providers: [
        AuthService, AuthGuardService, GlobalEventsManager 
     ]
 })

Finally, in the shared file change any routes that you would like to require the user to be logged in to be like the following which utilizes the canActivate of the route.

{ path: 'fetch-data', 
  component: FetchDataComponent, 
  canActivate:[AuthGuardService]  }

In the navmenu.component.html which is the UI for the navigation menu add the following two options to the unordered list.

<li *ngIf="!_loggedIn" [routerLinkActive]="['link-active']">
    <a (click)="login()" [routerLink]="['/login']">
       <span class="glyphicon glyphicon-user"></span> Login
    </a>
</li>
<li *ngIf="_loggedIn" [routerLinkActive]="['link-active']">
    <a (click)="logout()" [routerLink]="['/logout']">
       <span class='glyphicon glyphicon-log-out'></span> Logout
    </a>
</li>

The user will only ever see one of the above options based on being logged in or not which is what the *ngIf is doing.

The navigation view model (navmenu.component.ts) changed a bit more. The following is the complete file.

import { Component } from '@angular/core';
import { AuthService } from '../services/auth.service'
import { GlobalEventsManager } from '../services/global.events.manager'

@Component({
    selector: 'nav-menu',
    templateUrl: './navmenu.component.html',
    styleUrls: ['./navmenu.component.css']
})
export class NavMenuComponent {
    public _loggedIn: boolean = false;

    constructor (
      private _authService: AuthService,
      private _globalEventsManager: GlobalEventsManager) {
          _globalEventsManager.showNavBarEmitter.subscribe((mode)=>{
            // mode will be null the first time it is created, so you need to igonore it when null
            if (mode !== null) {
                console.log("Global Event, sent: " + mode);
                this._loggedIn = mode;
            }
        });
  }

  public login(){
      this._authService.startSigninMainWindow();
  }

  public logout(){
      this._authService.startSignoutMainWindow();
  }
}

New imports were added for the AuthService and GlobalEventsManager which get injected into the constructor of the class. The class also contains a _loggedIn property to track if the user is logged in or not. Finally, functions were added for login and logout to go with the two new links shown in the navigation.

Wrapping up

With the above, the Client Application can now log a user in and out utilizing IdentityServer from Angular. There are a lot of details in the files we just copied, but with a working sample, it is much easier to examine/customize how the process is being handled. Check back next week to see how to call the API Application from the Angular part of the Client Application.

The completed code can be found here.

Identity Server: Usage from Angular Read More »

Identity Server: Sample Exploration and Initial Project Setup

This post will be a continuation of my exploration around Identity Server which was started with this post which was more of an overview of the space and my motivations for learning about Identity Server. There were a lot of things that were unclear to me as I first started looking through the samples so this post is going to communicate so of those issues and hopefully clear them up for you as well.

After this post, the follow-up post should be more focused on one thing instead of trying to cover so much information in on go.

The following are all the posts in this series.

Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup (this post)
Identity Server: Interactive Login using MVC
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data
Identity Server: Usage from Angular

Typical Sample Solution Structure

I started my exploration with the IdentityServer4.Sample repo specifically in the Quickstarts folder. For me, this was a mistake as I didn’t have a good enough grasp on the larger concepts for the code to provide proper guidance. Big surprise here, but using the docs and actually walking through the project creation was very helpful.

The code associated with this blog can be found here. The solution contains three projects.

  • ApiApp – Backend for the application and is a resource that is will require authorization to access. The API will be an ASP.NET Core Web API.
  • ClientApp – Frontend application that will be requesting authorization. This is an ASP.NET Core application that is hosting an Angular (4) app.
  • IdentityApp – This is ASP.NET Core application that is the IdentityServer and will end up authorizing users and issuing tokens for resources.

Identity Application

For the Identity application, create an ASP.NET Core Web Application using the empty template targeting at least ASP.NET Core 1.1. Next, using NuGet install the IdentityServer4 NuGet package.

Now that the IdentityServer4 NuGet package is installed open up the Startup class and add the following to the ConfigureServices function.

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServer()
        .AddTemporarySigningCredential()
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients());
}

The above registers IdentityServer with ASP.NET Core as a service available via dependency injection using temporary and in-memory components as a stand in for testing. The Config class used here will be added a bit later.

Next, the Configure function should look like the following.

public void Configure(IApplicationBuilder app, 
                      IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseDeveloperExceptionPage();
    app.UseIdentityServer();
}

The above is a basic Configure function. app.UseIdentityServer() is the only the only bit related to IdntityServer and it is adding it to the application’s pipeline.

The final part of this application is the Config class which is used to define the in memory test resources for this application. As you can see below it is defining both API resources and Clients. In the future theses, items would be pulled from a datastore.

public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("apiApp", "API Application")
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "clientApp",

                // no interactive user, use the clientid/secret for authentication
                AllowedGrantTypes = GrantTypes.ClientCredentials,

                // secret for authentication
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },

                // scopes that client has access to
                AllowedScopes = { "apiApp" }
            }
        };
    }
}

API Application

For the API application, create an ASP.NET Core Web Application using the Web API template targeting at least ASP.NET Core 1.1. Next, using NuGet install the IdentityServer4.AccessTokenValidation NuGet package.

Ater the above NuGet package installed open up the Startup class. In the Configure function, the IdentityServer middleware needs to be added to the application pipeline before MVC using the app.UseIdentityServerAuthentication function. The following is the full Configure function.

public void Configure(IApplicationBuilder app, 
                      IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
    {
        Authority = "http://localhost:5000",
        RequireHttpsMetadata = false,
        ApiName = "apiApp"
    });

    app.UseMvc();
}

Notice that the address of the of the authority must be specified (this will need to be the address the Identity Application is running on) as well as the ApiName matches the API Resource we added in the Config class of the Identity Application.

Next, in following the IdentityServer quickstart docs add a new IdentityController to the project. Just to be 100% clear this is just a test endpoint to show how to require authorization on a controller and isn’t something that is required to use IdentityServer. The controller has a single Get that returns the type and value of all the user’s claims.

[Route("api/[controller]")]
[Authorize]
public class IdentityController : Controller
{
    [HttpGet]
    public IActionResult Get()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

The [Authorize] attribute on the class is the bit that requires calls to this endpoint to have authorization. Keep in mind that the same attribute can be left off the class level and added to specific functions if the whole controller doesn’t require authorization.

Adding the [Authorize] attribute means that the IdentityServer middleware we added in the Startup class will validate the token associated with the request to make sure it is from a trusted issues and that it is valid for this API.

Client Application

For the Client application, I deviated from the samples a bit. Instead of just creating a new MVC application I used JavaScriptServices to generate an Angular (4) application. If you want detail on how that is done you can check out this post (yes it says Angular 2, but the newest version of JavaScriptServices now outputs Angular 4 and the steps haven’t changed). An Angular application is my end goal and why I made this deviation from the samples.

After the Client application has been created use NuGet to add the IdentityModel package. This package is to make interacting with the Identity Application simpler.

For this first go instead of actually interacting with the Identity Application from Angular I will be using it from MVC instead. The detail of interaction from Angular will come in a later post. The IdentityController is what does the interaction with both the Identity Application and the API Application interactions in this version of the client application. The following is the full IdentityController class.

public async Task<IActionResult> Index()
{
  var discovery = 
      await DiscoveryClient.GetAsync("http://localhost:5000");

  var tokenClient = new TokenClient(discovery.TokenEndpoint, 
                                    "clientApp", 
                                    "secret");
  var tokenResponse = 
      await tokenClient.RequestClientCredentialsAsync("apiApp");

  ViewData["tokenResult"] = tokenResponse.IsError 
                            ? tokenResponse.Error 
                            : tokenResponse.Json.ToString();

  var client = new HttpClient();
  client.SetBearerToken(tokenResponse.AccessToken);

  var apiResponse = 
      await client.GetAsync("http://localhost:5001/api/identity");
 
  ViewData["apiResult"] = apiResponse.IsSuccessStatusCode 
                          ? await apiResponse.Content.ReadAsStringAsync() 
                          : apiResponse.StatusCode.ToString();

  return View();
}

In the above, you can see the IdentityModel in action. Using the DiscoveryClient means the client application only needs to know about the root address of the Identity Application. TokenClient is being used to request a token from the Identity Application for the clientApp using the client secret which in this case is actually the word secret. Keep in mind in a real application secrets should be kept using the ASP.NET Core Secrets manager, see this related post. Also, take note that clientApp and secret are the values that were defined in the Config class of the Identity application.

The rest of the code is taking the token response and making a call to the API application with the response from both of those calls being stored in ViewData for display on the view associated with the controller.

The view is just an Index.cshtml file in the path Views/Identity. The following is the full view.

@{
    ViewData["Title"] = "Identity";
}

@ViewData["tokenResult"]
@ViewData["apiResult"]

It isn’t pretty, but the whole point of this controller and view is just for verification that the three applications are properly communicating with each other.

URL Configuration

In this setup, it is important that the URL for the Identity Application and API Application be fixed so they can be accessed by the client. In a more production level application, these values would at a minimum need to be in configuration. The following is the setup used for this solution.

  • Identity Application – http://localhost:5000
  • API Application – http://localhost:5001
  • Client Application – http://localhost:5002

There are a couple of ways to configure test values. The first is to open the project properties and select the Debug tab and set the App URL.

The second option is to go to the Program class for each project and add a UseUrls to the WebHostBuilder like the following.

public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseUrls("http://localhost:5002")
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();

    host.Run();
}

Wrapping up

After going through the above process I now have a much better understanding of how the very basic setup using Identity Server should work. I hope if you made this far you found some helpful bits.

There is a bit of strangeness using Visual Studio to try and launch all three applications and can result in an error message if multiple of the projects are run in debug mode. For the most part, this can be worked around by only debugging one application at a time. It is a bit annoying at the beginning stages, but once an applications gets past that point I imagian that the Identity Application won’t require much debugging.

If there are any questions please leave a comment and I would be happy to try and help. The finished code can be found here. This basic example will be expanded over time and all the related entries can be found in the IdentityServer category.

Identity Server: Sample Exploration and Initial Project Setup Read More »

Upgrading a JavaScript Services Application

As part of the ASP.NET Core Basics series of posts, JavaScript Services was used to create a couple of front end for a basic contacts API using Aurelia and Angular 2. Theses applications were created a few months ago and JavaScript Services has kept moving since then. This post is going to look at one strategy for taking an application created on an older version of JavaScript Services and update it to match the current version. This post will be following the upgrade of the Angular project from ASP.NET Core Basics repo with the starting point of the code being from this release.

The strategy

One of the considerations when doing this upgrade was getting the changes that happen on the ASP.NET Core side of the application and not just the JavaScript bits. In order to make sure that nothing was missed I decided to use JavaScript Services to generate a new application and use that to compare with the implementations in the existing application.

Create comparison application

This is going to assume JavaScript Services is already installed. If it isn’t this page has instructions or this post has sections that deal with creating a new application using JavaScript Services.

The update

Following is the files that changed during this update. This is also the list of files I would check anytime an upgrade needs to be done.

Angular.csproj
ClientApp/boot-client.ts
ClientApp/boot-server.ts
Program.cs
package.json
webpack.config.js
webpack.config.vendor.js

There were a fair amount of changes in the files listed above and instead of posting the code the differences can be found here. The previous diff didn’t contain the webpack.config files and those diffs can be found here and here.

After all the files have been updated make sure to run the following command from a command prompt in your project directory to make sure webpack has vendor related items regenerated.

webpack --config webpack.config.vendor.js

Wrapping up

This post is a lighter on the details that I do most of the time, but this type of upgrade would just have been a wall of code and not been overly useful and the commits on GitHub are a much better guide to what the upgrade looked like. My feeling is that over time the number of changes going forward may end up being smaller and easier to integrate.

Both the Aurelia and Angular projects have been upgraded and the final version of the code can be found here.

Upgrading a JavaScript Services Application Read More »

Angular 2 Contact Creation and Post to an API

Expanding on this post which created a placeholder for creating new contacts this post will create an actual UI and post the newly created contact to an ASP.NET Core API. The code before any changes can be pulled using this release tag. Keep in mind all changes in this post take place in the Angular project if you are using the associated sample.

Contact service changes to all post

The contract service found in the contact.service.ts file of the ClientApp/app/components/contacts/ directory is used to encapsulate interaction with the associated ASP.NET Core API and prevent access to Angular’s Http library from being spread across the application. The first change is to expand the existing import of Angular’s Http library to include Headers and RequestOptions.

import { Http, Headers, RequestOptions } from '@angular/http';

Next, a save is added that makes a post request to the ASP.NET Core API with the body set to a JSON version of the contact to be added.

save(contact: Contact): Promise<Contact> {
   let headers = new Headers({ 'Content-Type': 'application/json' });
   let options = new RequestOptions({ headers: headers });

   return this.http.post(this.baseUrl, JSON.stringify(contact), options)
        .toPromise()
        .then(response => response.json())
        .then(contact => new Contact(contact))
        .catch(error => console.log(error));
}

The API will return the created contact with the ID now filled in which will be returned to the caller. As I have said before catching the error and just writing it to the console isn’t the proper way to handle errors and this should be done in a different manner for a production application.

Contact detail view model

The view model that backs the contact detail view needed a function to allow saving of a contact. The following code uses the contact service to save a contact and then replace its local contact with the new one returned from the API. Finally, the class level variable indicating if the view model is in create or detail mode is set to true which triggers the UI to change out of create mode.

save() {
     this.contactService.save(this.contact)
         .then(contact => this.contact = contact)
         .then(() => this.hasContactId = true);
}

You will see in the sample code that a second function was added for reset which is a quick way to reset the create UI.

reset() {
    this.contact = new Contact();
}

Contact model

The constructor of the contact class changed to make the data parameter optional to allow for the creation of an empty contact.

constructor(data?) {
    if (data == null) return;
    Object.assign(this, data);
}

Include Angular Forms

For the two-way binding needed on the contact detail page Angular forms will be used. To include them in the project open the app.module.ts file in the ClientApp/app/ directory. Add the following import.

import { FormsModule } from '@angular/forms';

Then add FormsModule to the imports array.

imports: [
     UniversalModule,
     FormsModule,
     RouterModule.forRoot([

Contact detail view

The following is the full contact view as it stands with all of the needed changes made. This will be followed by call outs of some of the important items.

 <h1>Contact Details</h1>
  <hr />
  <div *ngIf="hasContactId">
      <dl class="dl-horizontal">
          <dt>ID</dt>
          <dd>{{contact.id}}</dd>
         <dt>Name</dt>
         <dd>{{contact.name}}</dd>
         <dt>Address</dt>
         <dd>{{contact.getAddress()}}</dd>
         <dt>Phone</dt>
         <dd>{{contact.phone}}</dd>
         <dt>Email</dt>
          <dd>{{contact.email}}</dd>
      </dl>
  </div>
 <div *ngIf="!hasContactId">
     <div>
         <form role="form" class="form-horizontal">
             <div class="form-group">
                 <label class="col-sm-2 control-label">Name</label>
                 <div class="col-sm-10">
                     <input type="text" 
			    placeholder="name" 
			    class="form-control" 
                            [(ngModel)]="contact.name" 
			    name="name" />
                 </div>
             </div>
             <div class="form-group">
                 <label class="col-sm-2 control-label">Address</label>
                 <div class="col-sm-10">
                     <input type="text" 
			    placeholder="address" 
			    class="form-control" 
			    [(ngModel)]="contact.address" 
			    name="address" />
                 </div>
             </div>
             <div class="form-group">
                 <label class="col-sm-2 control-label">City</label>
                 <div class="col-sm-10">
                     <input type="text" 
			    placeholder="city" 
			    class="form-control" 
			    [(ngModel)]="contact.city" 
			    name="city" />
                 </div>
             </div>
             <div class="form-group">
                 <label class="col-sm-2 control-label">State</label>
                 <div class="col-sm-10">
                     <input type="text" 
			    placeholder="state" 
			    class="form-control" 
			    [(ngModel)]="contact.state" 
			    name="state" />
                 </div>
             </div>
             <div class="form-group">
                 <label class="col-sm-2 control-label">Zip</label>
                 <div class="col-sm-10">
                     <input type="text" 
			    placeholder="zip" 
			    class="form-control" 
			    [(ngModel)]="contact.postalCode" 
			    name="postalCode" />
                 </div>
             </div>
             <div class="form-group">
                 <label class="col-sm-2 control-label">Phone</label>
                 <div class="col-sm-10">
                     <input type="text" 
			    placeholder="phone" 
			    class="form-control" 
			    [(ngModel)]="contact.phone" 
			    name="phone" />
                 </div>
             </div>
             <div class="form-group">
                 <label class="col-sm-2 control-label">Email</label>
                 <div class="col-sm-10">
                     <input type="email" 
			    placeholder="email" 
			    class="form-control" 
			    [(ngModel)]="contact.email" 
			    name="email" />
                 </div>
             </div>
         </form>
     </div>
     <div class="text-center">
         <button class="btn btn-success btn-lg" 
		 (click)="save()">Save</button>
         <button class="btn btn-danger btn-lg" 
		 (click)="reset()">Reset</button>
     </div>
 </div>
  <a routerLink="/contact-list">Back to List</a>
  <hr />

All control of content rendering has been changed to use hasContactId.

Default view:
<div *ngIf="hasContactId">

Create view:
<div *ngIf="!hasContactId">

For the creation UI, the data is bound using Angular’s ngModel binding.

<input type="text" 
       placeholder="address" 
       class="form-control" 
       [(ngModel)]="contact.address" 
       name="address" />

If you have any issues make sure and check that you have the name attribute set to the property you are wanting to bind to.

The last thing to point out is the click handlers that are used to call the associate save and rest functions with the Save and Reset buttons are clicked.

<button class="btn btn-success btn-lg" 
        (click)="save()">Save</button>
<button class="btn btn-danger btn-lg" 
        (click)="reset()">Reset</button>

Wrapping up

Now the application has the ability to add contact not just view them which is one step closer to what would be needed for a real application. The finished code can be found here.

Angular 2 Contact Creation and Post to an API Read More »

Aurelia Contact Creation and Post to an API

Expanding on this post where a placeholder was added for contact creation the placeholder will be replaced with an actual UI. As part of the contact creation process, Aurelia’s fetch client will be used to make a post request to the ASP.NET API. The code at the starting point can be found here. If using the sample code keep in mind all the changes in this post takes place in the Aurelia project.

Contact service changes to allow post

In this project a service is used to keep all the Http bits isolated from the rest of the application. In the contactService.ts file found in the ClientApp/app/components/contacts/ directory a couple of changes need to be made. First the fetch import needs to expose json in addition to HttpClient.

import { HttpClient, json } from 'aurelia-fetch-client';

Then a save function is added that makes a post request to the ASP.NET API and return a new contact based on the response from the post request. The contact in the post response will contain the ID assigned by the API.

save(contact: Contact): Promise<Contact> {
    return this.http.fetch('',
        {
            method: 'post',
            body: json(contact)
        })
        .then(response => response.json())
        .then(contact => new Contact(contact))
        .catch(error => console.log(error));
}

Notice the usage of json to serialize the contact being create to JSON before sending to the server. Also, note that just logging an error to the console isn’t a best practice and should be handling in a different way in a production application.

Contact detail view model

The view model that backs the contact detail view needed a function to allow saving of a contact. The following code uses the contact service to save a contact and then replace its local contact with the new one returned from the API. Finally, the class level variable indicating if the view model is in create or detail mode is set to true which triggers the UI to change out of create mode.

save() {
     this.contactService.save(this.contact)
         .then(contact => this.contact = contact)
         .then(() => this.hasContactId = true);
}

You will see in the sample code that a second function was added for reset which is a quick way to reset the create UI.

reset() {
    this.contact = new Contact();
}

Contact model

The constructor of the contact class changed to make the data parameter optional to allow for the creation of an empty contact.

constructor(data?) {
    if (data == null) return;
    Object.assign(this, data);
}

Contact detail view

The view under when the most changes and the following is the entirety of the UI file which can be found in the contactDetail.html file. The code will be followed up with call outs for the important bits.

 <template>
      <h1>Contact Details</h1>
      <hr />
      <div if.bind="hasContactId">
          <dl class="dl-horizontal">
              <dt>ID</dt>
              <dd>${contact.id}</dd>
             <dt>Name</dt>
             <dd>${contact.name}</dd>
             <dt>Address</dt>
             <dd>${contact.getAddress()}</dd>
             <dt>Phone</dt>
             <dd>${contact.phone}</dd>
             <dt>Email</dt>
              <dd>${contact.email}</dd>
          </dl>
      </div>
     <div if.bind="!hasContactId">
         <div>
             <form role="form" class="form-horizontal">
                 <div class="form-group">
                     <label class="col-sm-2 control-label">Name</label>
                     <div class="col-sm-10">
                         <input type="text" 
                                placeholder="name" 
                                class="form-control" 
                                value.bind="contact.name" />
                     </div>
                 </div>
                 <div class="form-group">
                     <label class="col-sm-2 control-label">Address</label>
                     <div class="col-sm-10">
                         <input type="text" 
                                placeholder="address" 
                                class="form-control" 
                                value.bind="contact.address" />
                     </div>
                 </div>
                 <div class="form-group">
                     <label class="col-sm-2 control-label">City</label>
                     <div class="col-sm-10">
                         <input type="text" 
                                placeholder="city" 
                                class="form-control" 
                                value.bind="contact.city" />
                     </div>
                 </div>
                 <div class="form-group">
                     <label class="col-sm-2 control-label">State</label>
                     <div class="col-sm-10">
                         <input type="text" 
                                placeholder="state" 
                                class="form-control" 
                                value.bind="contact.state" />
                     </div>
                 </div>
                 <div class="form-group">
                     <label class="col-sm-2 control-label">Zip</label>
                     <div class="col-sm-10">
                         <input type="text" 
                                placeholder="zip" 
                                class="form-control" 
                                value.bind="contact.postalCode" />
                     </div>
                 </div>
                 <div class="form-group">
                     <label class="col-sm-2 control-label">Phone</label>
                     <div class="col-sm-10">
                         <input type="text" 
                                placeholder="phone" 
                                class="form-control" 
                                value.bind="contact.phone" />
                     </div>
                 </div>
                 <div class="form-group">
                     <label class="col-sm-2 control-label">Email</label>
                     <div class="col-sm-10">
                         <input type="email" 
                                placeholder="email" 
                                class="form-control" 
                                value.bind="contact.email" />
                     </div>
                 </div>
             </form>
         </div>
         <div class="text-center">
             <button class="btn btn-success btn-lg" 
                     click.delegate="save()">Save</button>
             <button class="btn btn-danger btn-lg" 
                     click.delegate="reset()">Reset</button>
         </div>
     </div>
     <div>
         <a route-href="route: contactlist">Back to List</a>
     </div>
      <hr />
  </template>

All control of content rendering has been changed to use hasContactId.

Default view:
<div if.bind="hasContactId">

Create view:
<div if.bind="!hasContactId">

For the creation UI, the data is bound using Aurelia’s value converters for more detail see the docs. The value converter is the value.bind bit.

<input type="text" 
       placeholder="name" 
       class="form-control" 
       value.bind="contact.name" />

The last thing to point out is the click delegates that are used to call the associate save and rest functions with the Save and Reset buttons are clicked.

<button class="btn btn-success btn-lg" 
        click.delegate="save()">Save</button>
<button class="btn btn-danger btn-lg" 
        click.delegate="reset()">Reset</button>

Wrapping up

The application now has the ability to add contact instead of only viewing existing contact which brings it close to a more realistic application. The code in its finished state can be found here.

The plan is to continue iterating on this application and moving the Aurelia and Angular 2 projects in parallel. I hope this is useful and if you have any specific features you would like to see implemented leave a comment.

Aurelia Contact Creation and Post to an API Read More »

Angular 2 Optional Route Parameter

This post expands on the Angular 2 post from a couple of weeks ago involving route links and parameters to include an “optional” route parameter. Using the use case from the same topic with Aurelia from last week’s post which is if a user ends up on the contact detail page with no contact ID they will be presented with the option to add a new contact. The starting point for the code can be found here. Keep in mind any changes in this post are taking place in the Angular project.

Route with an “optional” parameter

The reason for the quotes around optional is that with Angular’s current router I have found no way to make a route optional. As a work around two routes can be added that point to the same component. The following code is in the app.module.ts file of the ClientApp/app folder. The first handles calling the contact detail component without an ID and the second makes the call with an ID.

 { path: 'contact-detail', component: ContactDetailComponent },
 { path: 'contact-detail/:id', component: ContactDetailComponent },

Contact detail changes

The contact detail view model found in the contactdetail.component.ts file a class level property is needed to track of the contact detail component was called with an ID or not.

hasContactId: boolean;

Next, in the ngOnInit function has been changed to set the new property based on the route params having an ID set or not. If a contact ID is found then the details for that contact are loaded. The following is the full function.

ngOnInit(): void {
    var contactId: string;
 
    this.route.params
        .subscribe((params: Params) => contactId = params['id']);
 
   this.hasContactId = contactId != undefined;

   if (this.hasContactId) {
       this.contactService.getById(contactId)
           .then((contact: Contact) => this.contact = contact);
   }
}

In the associated view a placeholder for creating a new contact was added in the contactdetail.component.html file. This placeholder was added just above the link back to the contact list. This placeholder will only show if the detail components are loaded with no ID.

<h3 *ngIf="!hasContactId">
    Place holder for creating a new contact
</h3>

Add create link to the contact list

To finish a “Create New Contact” link is added to the contact list view found in the contactlist.component.html file which will call the contact detail component without a contact ID.

<a [routerLink]="['/contact-detail']">Create New Contact</a>

In the example solution, this link will show above the table of contacts.

Wrapping up

The finished code can be found here. If you have tried both Angular 2 and Aurelia leave a comment on how you think they compare and which you prefer.

Angular 2 Optional Route Parameter Read More »

Aurelia Optional Route Parameter

This post expands on the Aurelia post from a couple of weeks ago that involved router links and routing parameters to optional route parameters. The use case for this post is if a user ends up on the contact detail page with no contact ID they will be presented with the option to add a new contact. The starting point for the code can be found here. Keep in mind any changes in this post are taking place in the Aurelia project.

Route with optional parameter

Making a route parameter optional is as simple as adding a question mark to the end of the parameter name. The following is the before and after of the contact detail route found in the app.ts file in the ClientApp/app/components/app folder.

Before:
route: 'contact-detail/:id'

After:
route: 'contact-detail/:id?'

For more information on routing see the Aurelia docs.

Contact detail changes

In the contact detail view model which is in the contactDetail.ts file a class level property is added for if the component was activated with a contact ID or not.

hasContactId: boolean;

The activate function is changed to set the new class level variable to false if the function is called parms.id is falsy as well as to only pull contact details if it has a contact ID.

activate(parms, routeConfig) {
    this.hasContactId = parms.id;

    if (this.hasContactId) {
        return this.contactService.getById(parms.id)
            .then(contact => this.contact = contact);          
    }

    return null;

}

A placeholder for creating a new contact was added to the contact detail view found in contactDetail.html file. This placeholder was added just above the link back to the contact list. This placeholder will only show if the detail components are loaded with no ID.

<h3 if.bind="!hasContactId">
    Placeholder for creating a new contact
</h3>

Add create link to the contact list

Finally, add a link in the contactList.html file that sends the user to the contact detail view, but without sending a contact ID.

<a route-href="route: contactdetail">Create New Contact</a>

In the example code this was added before the table of contacts, but of course, it could be anywhere.

Wapping up

The code in it’s completed state can be found here. The same functionality using Angular 2 will be coming up next week.

Aurelia Optional Route Parameter Read More »

Angular 2 – Router links, click handlers, routing parameters

As with last week’s post this post is going to cover multiple topics related to while creating a contact detail page to go along with the existing contact list page that is part of the ASP.NET Basics repo, but this time using Angular 2 instead of Aurelia. The code before any changes can be found here. If you are using the sample application keep in mind all the changes in this post take place in the Angular project.

Creating a detail view and view model

Create contactdetail.component.html in ClientApp\app\components\contacts. This view that will be used to display all the details of a specific contact. It will also link back to the contact list. The following image shows the folder structure after the view and view model have been added.

View

The following is the full contents of the view.

<h1>Contact Details</h1>
<hr />
<div *ngIf="contact">
    <dl class="dl-horizontal">
        <dt>ID</dt>
        <dd>{{contact.id}}</dd>
        <dt>Name</dt>
        <dd>{{contact.name}}</dd>
        <dt>Address</dt>
        <dd>{{contact.getAddress()}}</dd>
        <dt>Phone</dt>
        <dd>{{contact.phone}}</dd>
        <dt>Email</dt>
        <dd>{{contact.email}}</dd>
    </dl>
</div>
<a routerLink="/contact-list">Back to List</a>
<hr />

*ngIf is adding/removing the associated div based on a contact being set or not.

{{value}} is Angular’s one-way binding syntax. For more details check out the docs.

<a routerLink=”/contact-list”> is using Angular to generate a link back to the contact list component.

View model

For the view model add contactdetail.component.ts in the ClientApp\app\components\contacts folder which is the same folder used for the view.

Make not of the imports needed to make this view model work. To start off Contact is needed to define what the definition of a contact is and ContactService is being used to load the data for a specific contact.

Angular’s core is imported to allow the view model to set as a component using @Component decorator as well as to all implementation of OnInit lifecycle hook.  Angular’s router is being used in the ngOnInit function to allow access the parameters of the route that caused the route to be triggered using this.route.params.

The switchMap operator from reactive extensions is used to map the id from the route parameters to a new observable that has the result of this.contactService.getById.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import 'rxjs/add/operator/switchMap';
import { Contact } from './contact';
import { ContactService } from './contact.service';

@Component({
    selector: 'contactdetail',
    template: require('./contactdetail.component.html'),
    providers: [ContactService]
})
export class ContactDetailComponent implements OnInit {
    contact: Contact;

    constructor(private route: ActivatedRoute,
                private router: Router,
                private contactService: ContactService) { }

    ngOnInit(): void {
        this.route.params
            .switchMap((params: Params) => 
                   this.contactService.getById(params['id']))
            .subscribe((contact :Contact) => this.contact = contact);
    }
}

Adding get by ID to the Contact Service

The existing ContactService doesn’t provide a function to get a contact by a specific ID so one needs to be added.

The following calls the API in the Contacts project and uses the result to create an instance of a  Contact as a promise which is returned to the caller.

getById(id: string): Promise<Contact> {
    return this.http.get(this.baseUrl + id)
        .toPromise()
        .then(response => response.json())
        .then(contact => new Contact(contact))
        .catch(error => console.log(error));
}

The base URL was also moved to a class level variable so that it could be shared.

Add a route with a parameter

To add the new contact detail to the list of routes that the application handles open app.module.ts in the ClientApp/app folder. First, add an import at the top of the file for the new component.

import { ContactDetailComponent } from './components/contacts/contactdetail.component';

Next, add the ContactDetailComponent to the declarations array of the @NgModule decorator.

declarations: [
    AppComponent,
    NavMenuComponent,
    CounterComponent,
    FetchDataComponent,
    ContactListComponent,
    ContactDetailComponent,
    HomeComponent
]

Finally, add the new route to the RouteModule in the imports section of the @NgModule decorator.

RouterModule.forRoot([
    { path: '', redirectTo: 'home', pathMatch: 'full' },
    { path: 'home', component: HomeComponent },
    { path: 'counter', component: CounterComponent },
    { path: 'fetch-data', component: FetchDataComponent },
    { path: 'contact-list', component: ContactListComponent },
    { path: 'contact-detail/:id', component: ContactDetailComponent },
    { path: '**', redirectTo: 'home' }
])

path is the pattern used to match URLs. In addition, parameters can be used in the form of :parameterName. The above route will handle requests for http://baseurl/contact-detail/{id} where {id} is an ID of a contact. As demonstrated above in the ngOnInit function of the view model route.params can be used to access route parameters.

component is used to locate the view/view model that goes with the route.

Integrating the detail view with the contact list

The contact list view and view model needed the following changes to support the contact detail page.

View model

A variable was added for the ID of the select contact was as well as a onSelect function which takes a contact and gets called when a contact is selected. The following is the fully contact list view model.

import { Component, OnInit } from '@angular/core';
import { Contact } from './contact';
import { ContactService } from './contact.service';

@Component({
    selector: 'contactlist',
    template: require('./contactlist.component.html'),
    providers: [ContactService]
})
export class ContactListComponent implements OnInit {
    contacts: Contact[];
    selectedContactId: number = null;

    constructor(private contactService: ContactService) { }

    ngOnInit(): void {
        this.contactService.getAll()
            .then(contacts => this.contacts = contacts);
    }

    onSelect(contact) {
        this.selectedContactId = contact.id;
    }
}

The changes to the view model were not required to add the contact detail page, but are used show how to set up a click handler the view side. In the future, the selected contact will come in handy when the list and details were shown at the same time.

View

The amount of data being displayed was reduced to just ID and name. A column was added with a link to the details page. The following is the full view.

<h1>Contact List</h1>

<p *ngIf="!contacts"><em>Loading...</em></p>

<table class="table" *ngIf="contacts">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let contact of contacts">
            <td>{{contact.id}}</td>
            <td>{{contact.name}}</td>
            <td><a [routerLink]="['/contact-detail', contact.id]" (click)="onSelect(contact)">Details</a></td>
        </tr>
    </tbody>
</table>

(click)=”onSelect(contact)” will cause the onSelect function of the view model to be called with the related contact when the associated element is clicked.

[routerLink]=”[‘/contact-detail’, contact.id]” use the router to create a line to the contact details page passing the contact ID as a parameter.

As a reminder of how to use a route’s parameter here is the ngOnInit function from the contact detail view model.

ngOnInit(): void {
    this.route.params
        .switchMap((params: Params) => this.contactService.getById(params['id']))
        .subscribe((contact :Contact) => this.contact = contact);
}
 Wrapping up

As with the related Aurelia post, there are a lot of topics covered in this post, but they are all related to the process of adding a new page and route to the application.

The code including all the changes for this post can be found here.

 

Angular 2 – Router links, click handlers, routing parameters Read More »

Aurelia – Router links, click delegate, routing parameters

This post is going to cover multiple of topics that I hit while creating a contact detail page to go along with the contact list that is part of my ASP.NET Basics repo. The code before any changes can be found here. If you are following along with the sample application keep in mind all the changes in this post take place in the Aurelia project.

Creating a detail view and view model

Create contactDetail.html file inside of ClientApp\app\components\contacts. This is the view that will be used to display all the details of a specific contact as well as a link back to the contact list. The following image shows the folder structure with the view and view model already added.

View

The following is the full contents of the view. This will be followed up few a calls out of Aurelia specific things going on.

<template>
    <h1>Contact Details</h1>
    <hr />
    <div if.bind="contact">
        <dl class="dl-horizontal">
            <dt>ID</dt>
            <dd>${contact.id}</dd>
            <dt>Name</dt>
            <dd>${contact.name}</dd>
            <dt>Address</dt>
            <dd>${contact.getAddress()}</dd>
            <dt>Phone</dt>
            <dd>${contact.phone}</dd>
            <dt>Email</dt>
            <dd>${contact.email}</dd>
        </dl>
    </div>
    <a route-href="route: contactlist">Back to List</a>
    <hr />
</template>

if.bind will keep the contact details out of the DOM if the condition fails. In this case, if the contact is null.

${value} is one of Aurelia’s bind syntaxes. For more details check out their docs.

<a route-href=”route: contactlist”> is using Aurelia’s router to generate a link back to the contact list component.

View model

Next, create a contactDetail.ts file inside of ClientApp\app\components\contacts  which is the view model that goes with the view created above.

The view model gets an instance of the ContactService injected which is used to pull a contact’s detail information during the activate lifecycle hook. Notice the first parameter of the function (parms) which is where the route parameters are passed in.

import { inject } from 'aurelia-framework';
import { Contact } from './contact';
import { ContactService } from './contactService';

@inject(ContactService)
export class ContactDetail {
     contact: Contact;

    constructor(private contactService: ContactService) { }

    activate(parms, routeConfig) {
        return this.contactService.getById(parms.id)
            .then(contact => this.contact = contact);
    }
}

Adding get by ID to the Contact Service

The existing ContactService doesn’t provide a function to get a contact by a specific ID so one needs to be added.

The following calls the API in the Contacts project and uses the result to create an instance of a Contact as a promise which is returned to the caller.

getById(id: string): Promise<Contact> {
    return this.http.fetch(id)
        .then(response => response.json())
        .then(contact => new Contact(contact))
        .catch(error => console.log(error));
}

Add a route with a parameter

Next, the router needs to be made aware of the new contact details. Open app.ts inside of the ClientApp/app/components/app folder. This file contains all the routes for the application. The bit we are interested in is the config.map which is an array of routes the application handles. Contact details is a new route which means adding a new object to the config.map array.

{
 route: 'contact-detail/:id',
 name: 'contactdetail',
 moduleId: '../contacts/contactDetail',
 nav: false,
 title: 'Contact Detail'
}

The route is the pattern used to match URLs. In addition, parameters can be used in the form of :parameterName. The above route will handle requests for http://baseurl/contact-detail/{id} where {id} is an ID of a contact. If a route has a parameter it will be made available to the activate function via the parms parameter in the view model.

moduleId is used to locate the view/view model that goes with the route.

nav controls if the route will be included in the routers navigation model. This is used to build the menu in this application. Contact details shouldn’t show in the menu which is why nav is set to false. For more details on Aurelia’s router check out the docs.

Integrating the detail view with the contact list

The contact list view and view model needed changes to support the contact detail page.

View model

The change to the view model was the simplest. A variable for the ID of the select contact was added as well as a function that gets called when a contact is selected. The following is the fully contact list view model.

import { inject } from 'aurelia-framework';
import { Contact } from './contact';
import { ContactService } from './contactService';

@inject(ContactService)
export class ContactList {
    contacts: Contact[];
    selectedContactId: number = null;

    constructor(private contactService: ContactService) {}

    created() {
        this.contactService.getAll()
            .then(contacts => this.contacts = contacts);
    }

    select(contact) {
        this.selectedContactId = contact.id;
    }
}

The changes to the view model were not really required to add the contact detail page, but they are there to show how to setup a click delegate on the view side. In the future, the selected contact could come in handy if the list and details were shown at the same time.

View

On the view, the amount of data being displayed was reduced to show just contact ID and name. A column was added with a link to the details page that is now used to show the rest of information about a contact. The following is the full view.

<template>
    <h1>Contact List</h1>
    <p if.bind="!contacts"><em>Loading...</em></p>

    <table class="table" if.bind="contacts">
        <thead>
        <tr>
            <th>IDs</th>
            <th>Name</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        <tr repeat.for="contact of contacts" 
          class="${contact.id === selectedContactId ? 'active' : ''" }>
            <td>${contact.id}</td>
            <td>${contact.name}</td>
            <td><a route-href="route: contactdetail; params.bind: {id:contact.id}" click.delegate="select($contact)">
                  Details
                </a>
            </td>
        </tr>
        </tbody>
    </table>
    <hr />
</template>

click.delegate=”select($contact)” will cause the select function of the view model to be called when the associated element clicked and passes the relevant contact object. The docs go into more depth on when to use delegates vs triggers.

The details link contains a lot of concepts. route-href is binding the anchor to Aurelia’s router.

route: contactdetail is telling the router which route to load.

params.bind: {id:contact.id} is telling the link to pass the contact’s ID to through the router to the view model that is being loaded. The following is the activate function of the contact detail view model as a reminder of the parameter’s usage.

activate(parms, routeConfig) {
    return this.contactService.getById(parms.id)
        .then(contact => this.contact = contact);
}

Wrapping up

This post cover a lot of topics, but they were all things I had to review in the course of adding contact details. My hope is this post will shortcut your own research and get you back to your task at hand. The finished code can be found here.

Leave a comment with any thoughts and/or questions.

Aurelia – Router links, click delegate, routing parameters Read More »