The following tutorial uses .NET Core 1.0.3 SDK Preview 2 build 3156 which can be downloaded from https://github.com/dotnet/core/blob/master/release-notes/download-archives/1.0.3-preview2-download.md. Future version of .NET Core may not work with this tutorial.

Angular 2 + .NET Core Tutorial Part IV: Forms and Pipes

In the first three tutorials, an Angular 2 + .NET Core project was set up and coded. In this tutorial, the project will be expanded to provide a form for creating, modifying and deleting widgets. Additionally, a built-in pipe and custom pipe will be used to format data.

The application in this tutorial builds off the application completed in the previous tutorial. If you wish to complete part 3 of this tutorial, please visit https://www.accelebrate.com/library/tutorials/angular2-net-core-routing-services and complete the tutorial. Then come back here and use that application to complete this tutorial. If you wish to start this tutorial without completing the first one, download the starter files for this tutorial from the following GitHub repository:

https://github.com/training4developers/dotnet-core-angular2-routing-services

If you wish to download the completed application to use with this tutorial, you may download it from the following GitHub repository:

https://github.com/training4developers/dotnet-core-angular2-forms-pipes

For the backend database, a local SQLite database file will be used. With a few slight modifications, the database could easily be switched to another platform (i.e., SQL Server or PostgreSQL), because the repository pattern and Entity Framework will be used by the REST services to retrieve data from the database.

Enhancing the REST Service to Insert, Update, and Delete Widgets

Four additional endpoint methods need to be added to the REST service. One POST endpoint is needed to insert a new widget. A PUT and PATCH endpoint are used to update a widget, and a delete endpoint to delete a widget. Generally PUT methods are implemented as replace, however, this application will implement it as update.

PATCH is commonly used for updates even though PATCH is not an official HTTP verb. For this application, PATCH will simply invoke the same method as PUT.

Finally, GET for a single widget will be modified to be a named route so that POST can redirect to it after the widget is created. This makes it possible to get the widget’s id as it is part of the response from the POST request.

In addition to these new endpoints, several new controller actions for the Widgets controller need to be coded as well. First, open the file Widgets.cs in the Controllers folder and then add the following methods after the Get(int id) method:

[HttpPost]
public IActionResult Insert([FromBody] Widget widget)
{
    if (widget == null)
    {
        return BadRequest();
    }
    
    _widgetRepo.Insert(widget);
    
    return CreatedAtRoute("GetWidget", new { id = widget.Id }, widget);
}   

[HttpPut("{widgetId}")]
public IActionResult Update(int widgetId, [FromBody] Widget widget)
{
    if (widget == null || widgetId != widget.Id)
    {
        return BadRequest();
    }

    if (_widgetRepo.Update(widget) == null)
    {
        return NotFound();
    }

    return NoContent();
}   

[HttpPatch("{widgetId}")]
public IActionResult Update([FromBody] Widget widget, int widgetId)
{
    return this.Update(widgetId, widget);
}

[HttpDelete("{widgetId}")]
public IActionResult Delete(int widgetId)
{
    if (_widgetRepo.Delete(widgetId) == null)
    {
        return NotFound();
    }

    return NoContent();
}

In order to provide the various data operations for the new REST endpoints, the Widget repository needs a new method which will allow the endpoint method to query the database for a single widget. Open the file named WidgetRepo.cs in the Data/Models folder, then add the following methods after the Get(int id) method in the repository class:

public IWidget Insert(IWidget widget)
{
    
    var newWidget = new WidgetData
    {
        Name = widget.Name,
        Description = widget.Description,
        Color = widget.Color,
        Size = widget.Size,
        Quantity = widget.Quantity,
        Price = widget.Price
    };

    _dbContext.Widgets.Add(newWidget);
    _dbContext.SaveChanges();

    widget.Id = newWidget.Id;

    return widget;
}

public IWidget Update(IWidget widget)
{
    var findWidget = _dbContext.Widgets.SingleOrDefault(w => w.Id == widget.Id);

    if (findWidget == null)
    {
        return null;
    }

    findWidget.Name = widget.Name;
    findWidget.Description = widget.Description;
    findWidget.Color = widget.Color;
    findWidget.Size = widget.Size;
    findWidget.Quantity = widget.Quantity;
    findWidget.Price = widget.Price;

    _dbContext.SaveChanges();

    return widget;
}

public IWidget Delete(int widgetId)
{
    var widget = _dbContext.Widgets.SingleOrDefault(w => w.Id == widgetId);

    if (widget == null)
    {
        return null;
    }

    _dbContext.Widgets.Remove(widget);
    _dbContext.SaveChanges();

    return new Widget
    {
        Id = widget.Id,
        Name = widget.Name,
        Description = widget.Description,
        Color = widget.Color,
        Size = widget.Size,
        Quantity = widget.Quantity,
        Price = widget.Price
    };
}

Lastly, the IWidgetRepo interface needs to be updated for the new methods. Start by opening the file named IWidgetRepo.cs in the Interfaces folder and then add the following method definition after the Get(int id) definition:

IWidget Insert(IWidget widget);
IWidget Update(IWidget widget);
IWidget Delete(int widgetId);

These changes complete the .NET Core modifications needed to support the insertion, modification, and deletion of widgets through the REST service. To summarize the steps: action methods are added to the controller, methods are added to the repository class, and the additional method definitions are added to the interface class.

Enhance the Widget Service to Insert, Update, and Delete Widgets

With the REST service updated, the Angular 2 service Widgets must be updated to call those endpoints. Begin by opening the file named widgets.ts in the folder wwwrootsrc/js/app/services. Next, add the following methods following the get(widgetId: number) method:

// insert a new widget and return the widget that will contain its id
insert(widget: Widget): Observable {
    return this.http.post(`/widgets`, JSON.stringify(widget),
        this.requestOptions).map(res => res.json());
}

// only return Observable when using 204 NoContent for REST service responses
update(widget: Widget): Observable {
    return this.http.put(`/widgets/${encodeURIComponent(widget.id.toString())}`,
        JSON.stringify(widget), this.requestOptions);
}

// delete a widget by id
delete(widgetId: number): Observable {
    return this.http.delete(`/widgets/${encodeURIComponent(widgetId.toString())}`);
}

With these changes made to the methods, the Widgets service is completed.

Formatting Data with Pipes

Pipes were previously known as “Filters” in Angular 1. Because of their syntax within a template, they were renamed to “Pipes” in Angular 2. Pipes are used to transform data. Angular 2 comes with many built-in types for formatting dates, currency and text. In addition to using the built-in pipes, custom pipes can be created.

For this application, the built-in current pipe will be used and a custom capitalize pipe will be created and used. In this section, the pipe will be defined and registered with the AppModule. In the WidgetTable component update section, the pipes will be applied to table.

To create pipes, a new folder named pipes needs to be added to the wwwrootsrc/js/app folder. Next, add a new file in the pipes folder named capitalize.pipe.ts. Then add the following code to the file:

import { Pipe, PipeTransform } from "@angular/core";

// Pipe decorator allows the name of the pipe to be specified,
// the pipe name will be the name used in the template to apply
// the pipe
@Pipe({ name: 'capitalize'})
export class CapitalizePipe implements PipeTransform {

    // Alll pipes must implement this method.
    // The first parameter is the value piped in.
    // Additional parameters can be specified for
    // colon-delimited arguments that can be passed
    // into pipes.
    public transform(value: string) {
        return value.charAt(0).toUpperCase() + value.slice(1);
    }

}

The CapitalizePipe class needs to be registered with the AppModule. Just like components, it must be registered in the declarations section so that it can be referenced in templates.

Open the file named app.module.ts in the wwwrootsrc/js/app folder. Then add the following lines of code after the import WidgetView statement:

// Imports the pipes to be registered with the application
// so they can be referenced in the templates
import { CapitalizePipe } from "./pipes/capitalize.pipe";

Now add the CapitalizePipe class to the NgModule declarations array:

declarations: [ AppComponent, WidgetTable, WidgetView, CapitalizePipe ],

This will make the CapitalizePipe pipe available in the module for use in template expressions. In the upcoming sections, the WidgetEdit component will be added.

Create a Widget Edit Component

To create or edit a widget, a widget form is needed. The widget form will be another component in the application. In the folder wwwrootsrc/js/app/components, create a new folder named widget-edit. Next, in this folder, three new files will be created. The first file will be an empty SASS file for component-level styling. The second file will be a HTML file for the component template. In the third, a TypeScript file will be added containing the class for the component.

Create a new file named widget-edit.component.scss in the wwwrootsrc/js/app/components/widget-edit folder. This file will remain empty.

Create a new file named widget-edit.component.html in the wwwrootsrc/js/app/components/widget-edit folder. Add the following HTML markup to the file:

<form novalidate>

    <div class="form-group">
        <label for="widget-name-input">Name:</label>
        <input type="text" id="widget-name-input" required #widgetNameInput="ngModel"
            name="widgetName" [(ngModel)]="widget.name">
        <span *ngIf="widgetNameInput.invalid">Please enter a widget name.</span>
    </div>
    <div class="form-group">
        <label for="widget-description-input">Description:</label>
        <textarea id="widget-description-input"
            name="widgetDescription" [(ngModel)]="widget.description"></textarea>         
    </div>
    <div class="form-group">
        <label for="widget-color-select">Color:</label>
        <select id="widget-color-select" required #widgetColorSelect="ngModel"
            name="widgetColor" [(ngModel)]="widget.color">
            <option value="">Select One...</option>
            <option *ngFor="let color of colors" [value]="color.value">{{color.label}}</option>
        </select>
        <span *ngIf="widgetColorSelect.invalid">Please select a widget color.</span>        
    </div>
    <div class="form-group">
        <label for="widget-size-input">Size:</label>
        <select id="widget-size-select" required #widgetSizeSelect="ngModel"
            name="widgetSize" [(ngModel)]="widget.size">
            <option value="">Select One...</option>
            <option *ngFor="let size of sizes" [value]="size.value">{{size.label}}</option>
        </select>
        <span *ngIf="widgetSizeSelect.invalid">Please select a widget size.</span>        
    </div>
    <div class="form-group">
        <label for="widget-quantity-input">Quantity:</label>
        <input type="number" id="widget-quantity-input" required #widgetQuantityInput="ngModel"
            name="widgetQuantity" [(ngModel)]="widget.quantity">
        <span *ngIf="widgetQuantityInput.invalid">Please enter a widget quantity.</span>        
    </div>
    <div class="form-group">
        <label for="widget-price-input">Price:</label>
        <input type="number" id="widget-price-input" required  #widgetPriceInput="ngModel"
            name="widgetPrice" [(ngModel)]="widget.price">
        <span *ngIf="widgetPriceInput.invalid">Please enter a price quantity.</span>        
    </div>

    <button type="button" (click)="saveWidget(widget)">Save</button>
    <button type="button" *ngIf="widget.id" (click)="deleteWidget(widget.id)">Delete</button>
    <button type="button" (click)="returnToList()">Return to List</button>

</form>

This form uses NgModel to collect the form data as well as perform form validation. The hash tag references are known as template references. When the template reference is assigned the value of NgModel, the template can access the NgModel object for each input control. Using this object, the directive NgIf can be used to show and hide validation messages based upon the state of the control’s NgModel object. Additionally, the delete button uses the NgIf directive to show or hide itself based upon whether the widget has an id. If there is no id, then the widget is being created.

Finally, the WidgetEdit class needs to be configured. In order to do this, first create a new file name widget-edit.component.ts in the wwwrootsrc/js/app/components/widget-edit folder. Next add the code below to the file. Below, there are numerous code comments included to explain each code statement.

import { Component, OnInit } from "@angular/core";
import { Router, ActivatedRoute } from "@angular/router";

import { Widgets } from "../../services/widgets";
import { Widget } from "../../models/widget";

interface SelectOption {
    value: string,
    label: string,
}

@Component({
    selector: "widget-edit",
    template: require("./widget-edit.component.html"),
    styles: [require("./widget-edit.component.scss")]
})
export class WidgetEdit implements OnInit {

    // the widget to modify in the form
    private widget: Widget = { color: "", size: "", quantity: 0, price: 0 } as Widget;

    // options for drop downs
    private colors: SelectOption[] = [
        { value: "red", label: "Red" },
        { value: "blue", label: "Blue" },
        { value: "green", label: "Green" },
        { value: "yellow", label: "Yellow" },
        { value: "orange", label: "Orange" },
        { value: "purple", label: "Purple" },
    ];

    private sizes: SelectOption[] = [
        { value: "tiny", label: "Tiny" },
        { value: "small", label: "Small" },
        { value: "medium", label: "Medium" },
        { value: "large", label: "Large" },
        { value: "huge", label: "Huge" },
    ];

    constructor(
        private widgets: Widgets, // the widgets data service
        private route: ActivatedRoute, // the current route
        private router: Router, // the router to navigate to other routes
    ) { }

    public ngOnInit() {

        // if a widget id param is supplied, load the widget
        this.route.params.subscribe(params => {

            // check to see if a widget id has been specified, if it has,
            // then load the widget
            if (params["widgetId"]) {
                this.widgets.get(Number(params["widgetId"])).subscribe(widget =>
                    this.widget = widget);
            }

        });
    }

    public saveWidget(widget: Widget) {

        // if the widget has an id, then update it; otherwise, insert it.
        // After saving, return to the widget table.
        if (this.widget.id) {
            this.widgets.update(this.widget).subscribe(() =>
                this.router.navigateByUrl("/"));
        } else {
            this.widgets.insert(this.widget).subscribe(() =>
                this.router.navigateByUrl("/"));
        }

    }

    // delete the widget with the specified id, then return
    // to the table of widgets
    public deleteWidget(widgetId: number) {
        this.widgets.delete(widgetId)
            .subscribe(() => this.router.navigateByUrl("/"));
    }

    // return to the table of widgets
    public returnToList() {
        this.router.navigateByUrl("/");
    }
}

The WidgetEdit component is similar to the other components in the application. One additional feature it has is an interface for configuring select options. Instead of hard-coding the drop downs in the template, they are driven by data provided by the component instance. The data on the instance is static but could be provided by a REST service or another data source. To set up the array of options, an interface was created with the two fields each option would need. Introducing strong-typing like this as needed is important to coding TypeScript applications in a way that benefits developers. TypeScript will let the developer determine how much of the application is strongly-typed.

Finally, the WidgetEdit component needs to be registered with the AppModule:

First, open the file named app.module.ts in the wwwrootsrc/js/app folder.

Then, add the following line of code after the import WidgetView statement:

import { WidgetEdit } from "./components/widget-edit/widget-edit.component";

Now add the WidgetEdit class to the NgModule declarations array:

declarations: [ AppComponent, WidgetTable, WidgetView, WidgetEdit, CapitalizePipe ],

This will make the WidgetEdit component available in the module for use in templates and in routing.

The WidgetView and WidgetTable components will be configured to work with WidgetEdit in the next two sections.

Update the Widget View Component

The WidgetView component needs to be wired up to allow the user to click an edit button in order to edit the widget. To begin, open the file named widget-view.component.html in the wwwrootsrc/js/app/components/widget-view folder. Then, add the following button HTML markup to the template before the “Return to List” button:

<button type="button" (click)="editWidget(widget.id)">Edit Widget</button>

Next, the event handler on the component class needs to be added. Open the file named widget-view.component.ts. After the ngOnInit method, add the following code:

// navigate to the widget view component
editWidget(widgetId: number) {
  this.router.navigate(['widget', widgetId, "edit"]);
}

With the new button, the widget can route to edit mode from the WidgetView component.

Update the Widget Table Component

The widget table needs to be updated in order to use some styling, pipes, and to support the new widget edit and widget create buttons.

Open the file named widget-table.component.scss in the wwwrootsrc/js/app/components/widget-table folder. Then add the following SCSS markup:

th { text-align: center; }

.text-center {
    text-align: center;
}

.text-right {
    text-align: right;
}

The SCSS markup will perform some basic styling to properly align the column data.
Open the file named widget-table.component.html. Modify the table element to use the following classes from Bootstrap:

<table class="table table-striped">

Next, replace the table columns with these columns:

<td>{{widget.name}}</td>
<td class="text-center">{{widget.color | capitalize}}</td>
<td class="text-center">{{widget.size | capitalize}}</td>
<td class="text-right">{{widget.quantity}}</td>
<td class="text-right">{{widget.price | currency:'USD':true:'1.2-2'}}</td>
<td class="text-center">
  <button type="button" (click)="viewWidget(widget.id)">View</button>
  <button type="button" (click)="editWidget(widget.id)">Edit</button>
</td>

Finally, add a Create Widget button to the bottom of the template:

<button type="button" (click)="createWidget()">Create Widget</button>

With these template changes complete, the following has been accomplished: styles have been applied, pipes are formatting table data, and the Edit and Create Widget buttons are available.

Open the file named widget-table.component.ts and add the following two methods to component class:

// navigate to the widget edit component in update mode
editWidget(widgetId: number) {
    this.router.navigate(['widget', widgetId, "edit"]);
}

// navigate to the widget edit component in insert mode
createWidget() {
    this.router.navigate(['widget', "create"]);
}

With the routes added, the application is ready to run! You are almost done!

Run the Application

You have done a lot of work to get this far. To run the application:

  1. Open a terminal, and change to the folder.
  2. From the terminal window, run the following command:
    $ npm start

  3. Open a web browser, and navigate to http://localhost:5000.

Next Steps

This concludes this four part tutorial. There are many more things that could be done to extend this application further. Additional styling could be added, the pipe could be moved to a shared module, unit tests and end-to-end tests could be created, etc. Perhaps they will be in a future set of tutorials. We hope you have enjoyed learning more Angular 2 and .NET Core. They are a powerful combination of technologies for building next generation web applications.

Author: Eric Greene, one of Accelebrate’s instructors

 

In-Depth Angular Training

For in-depth Angular training, click here to view all of
Accelebrate's Angular training courses for you and your staff.

 

Contact Us:

Accelebrate’s training classes are available for private groups of 3 or more people at your site or online anywhere worldwide.

Don't settle for a "one size fits all" public class! Have Accelebrate deliver exactly the training you want, privately at your site or online, for less than the cost of a public class.

For pricing and to learn more, please contact us.

Contact Us Train For Us

Toll-free in US/Canada:
877 849 1850
International:
+1 678 648 3113

Toll-free in US/Canada:
866 566 1228
International:
+1 404 420 2491

925B Peachtree Street, NE
PMB 378
Atlanta, GA 30309-3918
USA

Subscribe to our Newsletter:

Never miss the latest news and information from Accelebrate:

Microsoft Gold Partner

Please see our complete list of
Microsoft Official Courses

Recent Training Locations

Alabama

Huntsville

Montgomery

Birmingham

Alaska

Anchorage

Arizona

Phoenix

Tucson

Arkansas

Fayetteville

Little Rock

California

San Francisco

Oakland

San Jose

Orange County

Los Angeles

Sacramento

San Diego

Colorado

Denver

Boulder

Colorado Springs

Connecticut

Hartford

DC

Washington

Florida

Fort Lauderdale

Miami

Jacksonville

Orlando

Saint Petersburg

Tampa

Georgia

Atlanta

Augusta

Savannah

Idaho

Boise

Illinois

Chicago

Indiana

Indianapolis

Iowa

Ceder Rapids

Des Moines

Kansas

Wichita

Kentucky

Lexington

Louisville

Louisiana

Banton Rouge

New Orleans

Maine

Portland

Maryland

Annapolis

Baltimore

Hagerstown

Frederick

Massachusetts

Springfield

Boston

Cambridge

Michigan

Ann Arbor

Detroit

Grand Rapids

Minnesota

Saint Paul

Minneapolis

Mississippi

Jackson

Missouri

Kansas City

St. Louis

Nebraska

Lincoln

Omaha

Nevada

Reno

Las Vegas

New Jersey

Princeton

New Mexico

Albuquerque

New York

Buffalo

Albany

White Plains

New York City

North Carolina

Charlotte

Durham

Raleigh

Ohio

Canton

Akron

Cincinnati

Cleveland

Columbus

Dayton

Oklahoma

Tulsa

Oklahoma City

Oregon

Portland

Pennsylvania

Pittsburgh

Philadelphia

Rhode Island

Providence

South Carolina

Columbia

Charleston

Spartanburg

Greenville

Tennessee

Memphis

Nashville

Knoxville

Texas

Dallas

El Paso

Houston

San Antonio

Austin

Utah

Salt Lake City

Virginia

Richmond

Alexandria

Arlington

Washington

Tacoma

Seattle

West Virginia

Charleston

Wisconsin

Madison

Milwaukee

Alberta

Edmonton

Calgary

British Columbia

Vancouver

Nova Scotia

Halifax

Ontario

Ottawa

Toronto

Quebec

Montreal

Puerto Rico

San Juan

© 2013-2019 Accelebrate, Inc. All Rights Reserved. All trademarks are owned by their respective owners.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.