Click here to Skip to main content
15,891,745 members
Articles / Programming Languages / Typescript

Tour of Heroes: Aurelia, with ASP.NET Core Backend

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
22 Nov 2023CPOL2 min read 2.7K  
A series of articles comparing programmer experiences of Angular, Aurelia, React, Vue, Xamarin and MAUI
"Tour of Heroes" on Aurelia, talking to a real backend through generated client API

Background

"Tour of Heroes" is the official tutorial app of Angular 2+. The app contains some functional features and technical features which are common when building a real-world business application:

  1. A few screens presenting tables and nested data
  2. Data binding
  3. Navigation
  4. CRUD operations over a backend, and optionally through a generated client API
  5. Unit testing and integration testing

In this series of articles, I will demonstrate the programmer experiences of various frontend development platforms when building the same functional features: "Tour of Heroes", a fat client talking to a backend.

The frontend apps on Angular, Aurelia, React, Vue, Xamarin and MAUI are talking to the same ASP.NET (Core) backend through generated client APIs. To find the other articles in the same series, please search "Heroes" in my articles. And at the end of the series, some technical factors of programmer experiences will be discussed:

  1. Computing science
  2. Software engineering
  3. Learning curve
  4. Build size
  5. Runtime performance
  6. Debugging

Choosing a development platform involves a lot of non-technical factors which won't be discussed in this series.

References

Introduction

This article is focused on Aurelia.

Development Platforms

  1. Web API on ASP.NET Core 8
  2. Frontend on Aurelia 1.4.1

Demo Repository

Checkout DemoCoreWeb in GitHub, and focus on the following areas:

Core3WebApi

ASP.NET Core Web API

Aurelia Heroes

This is a rewrite version of the official tutorial demo of Angular "Tour of Heroes".

Using the Code

Prerequisites

  1. Core3WebApi.csproj has NuGet packages Fonlow.WebApiClientGenCore and Fonlow.WebApiClientGenCore.Aurelia imported.
  2. Add CodeGenController.cs to Core3WebApi.csproj.
  3. Core3WebApi.csproj has CodeGen.json. This is optional, just for the convenience of running some PowerShell script to generate client APIs.
  4. CreateWebApiClientApi3.ps1. This is optional. This script will launch the Web API on DotNet Kestrel web server and post the data in CodeGen.json.

Remarks

Depending on your CI/CD process, you may adjust item 3 and 4 above. For more details, please check:

Generate Client API

In CodeGen.json, include the following:

JavaScript
"Plugins": [
			{
				"AssemblyName": "Fonlow.WebApiClientGenCore.Aurelia",
				"TargetDir": "..\\..\\..\\..\\AureliaHeroes\\src\\clientapi",
				"TSFile": "WebApiCoreAureliaClientAuto.ts",
				"AsModule": true,
				"ContentType": "application/json;charset=UTF-8"
			},

Run CreateWebApiClientApi3.ps1, the generated codes will be written to WebApiCoreAureliaClientAuto.ts.

Data Models and API Functions

TypeScript
export namespace DemoWebApi_Controllers_Client {

    /**
     * Complex hero type
     */
    export interface Hero {
        id?: number | null;
        name?: string | null;
    }
}
TypeScript
@autoinject()
export class Heroes {
    constructor(private http: HttpClient) {
    }

    /**
     * DELETE api/Heroes/{id}
     */
    delete(id: number | null, headersHandler?: () =>
                        {[header: string]: string}): Promise<Response> {
        return this.http.delete('api/Heroes/' + id,
                        { headers: headersHandler ? headersHandler() : undefined });
    }

    /**
     * Get a hero.
     * GET api/Heroes/{id}
     */
    getHero(id: number | null, headersHandler?: () =>
    {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
        return this.http.get('api/Heroes/' + id,
        { headers: headersHandler ?
          headersHandler() : undefined }).then(d => d.json());
    }

    /**
     * Get all heroes.
     * GET api/Heroes
     */
    getHeros(headersHandler?: () => {[header: string]: string}):
                              Promise<Array<DemoWebApi_Controllers_Client.Hero>> {
        return this.http.get('api/Heroes',
        { headers: headersHandler ? headersHandler() : undefined }).then(d => d.json());
    }

    /**
     * POST api/Heroes
     */
    post(name: string | null, headersHandler?: () =>
    {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
        return this.http.post('api/Heroes', JSON.stringify(name),
        { headers: headersHandler ? Object.assign(headersHandler(),
        { 'Content-Type': 'application/json;charset=UTF-8' }):
        { 'Content-Type': 'application/json;charset=UTF-8' } }).then(d => d.json());
    }

    /**
     * Update hero.
     * PUT api/Heroes
     */
    put(hero: DemoWebApi_Controllers_Client.Hero | null, headersHandler?: () =>
       {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
        return this.http.put('api/Heroes', JSON.stringify(hero),
        { headers: headersHandler ? Object.assign(headersHandler(),
        { 'Content-Type': 'application/json;charset=UTF-8' }):
        { 'Content-Type': 'application/json;charset=UTF-8' } }).then(d => d.json());
    }
}

While there could be multiple ways of utilizing the generated API functions, the orthodox way is to inject through the recommended Dependency Injection mechanism of respective development platform. For example:

TypeScript
export function configure(aurelia: Aurelia): void {
...
  const c= new HttpClient();
  c.baseUrl='http://localhost:5000/';
  aurelia.container.registerInstance
  (DemoWebApi_Controllers_Client.Heroes, new DemoWebApi_Controllers_Client.Heroes(c));

Views

Editing

hero-detail.html

HTML
<template>
  <require from="./hero-detail.css"></require>
  <div if.bind="hero">
    <h2>${hero.name} Details</h2>
    <div><span>id: </span>${hero.id}</div>
    <div>
      <label for="hero-name">Hero name: </label>
      <input id="hero-name" value.bind="hero.name" placeholder="Name" />
    </div>

    <button type="button" click.delegate="goBack()">go back</button>
    <button type="button" click.delegate="save()">save</button>
  </div>
</template>

hero-detail.ts (Codes behind)

TypeScript
import {DemoWebApi_Controllers_Client} from '../clientapi/WebApiCoreAureliaClientAuto';
import { Router, RouterConfiguration, RouteConfig } from 'aurelia-router';
import {inject} from 'aurelia-framework';

@inject(Router, DemoWebApi_Controllers_Client.Heroes)
export class HeroDetailComponent {
  hero?: DemoWebApi_Controllers_Client.Hero;
  routeConfig?: RouteConfig;
 
  constructor(
    private router: Router,
    private heroesService: DemoWebApi_Controllers_Client.Heroes
  ) {
  }

  created(){

  }

  activate(params: any, routeConfig: RouteConfig) {
    this.routeConfig = routeConfig;
    const id = params.id;
    console.debug('service: ' + JSON.stringify(this.heroesService));
    this.heroesService.getHero(id).then(
      hero => {
        if (hero) {
          this.hero = hero;
          this.routeConfig.navModel.setTitle(this.hero.name);
        }
      }
    ).catch(error => alert(error));
  }

  save(): void {
    this.heroesService.put(this.hero).then(
      d => {
        console.debug('response: ' + JSON.stringify(d));
      }
    ).catch(error => alert(error));
  }

  goBack(): void {
    this.router.navigateBack();
  }
}

Heroes List

heroes.html

HTML
<template>
  <require from="./heroes.css"></require>
  <h2>My Heroes</h2>

  <div>
    <label for="new-hero">Hero name: </label>
    <input id="new-hero" ref="heroName" />

    <!-- (click) passes input value to add() and then clears the input -->
    <button type="button" class="add-button" click.delegate="addAndClear()">
      Add hero
    </button>
  </div>

  <ul class="heroes">
    <li repeat.for="hero of heroes">
      <a route-href="route: detail; params.bind: {id:hero.id}">
        <span class="badge">${hero.id}</span> ${hero.name}
      </a>
      <button type="button" class="delete" title="delete hero" 
              click.delegate="delete(hero)">x</button>
    </li>
  </ul>
</template>

heroes.ts

TypeScript
import {DemoWebApi_Controllers_Client} from '../clientapi/WebApiCoreAureliaClientAuto';
import { Router } from 'aurelia-router';
import {inject} from 'aurelia-framework';

@inject(Router, DemoWebApi_Controllers_Client.Heroes)
export class HeroesComponent {
    heroes?: DemoWebApi_Controllers_Client.Hero[];
    selectedHero?: DemoWebApi_Controllers_Client.Hero;
    private heroName: HTMLInputElement;

    constructor(private router: Router, 
                private heroesService: DemoWebApi_Controllers_Client.Heroes) { 
    }

    getHeroes(): void {
        this.heroesService.getHeros().then(
            heroes => {
                this.heroes = heroes;
            }
        );
    }

    add(name: string): void {
        name = name.trim();
        if (!name) { return; }
        this.heroesService.post(name).then(
            hero => {
                this.heroes?.push(hero);
                this.selectedHero = undefined;
            });
    }

    delete(hero: DemoWebApi_Controllers_Client.Hero): void {
        this.heroesService.delete(hero.id!).then(
            () => {
                this.heroes = this.heroes?.filter(h => h !== hero);
                if (this.selectedHero === hero) { this.selectedHero = undefined; }
            });
    }

    created() {
        this.getHeroes();
    }

    onSelect(hero: DemoWebApi_Controllers_Client.Hero): void {
        this.selectedHero = hero;
    }

    gotoDetail(): void {
        this.router.navigateToRoute('/detail', this.selectedHero?.id);
    }

    addAndClear(){
        this.add(this.heroName.value); 
        this.heroName.value='';
    }
}

View Models

In an Aurelia component, a public data field or a function is a view model, being monitored by Aurelia runtime for change detection. For example:

TypeScript
export class HeroDetailComponent {
  hero?: DemoWebApi_Controllers_Client.Hero;
TypeScript
export class HeroesComponent {
    heroes?: DemoWebApi_Controllers_Client.Hero[];
    selectedHero?: DemoWebApi_Controllers_Client.Hero;

Routing

Aurelia provides advanced routing.

Symbolic Routing Globally or Within a Module

TypeScript
configureRouter(config: RouterConfiguration, router: Router) {
  config.title = 'Heroes';
  config.options.pushState = true;
  //config.options.root = '/';
  config.map([
    //{ route: '', redirect: '/dashboard' },
    { route: ['', 'dashboard'], moduleId: PLATFORM.moduleName('components/dashboard'),
                                title: 'Dashboard', name: 'dashboard' },
    { route: 'heroes', moduleId: PLATFORM.moduleName('components/heroes'),
                                 title: 'Heroes', name: 'heroes' },
    { route: 'detail/:id',
      moduleId: PLATFORM.moduleName('components/hero-detail'), name: 'detail' },
  ]);
}
TypeScript
gotoDetail(): void {
    this.router.navigateToRoute('/detail', this.selectedHero?.id);
}
HTML
<li repeat.for="hero of heroes">
  <a route-href="route: detail; params.bind: {id:hero.id}">
    <span class="badge">${hero.id}</span> ${hero.name}
  </a>
  <button type="button" class="delete" title="delete hero"
                        click.delegate="delete(hero)">x</button>
</li>

Integration Testing

Since the frontend of "Tour of Heroes" is a fat client, significant portion of the integration testing is against the backend.

TypeScript
describe('Heroes API', () => {
  const service = new namespaces.DemoWebApi_Controllers_Client.Heroes(http);

  it('getAll', (done) => {
    service.getHeros().then(
      data => {
        console.debug(data.length);
        expect(data.length).toBeGreaterThan(0);
        done();
      },
      error => {

        done();
      }
    );

  }
  );

  it('Add', (done) => {
    service.post('somebody').then(
      data => {
        console.info('Add hero: ' + JSON.stringify(data));
        expect(data.name).toBe('somebody');
        done();
      },
      error => {

        done();
      }
    );

  }
  );

History

  • 22nd November, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
-- There are no messages in this forum --