Click here to Skip to main content
15,030,744 members
Articles / Web Development / HTML
Article
Posted 7 Feb 2017

Stats

57.3K views
586 downloads
36 bookmarked

SPA^2 using ASP.NET Core 1.1 + Angular 2.4 - Part 2

Rate me:
Please Sign up or sign in to vote.
4.96/5 (35 votes)
23 Mar 2017CPOL27 min read
How to add a basic tag helper for data display, and start to link Angular to your ASP.NET Core
In this part of the series, you will learn how to accelerate development using C# tag helpers and Razor from ASP.NET Core MVC together with Angular 2. Yes - have your SPA and eat it too. Source now includes VS2015 and VS2017 versions.

Introduction

This is Part 2 of a series of articles. Part 1 covered a method of integrating ASP.NET Core and Angular 2.

Create an Angular Single Page Application or SPA and the terrific User Experience (Ux) can really impress users, and in turn, this is almost certain to impress your project sponsors and make happy product owners.

Why? Page navigation is snappy and fast since the first time you view a page, the view templates are cached, then subsequent calls to the same page only need to shout out for data, and not page + data. Even some of the data can be cached client side, and be re-used if you add some client side 'smarts' to your pages.

But suppose you have ASP.NET MVC developers on hand, and skills to create an Angular SPA are just out of reach. Then creating another MVC site seems easy. But look closely at what happens, your server will still be rendering and re-rendering both pages and data much of the time. Even with partial views and server side caching, this server side effort and extra traffic can make your site feel soggy and slow, users with limited mobile connectivity can suffer too as time for page views and their data costs increase.

But try to move to an Angular SPA, away from ASP.NET MVC, and your developers can lose some of their "super-powers" as they trade in familiar tools like Razor, or custom tag helpers and need to code some aspects of your application twice - once server side and again client side.

Previously, you could create data models, use automatically generated validation and crank out code quickly, but a typically attempt to push into the land of the SPA and you find you're now creating two sets of data models, creating data validation code client side in JavaScript or TypeScript, and then there's the server side data models and validation attributes server side in C# as well. Code become fragile, badly coupled, bugs and technical debt increases and the project deadlines seem further away.

As the big divide between server side and client side code (and coders) increases, your shiny ASP.NET Core MVC app is serving flat HTML and the only awesomeness left is serving data using Web API calls.

There is another alternative, use ASP.NET Core MVC partial views in place of flat HTML templates, and keep using the great ASP.NET Core features as well as those in Angular - you can get your SPA and eat it too (that is have an Angular SPA + Razor and Custom Tag Helpers).

Background

This series of articles are the end result of a number of web applications I have built over the last few years using Angular (1.x) and ASP.NET MVC 4.5x, now altered and adapted around ASP.NET Core MVC and Angular 2.

Caveat: I have not used this combination of Angular 2 and ASP.NET Core 1.x in production yet, though I have started projects for two clients using this code, it is still a work in progress, and may well take a few different turns along the way before it is complete. At the end however, I hope to share my findings and give you a head start into what I think is a better way to create an SPA.

Please don't look to these articles to tell you what is the best backend. I will not be proscribing anything particular there, instead I'll use a simple EF core + MS SQL backend. You could use CQRS, your favourite ORM, Raven, Mongo or whatever you want and as far as I can see, this will not matter here.

Instead, these articles will concentrate on how you can make Angular 2 code fit more easily into an ASP.NET Core MVC backend, how to automate a lot of the work, how to reduce bad coupling and help remove the issues of cut/paste repetition.

Part 1 of this series has a background on the basic way to get ASP.NET Core to serve smarter views to your Angular 2 SPA. We replace the standard flat HTML template views in Angular QuickStart ASP.NET Core with partial views served up from ASP.NET Core MVC and get something functionally very similar.

Here in Part 2, we will add a simple Web API service to the back end, and alter one of the Angular views to call an Angular service which in turn calls this new Web API service. This will introduce how tag helpers can reduce overheads of repetitive code and generate Angular markup at the same time.

Adding a Simple Web API Service

First, we add our Web API service; right click the A2SPA solution, at the root level, click Add, then New Folder:

Add New Folder

Enter the new folder name Api:

Change folder name

Next right click this new folder, click Add, then New Item.

Add New Item

Now enter the word controller in the search box, from the results, choose Web API Controller Class, then rename the default name from ValuesController.cs to SampleDataController.cs.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
 
// For more information on enabling Web API for empty projects, 
// visit http://go.microsoft.com/fwlink/?LinkID=397860
 
namespace A2SPA.Api
{
    [Route("api/[controller]")]
    public class SampleDataController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
 
        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
 
        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
 
        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
 
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

We'll leave this Web API controller with the default contents for now, as it will serve up a simple string array and ensure the services are running as planned without too much added complexity, yet.

Next, we'll add a folder called "services" in the wwwroot/app folder. Again, just right click the parent "app" folder:

add new folder called services

Right click the new folder, click Add, New Item, this time type typescript in the search box:

add new typescript file

And again, click the Add button.

This new SampleData.services.ts will be a re-usable HTTP data service used by our Angular controller to fetch data from the Web API controller just created.

This template results in a blank file, so copy the following code into it:

JavaScript
import { Injectable } from '@angular/core'>;
import { Http, Response } from '@angular/http';
import { Observable }     from 'rxjs/Observable';

@Injectable()
export class SampleDataService {
    private url: string = 'api/';
    constructor(private http: Http) { }
    getSampleData(): Observable<string[]> {
        return this.http.get(this.url + 'sampleData')
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    // from https://angular.io/docs/ts/latest/guide/server-communication.html

    private handleError(error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }
}

Next, we'll update the existing app.module.ts file to provide required dependencies, change it to the following:

JavaScript
import { NgModule, enableProdMode } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { routing, routedComponents } from './app.routing';
import { APP_BASE_HREF, Location } from '@angular/common'>;
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms'>;
import { HttpModule  } from '@angular/http';
import { SampleDataService } from './services/sampleData.service';
import './rxjs-operators';
 
// enableProdMode();
 
@NgModule({
    imports: [BrowserModule, FormsModule, HttpModule, routing],
    declarations: [AppComponent, routedComponents],
    providers: [SampleDataService, Title, { provide: APP_BASE_HREF, useValue: '/' }],
    bootstrap: [AppComponent]
})
export class AppModule { }

You'll notice that we've added a new import:

JavaScript
import './rxjs-operators';

This will need another new typescript file. Just add a new Typescript file, in much the same way as we did a moment earlier, except this time, right click the wwwroot/app folder, then click Add, New Item, again type typescript in the search box, select Typescript File, change the name to rxjs-operators.ts.

This new file rxjs-operators.ts should be updated to contain the following code:

JavaScript
// NOTE: Use this option to add ALL RxJS statics & operators to Observable 
// (upside: simple, downside: larger, slower to load)
// import 'rxjs/Rx';
 
// NOTE: Use this option below to import just the rxjs statics and 
// operators needed for this app.
 
 // Observable class extensions
 import 'rxjs/add/observable/of';
 import 'rxjs/add/observable/throw';
 // Observable operators
 import 'rxjs/add/operator/catch';
 import 'rxjs/add/operator/debounceTime';
 import 'rxjs/add/operator/distinctUntilChanged';
 import 'rxjs/add/operator/do';
 import 'rxjs/add/operator/filter';
 import 'rxjs/add/operator/map';
 import 'rxjs/add/operator/switchMap';

Now update our existing about.component.ts code so that on initialization, it will fetch data from our new Angular data service and in turn from the new Web API service:

JavaScript
import { Component, OnInit } from '@angular/core';
import { SampleDataService } from './services/sampleData.service';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent implements OnInit {
    testData: string[] = [];
    errorMessage: string;
    constructor(private sampleDataService: SampleDataService) { }
    ngOnInit() {
        this.sampleDataService.getSampleData()
            .subscribe((data: string[]) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
}

Lastly, we'll update the ASP.NET partial view/Angular template AboutComponent.cshtml so that it will consume our data.

Initially, AboutComponent.cshtml was:

HTML
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Use this area to provide additional information.</p>

Update this to the following:

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Example of Angular 2 requesting data from ASP.Net Core.</p>
 
<p>Data:</p>
<table>
    <tr *ngFor="let data of testData">
        <td>{{ data }}</td>
    </tr>
</table>
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

Rebuild your app, hit Ctrl-F5 and when you navigate to the About page, you should see our amazing new content. Not so amazing, but best start off small. The F12 browser debug window, console tab should show no errors:

basic angular markup

And when you view the F12 debug window, and select the network tab, then you should be able to navigate to point of time where the "SampleData" was fetched (if not visible, check the settings in the network debug tab and refresh the page to see it again), then when the SampleData is selected on the left, you'll see the Request and (as below), Response data that was sent by our Web API data service to the browser, when requested by our new Angular Service.

our sample data service in action

Reading a small array of two strings isn't likely to be very useful, in real life we have many different data types and we usually need to support more than reading data, we need to provide the usual "CRUD" operation (Create, Read, Update, Delete).

Support for CRUD operations will be added in a later part of this series of articles; we'll use tooling to automatically add these operations. Less manual coding using tooling will not only save time and reduce the chance of errors, but help remove the temptation from developers to cut and paste one service to create another as the number of services expands.

Still, if you’d like to get familiar with CRUD operations for Angular 2 data services in the meantime, you could refer to these Angular 2 tutorials:

In the next section, we'll be adding support for a few other data types to our Web API data service.

Supporting Multiple Data Types

Next, we'll expand our Web API controller to enable serving a wider variety of data types, demonstrate how this is usually handled for some basic operations and introduce another key way to accelerate development- tag helpers.

First to replace the simple array of strings served up by our default Web API code in SampleDataController.cs, we'll create a new class, a View Model, called TestData. Though small and simple to allow us to focus on the concepts involved, it will be closer to real-life data.

At the root of the A2SPA project, click Add, click New Folder, create a folder called ViewModels.

Right click the ViewModels folder, click Add, click Class, use the file name TestData.cs for the new class.

Update the content of TestData.cs to contain the following:

C#
using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        public string Username { get; set; }
 
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; } 
 
        [Required, RegularExpression(@"([a-zA-Z0-9_\-\.]+)@
        ((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))
        ([a-zA-Z]{2,4}|[0-9]{1,3})", ErrorMessage = "Please enter a valid email address.")]
        [EmailAddress]
        [Display(Name = "EmailAddress", ShortName = "Email", Prompt = "Email Address")]
        [DataType(DataType.EmailAddress)]
        public string EmailAddress { get; set; }
 
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", 
                      MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; } 
    }
}

Next, we'll add an equivalent data model TestData.ts into our Angular app, right click the wwwroot\app folder, click Add, click New Folder, call the folder models. Next, right click this new folder, click Add, click New Item, change the name to TestData.ts from the default.

Change the content of this new file to the following:

C#
import { Component } from '@angular/core';
 
export class TestData {
    username: string;
    currency: number;
    emailAddress: string;
    password: string;
}

Now some of you will be already thinking hey this is repetitive, whatever happened to DRY "Don't Repeat Yourself"? True, for this initial cut, we're purposefully creating this small data model manually. Then, in the next parts of this series, we'll look refactoring this completely, and ultimately we'll automatically create all of our typescript data models and our typescript data services for Angular directly from our C# data model and Web API services.

This will mean we remove the bad coupling we'd often have, since changes to our data model, or data service methods won't cause a knock on effect, where we'd normally need to play catch up across our back end and front end.

Back to the code. We'll now update (just this once) our Angular service from the basic string array to instead cater for our newer, more complex data type, TestData. Update your existing SampleData.service.ts to this:

JavaScript
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import { TestData } from './models/testData';
@Injectable()
export class SampleDataService {
    private url: string = 'api/';
    constructor(private http: Http) { }
    getSampleData(): Observable<TestData> {
        return this.http.get(this.url + 'sampleData')
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }
 
    // from https://angular.io/docs/ts/latest/guide/server-communication.html

    private handleError(error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }
}

Next, we need to update our Angular component about.component.ts to handle the new TestData data type instead of the earlier, simpler string array.

JavaScript
import { Component, OnInit } from '@angular/core';
import { SampleDataService } from './services/sampleData.service';
import { TestData } from './models/testData';
 
@Component({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})
 
export class AboutComponent implements OnInit {
    testData: TestData = [];
    errorMessage: string;
    constructor(private sampleDataService: SampleDataService) { }
    ngOnInit() {
        this.sampleDataService.getSampleData()
            .subscribe((data: TestData) => this.testData = data,
            error => this.errorMessage = <any>error);
    }
}

Finally, so that we can view data now it is in shape of our new data type, TestData, we need to we'll alter our About view AboutComponent.cshtml to this:

HTML
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Example of Angular 2 requesting data from ASP.Net Core.</p>
 
<p>Data:</p>
 
<div *ngIf="testData != null">
    <table>
        <tr>
            <th> Username: </th>
            <td> {{ testData.username }} </td>
        </tr>
        <tr>
            <th> Currency: </th>
            <td> {{ testData.currency }} </td>
        </tr>
        <tr>
            <th> Email Address: </th>
            <td> {{ testData.emailAddress }} </td>
        </tr>
        <tr>
            <th> Password: </th>
            <td> {{ testData.password }} </td>
        </tr>
    </table>
</div>
 
<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

There's a couple of small changes here to cater for the flat data; previously the *ngFor loop handled empty data sets, so long as the data was initialized as an empty array. Here, we need to prevent using data that is not initialized as we've left the data as null into it gets populated by our service. Alternately, we could have initialized the empty data to ensure each property existed, and removed the *ngIf null detection, as the properties would exist and thereby not throw errors.

We could have used an arrays of our TestData object, but I've left this out of this section, in order that the purpose behind next steps are clearer and more obvious.

The last step in this section will be to alter our Web API controller to serve TestData shaped data instead of a simple string array.

C#
using Microsoft.AspNetCore.Mvc;
using A2SPA.ViewModels;
 
namespace A2SPA.Api
{
    [Route("api/[controller]")]
    public class SampleDataController : Controller
    {
        // GET: api/values
        [HttpGet]
        public TestData Get()
        {
            var testData = new TestData
            {
                Username="BillBloggs",
                EmailAddress = "bill.bloggs@example.com",
                Password= "P@55word",
                Currency = 123.45M
            };
 
            return testData;
        }
 
        // POST api/values
        [HttpPost]
        public void Post([FromBody]TestData value)
        {
        }
 
        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]TestData value)
        {
        }
 
        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Again this is simplified, and no database yet, as we're going to focus on the current bigger picture issues.
So let's build this, ensure it still works. Rebuild and hit Ctrl-F5:

the new improved sample data service in action

Normally, we'd not be displaying a password like this, and if we were entering a password it would be in a text box, so next we're going to see what our view would look like to display and enter properties from our TestData object.

We'll first do this the way most people do, by hand, and then we'll redo it the easy way - using tag helpers. And this will be where the fun really begins!

Angular Views - Data Entry and Data Display

When setting up an HTML form, irrespective of it being your design or not, you have something you want to look good to the end user but underneath something you will hook up to your front end code, and in case of a SPA, through a RESTful service to your backend code.

The Angular Hello World "Party Trick"

Using our simple sample data model as an example, we'll set up one column to display data, and another for data entry for each of our four data types. This will be something like the typical Angular "hello world" demo, where you'll be able to type data into an input form field and then as you type, see the text changes in another area of the page. This "party trick" is always impressive, and can usually get quite a bit of interest and excitement from management and end users, who have never seen 2 way data binding in action.

First, we'll create the required code by hand to see what a basic version of the backend code would look like. In this first step, we'll not use Razor or Tag Helpers, instead stick to plain HTML, CSS and Angular markup.

Return to your AboutComponent.cshtml page and change it to this:

HTML
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>
 
<p>Examples of Angular 2 data served by ASP.Net Core Web API:</p>
 
<form>
    <div *ngIf="testData != null">
        <div class="row">
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Entry</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label for="testDataUsername">Username</label>
                            <input type="text" id="testDataUsername" name="testDataUsername"
                                   class="form-control" placeholder="Username"
                                   [(ngModel)]="testData.username">
                        </div>
                        <div class="form-group">
                            <label for="testDataCurrency">Amount (in dollars)</label>
                            <div class="input-group">
                                <div class="input-group-addon">$</div>
                                <input type="number" id="testDataCurrency" 
                                       name="testDataCurrency"
                                       class="form-control" placeholder="Amount"
                                       [(ngModel)]="testData.currency">
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="testDataemailaddress">Email address</label>
                            <input type="email" id="testDataemailaddress" 
                                   name="testDataemailaddress"
                                   class="form-control" placeholder="Email Address"
                                   [(ngModel)]="testData.emailAddress">
                        </div>
                        <div class="form-group">
                            <label for="testDatapassword">Password</label>
                            <input type="password" id="testDatapassword" 
                                   name="testDatapassword"
                                   class="form-control" placeholder="Password"
                                   [(ngModel)]="testData.password">
                        </div>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label class="control-label">Username</label>
                            <p class="form-control-static">{{ testData.username }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Amount (in dollars)</label>
                            <p class="form-control-static">
                                {{ testData.currency | currency:'USD':true:'1.2-2' }}
                            </p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Email address</label>
                            <p class="form-control-static">{{ testData.emailAddress }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Password</label>
                            <p class="form-control-static">{{ testData.password }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

To support the updated validation, edit the custom styles in the file /wwwroot/css/site.css by adding these to the end of the file:

HTML
/* validation */
.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
} 

Save and hit Ctrl-F5 to build and view the changes; you should see a new About page looking like this:

Image 9

When this page is loaded, you'll see the data delivered from the service, on the left and right halves of the page, but as you alter the data on the left hand side, the right hand side will follow the changes.

Of course in a 'real' application, we'd never show the password in plain text, let alone store it in plain text, as we have here, but this is a sample app.

Manually Adding Some Validation

We're not adding validation covering everything everywhere, but once again, we'll add the required HTML, CSS and Angular markup manually, to see what some basic validation would look like, and get a feel for how these elements work.

Edit the AboutComponent.cshtml page and change it to this:

HTML
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Examples of Angular 2 data served by ASP.Net Core Web API:</p>
<form #testForm="ngForm">
    <div *ngIf="testData != null">
        <div class="row">
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Entry</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label for="username">Username</label>
                            <input type="text" id="username" name="username"
                                   required minlength="4" maxlength="24"
                                   class="form-control" placeholder="Username"
                                   [(ngModel)]="testData.username" #name="ngModel">
                            <div *ngIf="name.errors && (name.dirty || name.touched)"
                                 class="alert alert-danger">
                                <div [hidden]="!name.errors.required">
                                    Name is required
                                </div>
                                <div [hidden]="!name.errors.minlength">
                                    Name must be at least 4 characters long.
                                </div>
                                <div [hidden]="!name.errors.maxlength">
                                    Name cannot be more than 24 characters long.
                                </div>
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="currency">Payment Amount (in dollars)</label>
                            <div class="input-group">
                                <div class="input-group-addon">$</div>
                                <input type="number" id="currency" name="currency"
                                       required
                                       class="form-control" placeholder="Amount"
                                       [(ngModel)]="testData.currency" #currency="ngModel">
                            </div>
                            <div *ngIf="currency.errors && 
                                  (currency.dirty || currency.touched)"
                                 class="alert alert-danger">
                                <div [hidden]="!currency.errors.required">
                                    Payment Amount is required
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="emailAddress">Email address</label>
                            <input type="email" id="emailAddress" name="emailAddress"
                                   required minlength="6" maxlength="80"
                                   pattern="([a-zA-Z0-9_\-\.]+)@@
                                   ((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
                                   (([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})"
                                   class="form-control" placeholder="Email Address"
                                   [(ngModel)]="testData.emailAddress" #email="ngModel">
                            <div *ngIf="email.errors && (email.dirty || email.touched)"
                                 class="alert alert-danger">
                                <div [hidden]="!email.errors.required">
                                    Email Address is required
                                </div>
                                <div [hidden]="!email.errors.pattern">
                                    Email Address is invalid
                                </div>
                                <div [hidden]="!email.errors.minlength">
                                    Email Address must be at least 6 characters long.
                                </div>
                                <div [hidden]="!email.errors.maxlength">
                                    Email Address cannot be more than 80 characters long.
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <label for="password">Password</label>
                            <input type="password" id="password" name="password"
                                   required minlength="8" maxlength="16"
                                   class="form-control" placeholder="Password"
                                   [(ngModel)]="testData.password">
                        </div>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label class="control-label">Username</label>
                            <p class="form-control-static">{{ testData.username }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Payment Amount (in dollars)</label>
                            <p class="form-control-static">
                                {{ testData.currency | currency:'USD':true:'1.2-2' }}
                            </p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Email address</label>
                            <p class="form-control-static">{{ testData.emailAddress }}</p>
                        </div>
                        <div class="form-group">
                            <label class="control-label">Password</label>
                            <p class="form-control-static">{{ testData.password }}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

/* validation */
.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
} 

Save the files, and again hit Ctrl-F5 to rebuild and launch your browser, navigate to the About page and this time, you should see much the same as before, but now including a few basic items of validation:

Image 10

Try changing the username to less than 4 characters and as you make the field invalid, you'll see a validation error message:

Image 11

Navigate away from the form field and you'll clearly see the error style:

Image 12

Instead of reinventing the wheel, and covering how to do validation, please see the excellent Angular 2 tutorial site by the Angular 2 dev team here and here.

What Could Possibly Go Wrong Here?

Now that we have a little more to demonstrate and test, at this point, we're going to pause and cover some of the issues that may arise when trying to use ASP.NET Core views, Razor and Angular 2 together.

Escaping @ Symbols

The @ symbol needs to be 'escaped', as it has a loaded meaning in Razor. In the above code, I've included the already escaped @ symbol as @@, which gets translated to a single @. Code (from above) for the email entry and email regex validation is a good example of this:

HTML
<input type="email" id="emailAddress" name="emailAddress"
   required minlength="6" maxlength="80"
   pattern="([a-zA-Z0-9_\-\.]+)@@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
            (([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})"
   class="form-control" placeholder="Email Address"
   [(ngModel)]="testData.emailAddress" #email="ngModel">

If escaped as @@ our source from Visual Studio looks like this:

Image 13

If left as a single @, it will trigger Razor syntax parsing and look like this:

Image 14

All we need to do then is change any @ to @@ and similarly, if there happens to be a case of the original having two @ symbols, simply double it, 2 becomes 4, and then when passed through the Razor parser, each set of two @ symbols returns back to a single @ symbol.

Missing and Mismatched HTML Tags

The next issue is not so much an issue of ASP.NET Core, or Razor, but with Angular which happens to be particularly sensitive to missing HTML tags and mismatched HTML.

To see what this looks like, we'll purposefully remove a closing div tag, shown highlighted on line 33 below. With the </div> left in place:

Image 15

Now with this single </div> removed, save and hit Ctrl-F5, the page starts loading but the loader message does not go away. Hit F12 and look at the console and you'll see:

Image 16

Expanding the above error in the console will not usually give you a clear idea of what is happening, so perhaps the best advice I can give is, if you see an error similar to the above, then return to the HTML template, and look through the markup (and though somewhat artificial) look at line 9 and you'll see an indication of a typical error you need to look for, the green squiggly line:

Image 17
Hover your mouse over the green line and you'll see more detail:

Image 18

In practice, you'll likely still need to do some work to find precisely where the DIV mismatch occurs, as the mismatch warning is not necessarily on the tag that was missing the closing div, but on the first div tag directly affected by the missing closing tag.

Angular Views Using ASP.NET Core Tag Helpers

What are Tag Helpers?

A quick aside, for those more familiar with Angular and less familiar with ASP.NET Core. Tag helpers are similar in some respect to Angular directives; they are added to a page and at a glance, they look like a custom HTML tag.

Tag helpers are processed on the server by ASP.NET Core. They come in two main flavours, (i) built in tag helpers, and (ii) custom tag helpers - which you can create and customise yourself. They might best be explained by an example.

This example is from the documentation here, where we start with a data model. Update CSHTML markup using built in tag helpers:

Image 19

And then this HTML is produced by ASP.NET Core and sent to the browser:

Image 20

Tag Helpers (built in and fixed) feel like HMTL to a developers or designers. Because they work like ordinary HTML, people more familiar with HTML and less familiar with server side Razor syntax or client side code can work on your pages without getting lost in the code around the tag helper markup.

Tag Helpers can also support standard style sheet classes and, unlike Angular directives, provide extensive intellisense (code hints and lookup) within Visual Studio to assist developer and designer alike.

Lastly, and in my opinion, the biggest advantage is reduction of repeated code, keeping the code DRY - the Don't Repeat Yourself principle, to save time and money as we'll have less code to write and for the longer term, generate less code to maintain.

By now, you have figured where we're heading, we'll be custom tag helpers. However we'll take it to another level, as we'll use our tag helpers to create as much of our Angular markup at the same time, as possible.

Instead of multiple copies of many very similar HTML labels, input tags, and styling across a site you can have your tag helper backend code all in one place while the smaller tag helper markup holds the vital information about each instance.

To read a little more on the topic of tag helpers, please see the ASP.NET Core docs here, or Dave Paquette's article here.

We're going to make two custom tag helpers, one for data entry and the other for displaying data. These tag helpers will dynamically create all of the labels, basic styling and the form fields for our page as we can.

Data Validation?

When developers switch from conventional ASP.NET using razor syntax and tag helpers to build a SPA (or single page application), one of the biggest losses to the team can be the added burden of generating client-side data validation.

Server side validation is easily automated and generated dynamically using the metadata from the attributes decorating the data model. Some people have used code generation, to create the client side JavaScript required, other create data validation services from the metadata, validation data traffic can be an issue and we tend to still create too much bad coupling.

The alternative I am proposing here is that we use our custom tag helpers to create the bulk of our client side validation and that we still use server-side data validation as before. If there are complex database lookups required for some client side data I'd still recommend a hand created validation service, however the bulk of our work can be automated.

First a little housekeeping, we'll update the Microsoft.AspNetCore.MVC assembly and Microsoft.AspNetCore.SpaServices assembly using NuGet:

Image 21

Among other things, the recent update of the ASP.NET Core MVC corrects potential security issues, see here for details.

Displaying Data With a Custom Tag Helper

We'll begin with the simpler of our two tag helpers, used to display data.

When designing the tag helper, you will find it helpful to begin with some idea of what you want to achieve. That's not to say you can't change it later, but if you have a good example of the layout, the styles used and tags required, then you can avoid change later.

If you're building a larger website and have just yourself or a team of developers, all waiting on a designer or agency to supply the final look-and feel, then you can still start, and add this later, although it will always be easier to have these final designs as early as possible.

For our code, we'll start with an example of some of the HTML that we'd like to reproduce:

HTML
    <div class="form-group">
    <label class="control-label">Username</label>
    <p class="form-control-static">{{ testData.username }}</p>
</div>
<div class="form-group">
    <label class="control-label">Payment Amount (in dollars)</label>
    <p class="form-control-static">
        {{ testData.currency | currency:'USD':true:'1.2-2' }}
    </p>
</div>     

Next, we'll choose a suitable tag to use for our tag helper that will not conflict with other regular HTML, tag helper or Angular directives we'll want to use. I generally pick something short but descriptive, so for the sake of this, I'm using <tag-dd> from "tag" as it stands out (but this could be related to the project name), and "dd" for data display (can be anything memorable, say "cart" or "sc" for shopping-cart).

Create a folder at the root level of the application called say "Helpers", and then hit Shift-Alt-C (or create a new C# class file), call it TagDdTagHelper.cs.

As low level details on creating a custom tag helper are already well covered in a number of other places, consistent with this series of articles, we will not be going into very low level details.

If you need more, these links might help: ASP.NET Core documentation here, Dave Paquette's post here or the ASP.NET monster's videos covering tag helpers, over at MSDN Channel 9 here.

Before we go further, right click the project root and using your NuGet Package manager, add the package Humanizer.xproj:

Image 22

NOTE: Do not add the ordinary Humanizer package, the non .xproj version of Humanizer will install but not build.

Image 23

While here in NuGet, we'll pick up those updates:

Image 24

And we'll add some tooling to Razor (we'll use this later), just browse for the package "Microsoft.AspNet.Tooling.Razor" and then click "Install" button:

Image 25

Now update the new TagDdTagHelper.cs class to contain the following code:

C#
using Humanizer;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{ testData." + For.Name.Camelize() + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
    }
}

Edit your AboutComponent.cshtml file, add this to the very top of the markup:

HTML
addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*,A2SPA"
@model A2SPA.ViewModels.TestData

And then scroll down to the existing markup that displays the username. Initially, we'll add our new tag-helper tag beside the existing markup, you'll notice that we can see intellisense at work in the image below, in this instance, hinting at the properties available to us:

Image 26

Choose "username", then hit Enter.

Sadly, the current tooling emits a lowercased property name, so for now, alter the first character to uppercase, so that it reads for="Username" instead of for="username".

The code segment should now read:

HTML
...<div class="panel-heading">Data Display</div>
<div class="panel-body">
    <div class="form-group">
        <label class="control-label">Username</label>
        <p class="form-control-static">{{ testData.username }}</p>
    </div>
 
    <tag-dd for="Username"></tag-dd>
 
    <div class="form-group">...

Rebuild and it Ctrl-F5 to view, you should now see username twice:

Image 27

Note: If you get an error here, double check you have not missed the manual upper case needed on the model property name.

Well we see the username but no label. The reason is our tag helper is relying on the data model now, instead of hand-coded text.

The fix is simple, we'll update the metadata in the data model, /ViewModels/TestData.cs to this:

C#
using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        public string Username { get; set; }
 
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }

To add the description meta data, like this:

C#
using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        [Display(Description = "Username")]
        public string Username { get; set; }
 
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }

Rebuild again and it Ctrl-F5 to view, this time you should now see both username label and data twice:

Image 28

And in the F12 debug view, these should have identical markup.

Image 29

In the inspector, we see the final DOM after Angular has completed rendering, if we look over in the network tab, we'll see there is only a slight difference, our tag helper has generated a little less whitespace:

Image 30

Before we add our other datatypes, time for some refactoring.

Improving Our Tag Helper

Currently, the first cut of the data display tag helper is functional, but quite crude. Ideally, we want to automate everything we can, and where possible, use convention over configuration.

What's convention over configuration? Time saving + automation. Here's how it works.

Look at our TagDaTagHelper.cs class where we create the name of the Angular object we're binding to:

C#
pTag.InnerHtml.Append("{{ testData." + For.Name.Camelize() + "}}");

How does this work? When we pass in the For attribute value, "Username" it's treated as a property, recall the tag helper attribute, from our TagDaTagHelper.cs code:

HTML
/// <summary>
/// Name of data property 
/// </summary>
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }

when we enter this in the AboutComponent.cshtml view, it looks like a string:

HTML
<tag-dd for="Username"></tag-dd>

But when we reference it in code, it's treated as a ModelExpession, so we take it and get the .Name property.

Next we use the Humanizer package's "Camelize" method to take the name of the C# data model property ("Username", where the convention is Pascal Case) and make it into a suitable Angular property name ("username", where the convention is Camel Case).

The next part of the string hardcoded for now, adds "testData." to our camelized property name. The reason is that we have an object that will exist in our client called testData.

Configuration would have us add another attribute to the custom tag, while convention says "save time, assume it matches, and configure only if convention doesn't fit".

So let's modify our tag helper, now the developer(s) all assuming a rule or "convention" exists that says name objects and properties consistently between Angular data models and C# data models, and use a tool such as Humanizer to make Pascal Cased wording into Camel Cased wording.

C#
var className = For.Metadata.ContainerType.Name;
pTag.InnerHtml.Append("{{" + className.Camelize() + "." + For.Name.Camelize() + "}}");

It so happens we can get the parent class name by using For.MetaData.ContainerType.Name and next, we'll extract the property name as well:

C#
var className = For.Metadata.ContainerType.Name;
var propertyName = For.Name;
pTag.InnerHtml.Append("{{" + className.Camelize() + "." + propertyName.Camelize() + "}}");

Look at the AboutComponent.cshtml view and you'll see there's a lot of places using the same className.propertyName style Angular object, so let’s create a method to do this for us that we'll use in our second tag helper too.

C#
using Humanizer;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{" + CamelizedName(For) + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
 
        private static string CamelizedName(ModelExpression modelExpression)
        {
            var className = modelExpression.Metadata.ContainerType.Name;
            var propertyName = modelExpression.Name;
 
            return className.Camelize() + "." + propertyName.Camelize();
        }
    }
}

For cleanliness, I like to move helper methods like this into a separate class file, or at least into a new class which makes it easier for people to find any shared methods. Create a new class called VariableNames.cs, in the Helpers folder:

Image 31

Next, move the new method "CamelizedName" into the new class. Last of all, we'll change our new CamelisedName method into an extension method; alter the method to public, change the VariableNames class to static, and alter the method signature, adding "this". We need to add a couple of dependencies, so that the end result of VariableNames.cs is this:

C#
using Humanizer;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
 
namespace A2SPA.Helpers
{
    public static class VariableNames
   {
        public static string CamelizedName(this ModelExpression modelExpression)
        {
            var className = modelExpression.Metadata.ContainerType.Name;
            var propertyName = modelExpression.Name;
 
            return className.Camelize() + "." + propertyName.Camelize();
        }
    }
}

And our tag helper TagDaTagHelper.cs is now this:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{" + For.CamelizedName() + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
    }
}

BTW, we could clean up the code around this string concatentation:

HTML
pTag.InnerHtml.Append("{{" + For.CamelizedName() + "}}");

But this replacement with string.Format means we'd have to "escape" the "{" symbol and "}" symbol, and end up with somewhat unreadable code:

HTML
pTag.InnerHtml.Append(string.Format("{{{{ {0} }}}}", For.CamelizedName()));

Finishing the First Cut of Our Data Display Tag Helper

We can now update our view AboutComponent.cshtml adding our new tag-helper tags in place of the existing markup, but leaving the currency value in place for now, as we need to add custom pipes.

HTML
...
<div class="panel-body">
    <tag-dd for="Username"></tag-dd>
 
    <div class="form-group">
        <label class="control-label">Payment Amount (in dollars)</label>
        <p class="form-control-static">
            {{ testData.currency | currency:'USD':true:'1.2-2' }}
        </p>
    </div>
 
    <tag-dd For="Currency"></tag-dd>
 
    <tag-dd For="EmailAddress"></tag-dd>
 
    <tag-dd For="Password"></tag-dd>
</div>
...

Again, don't forget to manually uppercase the property names. (Hopefully, this will be fixed in a later release of the ASP.NET Core MVC assembly).

Also needed, a couple of further updates to our view model, TestData.cs, adding descriptions to the other properties so that our labels are populated automatically:

C#
using System.ComponentModel.DataAnnotations;
 
namespace A2SPA.ViewModels
{
    public class TestData
    {
        [Display(Description = "Username")]
        public string Username { get; set; }
 
        [Display(Description = "Payment Amount (in dollars)")]
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }
 
        [Required, RegularExpression(@"([a-zA-Z0-9_\-\.]+)@
        ((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))
        ([a-zA-Z]{2,4}|[0-9]{1,3})", ErrorMessage = "Please enter a valid email address.")]
        [EmailAddress]
        [Display(Description = "Username", Name = "EmailAddress", 
         ShortName = "Email", Prompt = "Email Address")]
        [DataType(DataType.EmailAddress)]
        public string EmailAddress { get; set; }
 
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", 
                                           MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Description = "Password", Name = "Password")]
        public string Password { get; set; } 
    }
}

Once again, build and hit Ctrl-F5 to re-display:

Image 32

And you can see the last part remaining in this first tag helper:

HTML
<div class="form-group">
    <label class="control-label">Payment Amount (in dollars)</label>
    <p class="form-control-static">
        {{ testData.currency | currency:'USD':true:'1.2-2' }}
    </p>

We need a way to add special formatting. Here you have a choice, you can fix it, either

  1. adding the pipe text into the tag helper so that all instances of currency use it, or
  2. dynamically add the country specific details based on the client/browser's language, or the server language settings, or
  3. add an optional attribute that you would need to add whenever you use currency, but this means you could at least customize different occurrences in your code, or
  4. a combination of the above, perhaps (i) as a default do all instances, and (iii) allow customization as well.

We'll be using (iii), adding an optional pipe attribute.

Add this optional attribute into our TagDaTagHelper.cs code, alongside the "For" attribute:

C#
[HtmlAttributeName("pipe")]
public string Pipe { get; set; } = null;

Then using this code:

C#
var pipe = string.IsNullOrEmpty(Pipe) ? string.Empty : Pipe;

and:

C#
pTag.InnerHtml.Append("{{" + For.CamelizedName() + pipe + "}}");

Here is the final data display tag helper code:

C#
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace A2SPA.Helpers
{
    [HtmlTargetElement("tag-dd")]
    public class TagDdTagHelper : TagHelper
    {
        /// <summary>
        /// Name of data property 
        /// </summary>
        [HtmlAttributeName("for")]
        public ModelExpression For { get; set; }
 
        /// <summary>
        /// Option: directly set display format using Angular 2 pipe and pipe format values
        /// </summary>
        ///<remarks>This attribute sets both pipe type and the pipe filter parameters.
        /// For simple formatting of common data types <seealso cref="Format"/>.
        /// Numeric formats for decimal or percent in Angular 
        /// use a string with the following format: 
        /// a.b-c where:
        ///     a = minIntegerDigits is the minimum number of integer digits 
        ///         to use.Defaults to 1.
        ///     b = minFractionDigits is the minimum number of digits 
        ///         after fraction.Defaults to 0.
        ///     c = maxFractionDigits is the maximum number of digits 
        ///         after fraction.Defaults to 3.
        /// </remarks>
        /// <example>
        /// to format a decimal value as a percentage use "|percent" for the default Angular
        /// or for a custom percentage value e.g.,. "| percent:'1:3-5' 
        /// </example>
        [HtmlAttributeName("pipe")]
        public string Pipe { get; set; } = null;
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var pipe = string.IsNullOrEmpty(Pipe) ? string.Empty : Pipe;
 
            var labelTag = new TagBuilder("label");
            labelTag.InnerHtml.Append(For.Metadata.Description);
            labelTag.AddCssClass("control-label");
 
            var pTag = new TagBuilder("p");
            pTag.AddCssClass("form-control-static");
            pTag.InnerHtml.Append("{{" + For.CamelizedName() + pipe + "}}");
 
            output.TagName = "div";
            output.Attributes.Add("class", "form-group");
 
            output.Content.AppendHtml(labelTag);
            output.Content.AppendHtml(pTag);
        }
    }
}

Then we need to update our view to this:

HTML
<tag-dd For="Currency" pipe="| 
currency:'USD':true:'1.2-2'"></tag-dd>

Which results in this, when we build and hit Ctrl-F5 again:

Image 33

Here then is an excerpt from the final AboutComponent.cshtml code:

HTML
<div class="panel-body">
    <tag-dd for="Username"></tag-dd>
 
    <tag-dd For="Currency" pipe="| currency:'USD':true:'1.2-2'"></tag-dd>
 
    <tag-dd For="EmailAddress"></tag-dd>
 
    <tag-dd For="Password"></tag-dd>
</div>

Obviously, you can extend this simple version of the tag helper code to handle other data types, allow custom classes, formatting, different colors or whatever you wish.

As an example, our password would never be shown in plain text like this, so for the sake of this simple demo, we'll modify our tag helper to hide the password (of course, you'd never send it from your Web API get method and you would have salted and hashed the password, rather than the simple plain text example we have here).

Look at the data model, notice that despite being a string property that we've decorated our view model with a specific data type to further hint at the purpose of the data.

C#
[DataType(DataType.Password)]
[Display(Description = "Password", Name = "Password")]
public string Password { get; set; }

So if we check on this data type in our tag helper, we can change what we emit; instead of creating an Angular data binding expression, we simply create a short string. This change:

C#
var dataBindExpression = ((DefaultModelMetadata)For.Metadata).DataTypeName == "Password" 
                                    ? "******" 
                                    : "{{" + For.CamelizedName() + pipe + "}}";
 
pTag.InnerHtml.Append(dataBindExpression);

Will render asterisks in place of the password.

Reality check: Don't do this in your production code … please! Again, this is just a simple to understand example!

Image 34

or handle optional prefixes (instead of assuming that we're always using the class name of the view model, but whatever direction you take, look for patterns in your client side code that you can reduce into a tag helper, try to use data types, data model metadata and anything else you can to automate code creation and reduce special cases.

Your code will be simpler, and allow you the flexibility to change the code in one place, the next time someone asks for a change.

The source for this is on Github here, or available for download here.

In the next part of the series, we'll be creating another custom tag helper, this time for data input.

Points of Interest

Did you know Angular 2 will not be followed by Angular 3, but instead Angular 4!

History

License

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

Share

About the Author

Robert_Dyball
Software Developer (Senior)
Australia Australia
Robert works as a full stack developer in the financial services industry. He's passionate about sustainable software development, building solid software and helping to grow teams. He's had 20+ years of development experience across many areas that include scheduling, logistics, telecommunications, manufacturing, health, insurance and government regulatory and licensing sectors. Outside work he also enjoys electronics, IOT and helping a number of non-profit groups.

Comments and Discussions

 
Questioncould not find .map Anyone else? Pin
matthew herb19-May-17 7:42
Membermatthew herb19-May-17 7:42 
AnswerRe: could not find .map Anyone else? Pin
Robert_Dyball19-May-17 11:44
professionalRobert_Dyball19-May-17 11:44 
AnswerRe: could not find .map Anyone else? Pin
Member 126509205-Jun-17 6:18
MemberMember 126509205-Jun-17 6:18 
GeneralRe: could not find .map Anyone else? Pin
Robert_Dyball5-Jun-17 11:44
professionalRobert_Dyball5-Jun-17 11:44 
QuestionAngular scripts (bundle) Pin
ptlh196921-Apr-17 0:15
Memberptlh196921-Apr-17 0:15 
AnswerRe: Angular scripts (bundle) Pin
Robert_Dyball22-Apr-17 1:23
professionalRobert_Dyball22-Apr-17 1:23 
QuestionRe: Angular scripts (bundle) Pin
ptlh196924-Apr-17 18:07
Memberptlh196924-Apr-17 18:07 
AnswerRe: Angular scripts (bundle) Pin
Robert_Dyball26-Apr-17 0:04
professionalRobert_Dyball26-Apr-17 0:04 
PraiseBest Core+Angular2 article in the web Pin
Barral Luis19-Mar-17 11:11
MemberBarral Luis19-Mar-17 11:11 
GeneralRe: Best Core+Angular2 article in the web Pin
Robert_Dyball19-Mar-17 14:48
professionalRobert_Dyball19-Mar-17 14:48 
PraiseMy Vote of 5 Pin
hmdvs17-Mar-17 0:02
Memberhmdvs17-Mar-17 0:02 
GeneralRe: My Vote of 5 Pin
Robert_Dyball17-Mar-17 19:11
professionalRobert_Dyball17-Mar-17 19:11 
GeneralMessage Closed Pin
23-Mar-17 19:27
MemberMember 1301837623-Mar-17 19:27 
GeneralMy vote of 5 :) Pin
fatih öztürk9-Mar-17 20:04
Memberfatih öztürk9-Mar-17 20:04 
GeneralRe: My vote of 5 :) Pin
Robert_Dyball17-Mar-17 19:10
professionalRobert_Dyball17-Mar-17 19:10 
Questionduh... is it only me having issue running the application Pin
Bryian Tan19-Feb-17 17:41
professionalBryian Tan19-Feb-17 17:41 
AnswerRe: duh... is it only me having issue running the application Pin
Robert_Dyball19-Feb-17 17:46
professionalRobert_Dyball19-Feb-17 17:46 
GeneralRe: duh... is it only me having issue running the application Pin
Bryian Tan19-Feb-17 17:49
professionalBryian Tan19-Feb-17 17:49 
GeneralRe: duh... is it only me having issue running the application Pin
Robert_Dyball19-Feb-17 17:52
professionalRobert_Dyball19-Feb-17 17:52 
GeneralRe: duh... is it only me having issue running the application Pin
Bryian Tan19-Feb-17 17:59
professionalBryian Tan19-Feb-17 17:59 
GeneralRe: duh... is it only me having issue running the application Pin
Robert_Dyball19-Feb-17 18:38
professionalRobert_Dyball19-Feb-17 18:38 
GeneralRe: duh... is it only me having issue running the application Pin
Bryian Tan19-Feb-17 18:47
professionalBryian Tan19-Feb-17 18:47 
Questionthe project is configured to use .net core sdk version 1.0.0-preview2-1-003177 Pin
Bryian Tan19-Feb-17 11:52
professionalBryian Tan19-Feb-17 11:52 
AnswerRe: the project is configured to use .net core sdk version 1.0.0-preview2-1-003177 Pin
Robert_Dyball19-Feb-17 14:50
professionalRobert_Dyball19-Feb-17 14:50 
GeneralRe: the project is configured to use .net core sdk version 1.0.0-preview2-1-003177 Pin
Bryian Tan19-Feb-17 14:58
professionalBryian Tan19-Feb-17 14:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.