Tutorial: How to Build a Web Application using ASP.NET Core and Angular

0

Modern distributed systems on the Web today consist of many different parts, systems and technologies. Frontend and backend are two very important elements of a current web application. For maximum flexibility, you can completely separate these parts and let a running in the browser, your own application as a front end with a REST service in the backend communicate.

Choosing a front-end framework is not easy, but in recent times Angular has not only highlighted a good “separation of concerns” concept and good synchronization mechanisms of model, view, architecture and high performance. Also, the regularity with which new versions appear, the Angular CLI and not least the Internet giant Google with Long Term Support are helping to put more and more business applications on the Web on the Angular platform.

In the backend, Microsoft has at least since version 2.x thrown off the old coat with ASP.NET Core and comes new, lean and, above all, fast. Live reload, middleware, speed, and cross-platform capability are just some of the reasons ASP.NET Core should be more than just a look.

In this article I want to explain the components and advantages of a front end with Angular and a REST backend with ASP.NET Core and how to program these parts of a web application. As an example, we program a book tracker that contains a bookmark for books that you can mark as read. You can also add new books and edit existing ones. The complete source code is of course available on GitHub .

The ASP.NET Core Backend

ASP.NET Core offers a Command Line Interface (CLI), which allows us to create templates for the framework of our Web API. A command line in the desired folder and the command dotnet new webapi in the console create a new web API for us, which we can run with dotnet watch run . When we created the Web API, we immediately ran the dotnet restore command, which downloaded all of our NuGet packages and deployed them to our application. Here, a new webserver “Kestrel” is hosted in a console that starts up our application and makes it available for requests. In the latest version we have an HTTP and an HTTPS endpoint available. dotnet watch run also provides a live reload server, so every time we change a file in our project, the backend restarts and we do not have to manually pause and reboot it.

Configure and start the Web API

ASP.NET core applications are basically console programs that we can start, for example, with the command dotnet run from the command line. Thus, the starting point of our Web API is a simple console application that provides us with a web server instead of, for example, issuing a “Hello World” on the console (Listing 1).

public class Program
{
  public static void Main(string[] args)
  {
    CreateWebHostBuilder(args).Build().Run();
  }
 
  public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>();
}

If any configurations are to be made for our application, these configuration files can be read in here. By default, the mitgenerated appsettings.json and appsettings. *. Json are used to configure and apply. But other configuration formats like * .xml or even * .ini are also supported. In addition to a web server, ASP.NET Core also creates a configuration object that we can provide and use in the Startup.cs.

Start-up and Dependency Injection

Based on the ready configuration we’re given, we can now configure our web api. ASP.NET Core makes use of the internal dependency injection system. In the file Startup.cs we get the configuration in the constructor of the class (Listing 2). ASP.NET Core comes with its own Dependency Injection system, which we will discuss later in this article.

Listing 2

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }
 
  public IConfiguration Configuration { get; }
   // ...
}

The Startup.cs file provides two additional methods: ConfigureServices and Configure . The former fills the dependency injection container that we get passed from ASP.NET Core:

public void ConfigureServices(IServiceCollection services) { /*...*/ }

On this container, we can enter our dependent services and later get injected into our classes via Dependency Injection and thus use. Also MVC itself is placed here together with its services in the container.

The Configure method creates a pipeline for all our requests before they are processed by our controllers. The order of added middleware is important here. That is, every incoming request goes through the middleware that we can specify in this method. Likewise the outgoing response, but this time the middleware is processed in reverse order. So features like Authentication etc. can be hooked into the pipeline for our requests. Also MVC itself is added as middleware. We have already specified the services in the ConfigureServices method; we use the associated middleware with app.UseMvc () . Thus, our web API requests can be received in controllers, use routing, etc.

Use the Dependency Injection container

In our sample application, we need a repository to store our entities in the database:

public interface IBookRepository { ... }
public class BookRepository : IBookRepository

We can now register this repository in our Dependency Injection container in front of the interface:

services.AddScoped<IBookRepository, BookRepository>();

AddScoped ensures that the instance is kept as long as the request is processed, and that a new instance is created with each request. The additional method AddSingleton () creates an instance of the service when the application starts , AddTransient () would create one instance per constructor injection .

Define a REST endpoint with controllers

The actual REST endpoint is mapped into ASP.NET Core controllers. The HTTP verbs such as GET, POST, PUT, PATCH, DELETE, etc. can be implemented in controllers (Listing 3).

Listing 3

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
  //
}

The route attribute above the controller class specifies the address of the endpoint. Here, the prefix is ​​conventionally api / , followed by a generic [controller] . This is replaced by the name of the class without the suffix “Controller”. In this case, api / values .

With the ApiController attribute, we determine that it is an HTTP endpoint that sends HTTP responses. By deriving ControllerBase , we dispense with all View functionalities that we would need in the case of a complete MVC application, because we do not want to send completely rendered pages, but JSON as an answer.

Within this controller, we can now define the REST endpoint that allows our books to be saved and updated.

In our example, we create a BooksController for which we define the endpoint api / books . Since the repository was already registered in the container, we can now simply inject it in the controller (Listing 4).

Listing 4

[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
  private readonly IBookRepository _bookRepository;
 
  public BooksController(IBookRepository repository)
  {
    _bookRepository = repository;
  }
}
method link
GET api / books /
GET api / books / {id}
POST OFFICE api / books /
PUT api / books / {id}
DELETE api / books / {id}

Table 1: REST endpoint

To be able to retrieve all books, we have to respond to the HTTP GET verb (Table 1) and, in response, return all books serialized as JSON (see box “AddMvc ()”).

“AddMvc ()”

With the call to AddMvc () in the Startup class, we’ve already added a JSON serializer that automatically parses the data from objects to JSON and from JSON to objects. ASP.NET Core delivers this functionality out of the box.

Among other things, ASP.NET Core provides the return type of a method with the IActionResult interface or an even generic class ActionResult <T> to define HTTP responses. At the same time, deriving the controller class from ControllerBase offers another advantage: You can use small helper methods such as Ok (…) or BadRequest (…) that help the developer get the correct HTTP response with the correct HTTP status code back to the client Send. This is of crucial importance for decoupled systems and such a service architecture.

A method that should respond to an HTTP verb can be marked as an attribute using the respective HTTP verb (Listing 5).

Listing 5

[HttpGet]
public IActionResult GetAll()
{
  List&lt;Book> items = _bookRepository.GetAll().ToList();
  IEnumerable&lt;BookDto> toReturn = items.Select(x => Mapper.Map&lt;BookDto>(x));
  return Ok(toReturn);
}

This method responds to an HTTP GET call and returns a 200 status code that represents Ok . The helper method Ok () with the data to be sent as body with the response makes this HTTP convention very easy.

If we want to query a single book, we can also specify and read parameters in the routing. For this, we also specify the routing attribute on the method and pass it as a parameter into the function itself (Listing 6).

Listing 6

[HttpGet]
[Route("{id:int}")]
public IActionResult GetSingle(int id)
{
  Book item = _bookRepository.GetSingle(id);
 
  if (item == null)
  {
    return NotFound();
  }
 
  return Ok(Mapper.Map&lt;BookDto>(item));
}

If the book is not found, we will return a 404 status code, otherwise our known OK status code 200. We implement all other endpoints accordingly. An example of a complete endpoint can be found in Listing 7.

Listing 7

[Route("api/[controller]")]
  [ApiController]
  public class BooksController : ControllerBase
  {
    private readonly IBookRepository _bookRepository;
 
    public BooksController(IBookRepository repository)
    {
      _bookRepository = repository;
    }
 
    [HttpGet(Name = nameof(GetAll))]
    public IActionResult GetAll()
    {
      List&lt;Book> items = _bookRepository.GetAll().ToList();
      IEnumerable&lt;BookDto> toReturn = items.Select(x => Mapper.Map&lt;BookDto>(x));
      return Ok(toReturn);
    }
 
    [HttpGet]
    [Route("{id:int}", Name = nameof(GetSingle))]
    public IActionResult GetSingle(int id)
    {
      Book item = _bookRepository.GetSingle(id);
 
      if (item == null)
      {
        return NotFound();
      }
 
      return Ok(Mapper.Map&lt;BookDto>(item));
    }
 
    [HttpPost(Name = nameof(Add))]
    public ActionResult&lt;BookDto> Add([FromBody] BookCreateDto bookCreateDto)
    {
      if (bookCreateDto == null)
      {
        return BadRequest();
      }
 
      Book toAdd = Mapper.Map&lt;Book>(bookCreateDto);
 
      _bookRepository.Add(toAdd);
 
      if (!_bookRepository.Save())
      {
        throw new Exception("Creating an item failed on save.");
      }
 
      Book newItem = _bookRepository.GetSingle(toAdd.Id);
 
      return CreatedAtRoute(nameof(GetSingle), new { id = newItem.Id },
        Mapper.Map&lt;BookDto>(newItem));
    }
 
    [HttpPatch("{id:int}", Name = nameof(PartiallyUpdate))]
    public ActionResult&lt;BookDto> PartiallyUpdate(int id, [FromBody] JsonPatchDocument&lt;BookUpdateDto> patchDoc)
    {
      if (patchDoc == null)
      {
        return BadRequest();
      }
 
      Book existingEntity = _bookRepository.GetSingle(id);
 
      if (existingEntity == null)
      {
        return NotFound();
      }
 
      BookUpdateDto bookUpdateDto = Mapper.Map&lt;BookUpdateDto>(existingEntity);
      patchDoc.ApplyTo(bookUpdateDto, ModelState);
 
      TryValidateModel(bookUpdateDto);
 
      Mapper.Map(bookUpdateDto, existingEntity);
      Book updated = _bookRepository.Update(id, existingEntity);
 
      if (!_bookRepository.Save())
      {
        throw new Exception("Updating an item failed on save.");
      }
 
      return Ok(Mapper.Map&lt;BookDto>(updated));
    }
 
    [HttpDelete]
    [Route("{id:int}", Name = nameof(Remove))]
    public IActionResult Remove(int id)
    {
      Book item = _bookRepository.GetSingle(id);
 
      if (item == null)
      {
        return NotFound();
      }
 
      _bookRepository.Delete(id);
 
      if (!_bookRepository.Save())
      {
        throw new Exception("Deleting an item failed on save.");
      }
 
      return NoContent();
    }
 
    [HttpPut]
    [Route("{id:int}", Name = nameof(Update))]
    public ActionResult&lt;BookDto> Update(int id, [FromBody] BookUpdateDto updateDto)
    {
      if (updateDto == null)
      {
        return BadRequest();
      }
 
      var item = _bookRepository.GetSingle(id);
 
      if (item == null)
      {
        return NotFound();
      }
 
      Mapper.Map(updateDto, item);
 
      _bookRepository.Update(id, item);
 
      if (!_bookRepository.Save())
      {
        throw new Exception("Updating an item failed on save.");
      }
 
      return Ok(Mapper.Map&lt;BookDto>(item));
    }
  }

Adding Cross-Origin Resource Sharing (CORS)

In order for our API to be able to receive calls from other domains, rather than just ours, we can configure CORS. Since the configuration of the API passes through the Startup.cs file, we can also enter the CORS policy that suits us.

First, we need to add CORS as a service and configure it at the same time (Listing 8).

Listing 8

services.AddCors(options =>
{
  options.AddPolicy("AllowAngularDevClient",
    builder =>
    {
      builder
        .WithOrigins("http://localhost:4200")
        .AllowAnyHeader()
        .AllowAnyMethod();
    });
});

Here we create a rule where we only allow requests from our development client of the Angular Application. We call this rule AllowAngularDevClient and need to use it in our pipeline – the Configure method (Listing 9).

Listing 9

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory,
IHostingEnvironment env)
{
  // ...
  app.UseCors("AllowAngularDevClient");
  // ...
}

Documentation with Swagger

Not only if an API is to be used by other, possibly remote developers or teams, a documentation of the API is useful. Also for the better own overview a good documentation is very helpful. So that we do not have to fill out, maintain and hand over endless Word documents, there is the solution to have our API documented electronically and to make the documentation available via endpoint. Swagger offers such a solution, which we can use in a few simple steps in our Web API.

With dotnet add Backend.csproj package Swashbuckle.AspNetCore we can add the NuGet package. After being inserted into the * .csproj , we can use it in the Startup.cs file (Listing 10).

Listing 10

public void ConfigureServices(IServiceCollection services)
{
  // ...
  services.AddSwaggerGen(c =>
  {
    c.SwaggerDoc("v1", new Info { Title = "My first ASP.NET Core WebAPI", Version = "v1" });
  });
  // ...
}
 
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IHostingEnvironment env)
{
  // ...
  app.UseSwagger();
  app.UseSwaggerUI(c =>
  {
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Version 1");
  });
  // ...
}

Swagger generates a JSON file as a description of our API, which we can use to pour into a more legible user interface using the SwaggerUi. After the API has started, we can view the UI via https: // localhost: 5001 / swagger ( Figure 1 ).

asp.net and Angular

Fig. 1: UI generated by Swagger

To save the suffix swagger after the address of the server, we can also set the RoutePrefix to an empty string:

app.UseSwaggerUI(c =>
{
  c.SwaggerEndpoint("/swagger/v1/swagger.json", "Version 1");
  c.RoutePrefix = string.Empty;
});

Now the Swagger documentation page is also visible under the server address https: // localhost: 5001 /, which makes the display even easier.

Of course, ASP.NET Core offers many more possibilities: WebSockets with SignalR, exception handling, configuration, query parameters and working with different environments are just some of the possibilities that would go beyond the scope of this article.

Now let’s turn to the client side with Angular. In the next section we want to use the created ASP.NET Core Web API, enter books, mark as read and also delete.

Create an Angular application

To create an Angular application, we use the Angular CLI, Angular’s Command Line Interface . With ng new angular-booktracker we can have the application created for the first time.
After the application has been created with your files, we can start it with npm start. A web server for development is available after a few seconds at http: // localhost: 4200 . Angular is based on the principle of components. These give us the opportunity to subdivide our application into many small parts and to implement them individually.

Distribution of the application in modules

In order to create an architecture on the client, Angular offers a further abstraction level in addition to the web components themselves: splitting the application into different Angular modules. A module separates our application into logical areas and individual features. They serve as containers for Angular Components, Pipes, Directives, etc., which are required by the module. By dividing into modules, the application becomes more maintainable, clearer and easier to test ( Fig. 2 ).

For our Booktracker we have three modules: a BooksModule , a CoreModule and a SharedModule . The AppModule , which is also used to start our application, is of course also included.

BooksModule is responsible for the book feature. The CoreModule provides a container for all our services, the SharedModule is used in our example for abstracting Model classes and the Angular-Material modules, and we need the AppModule to launch our application; It also serves as an entry-level module.

Modules also offer the possibility of lazy loading. So we can load the required feature module only when the user explicitly selects it, or we load it automatically after all modules have already been loaded without lazy loading.

Fig. 2: Division into modules

The routes allow us to set the lazy loading in the app.routing.ts (Listing 11).

Listing 11

export const AppRoutes: Routes = [
  { path: '', redirectTo: 'books', pathMatch: 'full' },
  {
    // Falls dieser Pfad angewählt wird ...
    path: 'books',
    // ... lade dieses Modul nach
    loadChildren: './books/books.module#BooksModule'
  },
  {
    path: '**',
    redirectTo: 'books'
  }
];

Using Angular Material

In order to be able to use ready-made UI controls such as a list or a menu, we use Angular Material [5]. All required elements are exported in Angular Material in individual modules, which we summarize in a material.module.ts and make our application available with the shared.module (Listing 12).

Listing 12

import { NgModule } from '@angular/core';
import { ... } from '@angular/material';
 
const materialModules: any[] = [
  // ... alle benötigten Material Modules
];
 
@NgModule({
  imports: materialModules,
  exports: materialModules
})
export class MaterialModule {}

The SharedModule now re-exports the MaterialModule to provide other modules that import the SharedModule with access to the MaterialModule’s exports (Listing 13).

Listing 13

import { NgModule } from '@angular/core';
import { MaterialModule } from './material.module';
 
@NgModule({
  imports: [MaterialModule],
  exports: [MaterialModule]
})
export class SharedModule {}

The communication with the API

Before we can display or display data, we need to retrieve it from the API we just created. For this we create a service that abstracts the communication with the API and makes it available by means of methods. The HttpClient from the @angular / common / http module, as well as the import of the HttpClientModule from Angular gives us exactly the HTTP verbs we need (Listing 14).

Listing 14

@Injectable({ providedIn: 'root' })
export class BookService {
  private url = `https://localhost:5001/api/books`;
  constructor(private readonly http: HttpClient) {}
  // alle Methoden
}

The @Injectable ({providedIn:, root ‘}) syntax inserts our service into the root injector of Angular. Thus, the service is available in our application. Using the constructor (private readonly httpBase: HttpClient) {} syntax, we use Angular Dependency Injection on one side; on the other hand, we register a private property in the BookService class called ” http “, which we can access from all methods.

For example, to query all books, we send a GET request to the URL https: // localhost: 5001 / api / books :

getAllBooks() {
    return this.http.get&lt;Book[]>(this.url);
}

We call the GET method, set the generic return type to Book [] , and use as destination the URL that is permanently stored in the service (see box “Configuring the URL”). The return type of the method is the observable, which is the stream of data we expect from the REST call. From outside one can register for the answer and, depending on the answer, react.

The Book class is a pure DTO class that we can set according to the expected JSON response. Specifying the type automatically serializes the JSON response into this class (Listing 15).

Listing 15

export class Book {
  id: number;
  read: boolean;
  title: sting;
  author: string;
  description: string;
  genre: string;
}

Configuration of the URL

The URL can be made configurable by various means: environments, config-url-requests etc.

The other methods on the service are exactly what we have implemented at the API and also need in our application (Listing 16).

Listing 16

getAllBooks() {
  return this.http.get&lt;Book[]>(this.url);
}
 
getSingle(bookId: number) {
  return this.http.get&lt;Book>(`${this.url}/${bookId}`);
}
 
update(updated: Book) {
  return this.http.put&lt;Book>(`${this.url}/${updated.id}`, updated);
}
 
add(book: Book) {
  return this.http.post&lt;Book>(this.url, book);
}
 
delete(bookId: number) {
  return this.http.delete(`${this.url}/${bookId}`);
}

Displaying the data

To display the data, we create components that have an HTML template. We enter the newly created service via Dependency Injection into the components and call the corresponding methods.

You can divide components in two different ways: Presentational Components and Container Components [6]. The former focus on how data is presented, but do not care about how data is loaded or where it comes from. Container Components have a dependency on a repository or data service and receive events from Presentational Components. Presentational Components are given the data and take care of the presentation, while Container Components worry about where the data comes from and can communicate via services with, for example, a REST interface. This makes Presentational Components extremely reusable and provides a clearer view of the places in the code that manipulate the status of an application.

This way we can also split the components in our project ( Fig. 3 ).

Fig. 3: Component distribution in the project

list component

The books-list.component.ts receives a collection of books as input and takes care of the visual presentation of this listing with a list of Angular material. It tells the component using it by means of an event if someone has marked a book as read.

Angular offers us the @Input () and @Output () decorators, which allow us to mark incoming data and outbound events on properties of the Component class (Listing 17).

Listing 17

@Component({
  /*...*/
})
export class BookListComponent implements OnInit {
  @Input()
  books: Book[] = [];
 
  @Output()
  bookReadChanged = new EventEmitter();
  // ...
}

In order to fill these properties with data or to register for events of the component, we link the component in the HTML with the parent component: It uses the child component via its selector in the HTML, binds data to the properties and registers itself to events ( Listing 18).

Listing 18

&lt;mat-tab-group dynamicHeight>
  &lt;mat-tab *ngIf="unreadBooks$ | async as unreadBooks">
    &lt;app-book-list [books]="unreadBooks" (bookReadChanged)="toggleBookRead($event)">&lt;/app-book-list>
  &lt;/mat-tab>
  &lt;mat-tab *ngIf="readBooks$ | async as readBooks">
    &lt;app-book-list [books]="readBooks" (bookReadChanged)="toggleBookRead($event)">&lt;/app-book-list>
  &lt;/mat-tab>
&lt;/mat-tab-group>

So we bind two properties of the parent component readBooks and unreadBooks to the input property of the child component app-book-list , which we can even reuse here.

Survey Component

In the Component class of the parent component, we fill in these two lists (Listing 19).

Listing 19

export class BooksOverviewComponent implements OnInit {
  unreadBooks$: Observable&lt;Book[]>;
  readBooks$: Observable&lt;Book[]>;
 
  constructor(private readonly bookService: BookService) {}
 
  ngOnInit() {
    this.getAllBooks();
  }
 
  private getAllBooks() {
    const allBooks$ = this.bookService.getAllBooks().pipe(
      publishReplay(1),
      refCount()
    );
 
    this.unreadBooks$ = allBooks$.pipe(
      map(books => books.filter(book => !book.read))
    );
 
    this.readBooks$ = allBooks$.pipe(
      map(books => books.filter(book => book.read))
    );
  }
}

Via Dependency Injection we get the BookService passed into the constructor and store it in an internal variables bookService . Then we store the observable for all books, regardless of the read status, in an observable and create two more observables: unreadBooks $ and readBooks $ (see box “$ suffix”).

“$” – suffix

The $ sign at the end of a variable (also called Finnish notation) makes it easier for developers to see which variable has an already resolved value and which variable represents a stream whose value will be resolved sometime in the future.

Async pipe

So that we do not have to manually attach subscribe (…) to the resolution of the observable, we can bind it to the template and resolve the data with the Async pipe. This has the advantage of making our code more legible in the component, and we do not have to worry about unsubscribe if the component is destroyed by Angular again. The Async Pipe does it automatically for us.

In order to bind the data of the observable in the template to a variable, Angular offers us a * ngIf-as syntax, which we see here in action:

&lt;mat-tab *ngIf="unreadBooks$ | async as unreadBooks" label="to buy ({{unreadBooks.length}})">
  &lt;!-- child component -->
&lt;/mat-tab>

* ngIf = “unreadBooks $ | async as unreadBooks “ fills the contents of the observable with a variable unreadBooks , which we use in the scope of the HTML element.

Each use of an Async pipe in the template is like using a subscribe (…) in the TypeScript code. By using two async pipes, we fire the observable twice so two requests should actually be sent to our server.

So we do not unnecessarily burden the server and our API, we can build a “cache” that RxJs offers us:

const allBooks$ = this.bookService.getAllBooks().pipe(
  publishReplay(1),
  refCount()
);

publishReplay (1), refCount () saves us the last call on the observable, without starting a new call to the API. The alternative would be a manual subscribe (…) , filtering the result with the filter (…) function and assigning it to two arrays on the component. This extra effort would make the Async Pipe obsolete.

routing

To switch between different templates of components, we work with routing on the client side. To enable routing, we import the RoutingModule into the AppModule and configure it with the forRoot (…) method (Listing 20).

Listing 20

@NgModule({
  declarations: [/*...*/],
  imports: [
    // ...
    RouterModule.forRoot([
      { path: '', redirectTo: 'books', pathMatch: 'full' },
      {
        path: 'books',
        loadChildren: './books/books.module#BooksModule',
      },
       {
      path: '**',
      redirectTo: 'books',
    },
    ], { useHash: true }),
  ],
  providers: [],
  bootstrap: [...],
})
export class AppModule {}

Thus, we can determine at which term (path) to which component should be jumped. With the loadChildren property and the syntax already shown, we can enable the lazy loading of this module and use the child module’s routes. We can configure these with the method forChild (…) , it should only be used once on the AppModule to configure it in the application (Listing 21).

Listing 21

@NgModule({
  imports: [
    RouterModule.forChild([
      { path: '', redirectTo: 'overview', pathMatch: 'full' },
      { path: 'overview', component: BooksOverviewComponent },
      { path: 'create', component: BookFormComponent },
      { path: 'edit/:id', component: BookFormComponent },
      { path: 'details/:id', component: BookDetailsComponent }
    ])
  ]
})
export class BooksModule {}

Thus, the BooksModule defines its own routes and is loaded completely – ie with its routes. The reference in the App Module is the entry point to the module. Routes of the root module and the child module are simply concatenated.

In order to be able to display the templates of the components, we need a replaceable part, which Angular replaces with the templates of the components.

The router module exports the router-outlet , which provides us with exactly this functionality. In the AppComponent template , we can put this in the place that needs to be dynamically replaced:

&lt;mat-sidenav-container class="app-root" fullscreen>
  &lt;!-- ... -->
  &lt;router-outlet>&lt;/router-outlet>
  &lt;!-- ... -->
&lt;/mat-sidenav-container>

Based on the route, the respective component is now displayed in the place of router-outlet.

Forms

Of course, you can not only view information with Angular data, but also send it to the server. We have already implemented the corresponding POST method on the server and in our service.

To give the user the ability to create or edit new books, we can use Reactive Forms. First of all, we import the ReactiveFormsModule and everything that this module exports into our module (Listing 22).

Listing 22

import { ReactiveFormsModule } from '@angular/forms';
 
@NgModule({
  imports: [
    // ...
    ReactiveFormsModule
  ]
})
export class BooksModule {}

With Reactive Forms, we create the shape in the component’s code, bind the created FormGroup to the template, and display the FormControls (Listing 23).

Listing 23

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
 
@Component({
  /*...*/
})
export class BookFormComponent implements OnInit {
  form: FormGroup;
 
  ngOnInit() {
    this.form = new FormGroup({
      id: new FormControl(''),
      title: new FormControl('', Validators.required),
      author: new FormControl('', Validators.required),
      description: new FormControl('', Validators.required),
      genre: new FormControl('')
    });
  }
}

We can assign a new FormGroup to the property form , which gets an object with properties that represent the individual FormControls id: new FormControl (“) and so on. As a parameter, the FormControl object receives the default value here. You can also add validators or options. In our case, we provide the Required Validator, which states that the form needs this value to be valid.

Once this object is filled, we can use it in the template and build our shape (Listing 24).

Listing 24

&lt;form [formGroup]="form">
  &lt;div class="form-container">
    &lt;input formControlName="title">
    &lt;input formControlName="author">
    &lt;textarea formControlName="description">&lt;/textarea>
   &lt;!-- more controls -->
  &lt;/div>
&lt;/form>

We create a form using the HTML <form> tag and bind it to the formGroup directive , which is exported by the ReactiveFormsModule . Within this HTML form tag are then the controls of the form available, which we can bind to the appropriate HTML controls.

In order to submit the form, Angular offers us another ngSubmit directive , which we can use as follows:

&lt;form (ngSubmit)="addBook()" [formGroup]="form">

The addBook () method is called using event binding when the form is submitted . The next step is a button with which we can send the form:

&lt;form (ngSubmit)="addBook()" [formGroup]="form">
  &lt;!-- more controls -->
  &lt;button [disabled]="form.invalid || form.pristine">Add Book&lt;/button>
&lt;/form>

Angular offers us the ability to bind HTML Properties, as in the code sample the disabled property of the button. We can set the button inactive if the form itself is invalid , ie at least one of its controls is not valid ( form.invalid ) or the form has not been changed yet ( form.pristine ).

The addGroup () method on the component uses the provided BooksService again to submit the book. The property value on the form provides us with all form controls that we can send, receive and enter at the backend (Listing 25).

Listing 25

export class BookFormComponent {
  constructor(
    private readonly bookService: BookService,
    private readonly notificationService: NotificationService
  ) {}
 
  addBook() {
    this.bookService
      .add(this.form.value)
      .subscribe(() => this.notificationService.show('Book added'));
  }
}

In order for the user to know that the addition was successful, we can still display a message or otherwise respond accordingly.

Summary

With server-side technology such as ASP.NET Core, we can build modern and flexible backend applications that we can not only consume from Angular. Tools such as the dotnet CLI , dependency injection, automatic restart of the server, the speed and the fact that you can use not only Visual Studio, but any editor for developing, make ASP.NET Core extremely flexible and the creation of web applications beyond the Web APIs very easy. The separation of front and backend relies on a very high flexibility and decoupling.

Angular as a modern web platform – with tooling such as the Angular CLI and features such as Dependency Injection, dividing into modules, lazy loading, etc. – makes it possible to implement even large applications on the Web with structure and architecture in TypeScript. Who would have thought a few years ago that such architectures could be developed on the client and ultimately with JavaScript?

Source: entwickler.de

Share This:

Leave A Reply

Powered by FrontNet