This document has multiple versions. Select the options best fit for you.

UI

ASP.NET Core Angular Tutorial - Part 2

About this tutorial

This is the second part of the ASP.NET Core Angular tutorial series. All parts:

You can also watch this video course prepared by an ABP community member, based on this tutorial.

Creating a new book

In this section, you will learn how to create a new modal dialog form to create a new book.

State definitions

Open book.action.ts in app\book\state folder and replace the content as below:

import { CreateUpdateBookDto } from '../models'; //<== added this line ==>

export class GetBooks {
  static readonly type = '[Book] Get';
}

// added CreateUpdateBook class
export class CreateUpdateBook {
  static readonly type = '[Book] Create Update Book';
  constructor(public payload: CreateUpdateBookDto) { }
}
  • We imported the CreateUpdateBookDto model and created the CreateUpdateBook action.

Open book.state.ts file in app\book\state folder and replace the content as below:

import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';

export class BookStateModel {
  public book: PagedResultDto<BookDto>;
}

@State<BookStateModel>({
  name: 'BookState',
  defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
  @Selector()
  static getBooks(state: BookStateModel) {
    return state.book.items || [];
  }

  constructor(private bookService: BookService) {}

  @Action(GetBooks)
  get(ctx: StateContext<BookStateModel>) {
    return this.bookService.getListByInput().pipe(
      tap((bookResponse) => {
        ctx.patchState({
          book: bookResponse,
        });
      })
    );
  }

  // added CreateUpdateBook action listener
  @Action(CreateUpdateBook)
  save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
    return this.bookService.createByInput(action.payload);
  }
}
  • We imported CreateUpdateBook action and defined the save method that will listen to a CreateUpdateBook action to create a book.

When the SaveBook action dispatched, the save method is being executed. It calls createByInput method of the BookService.

Add a modal to BookListComponent

Open book-list.component.html file in books\book-list folder and replace the content as below:

<div class="card">
  <div class="card-header">
    <div class="row">
      <div class="col col-md-6">
        <h5 class="card-title">
          {{ '::Menu:Books' | abpLocalization }}
        </h5>
      </div>
       <!--Added new book button -->
      <div class="text-right col col-md-6">
        <div class="text-lg-right pt-2">
          <button
            id="create"
            class="btn btn-primary"
            type="button"
            (click)="createBook()"
          >
            <i class="fa fa-plus mr-1"></i>
            <span>{{ "::NewBook" | abpLocalization }}</span>
          </button>
        </div>
      </div>
    </div>
  </div>
  <div class="card-body">
    <abp-table
      [value]="books$ | async"
      [abpLoading]="loading"
      [headerTemplate]="tableHeader"
      [bodyTemplate]="tableBody"
      [rows]="10"
      [scrollable]="true"
    >
    </abp-table>
    <ng-template #tableHeader>
      <tr>
        <th>{{ "::Name" | abpLocalization }}</th>
        <th>{{ "::Type" | abpLocalization }}</th>
        <th>{{ "::PublishDate" | abpLocalization }}</th>
        <th>{{ "::Price" | abpLocalization }}</th>
      </tr>
    </ng-template>
    <ng-template #tableBody let-data>
      <tr>
        <td>{{ data.name }}</td>
        <td>{{ booksType[data.type] }}</td>
        <td>{{ data.publishDate | date }}</td>
        <td>{{ data.price }}</td>
      </tr>
    </ng-template>
  </div>
</div>

<!--added modal-->
<abp-modal [(visible)]="isModalOpen">
    <ng-template #abpHeader>
        <h3>{{ '::NewBook' | abpLocalization }}</h3>
    </ng-template>

    <ng-template #abpBody> </ng-template>

    <ng-template #abpFooter>
        <button type="button" class="btn btn-secondary" #abpClose>
             {{ 'AbpAccount::Close' | abpLocalization }}
        </button>
    </ng-template>
</abp-modal>
  • We added the abp-modal which renders a modal to allow user to create a new book.
  • abp-modal is a pre-built component to show modals. While you could use another approach to show a modal, abp-modal provides additional benefits.
  • We added New book button to the AbpContentToolbar.

Open book-list.component.ts file in app\book\book-list folder and replace the content as below:

import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
  @Select(BookState.getBooks)
  books$: Observable<BookDto[]>;

  booksType = BookType;

  loading = false;

  isModalOpen = false; // <== added this line ==>

  constructor(private store: Store) {}

  ngOnInit() {
    this.get();
  }

  get() {
    this.loading = true;
    this.store
      .dispatch(new GetBooks())
      .pipe(finalize(() => (this.loading = false)))
      .subscribe(() => {});
  }

  // added createBook method
  createBook() {
    this.isModalOpen = true;
  }
}
  • We added isModalOpen = false and createBook method.

You can open your browser and click New book button to see the new modal.

Empty modal for new book

Create a reactive form

Reactive forms provide a model-driven approach to handling form inputs whose values change over time.

Open book-list.component.ts file in app\book\book-list folder and replace the content as below:

import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
  @Select(BookState.getBooks)
  books$: Observable<BookDto[]>;

  booksType = BookType;

  loading = false;

  isModalOpen = false;

  form: FormGroup; // <== added this line ==>

  constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==>

  ngOnInit() {
    this.get();
  }

  get() {
    this.loading = true;
    this.store
      .dispatch(new GetBooks())
      .pipe(finalize(() => (this.loading = false)))
      .subscribe(() => {});
  }

  createBook() {
    this.buildForm(); //<== added this line ==>
    this.isModalOpen = true;
  }

  // added buildForm method
  buildForm() {
    this.form = this.fb.group({
      name: ['', Validators.required],
      type: [null, Validators.required],
      publishDate: [null, Validators.required],
      price: [null, Validators.required],
    });
  }
}
  • We imported FormGroup, FormBuilder and Validators.
  • We added form: FormGroup variable.
  • We injected fb: FormBuilder service to the constructor. The FormBuilder service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms.
  • We added buildForm method to the end of the file and executed buildForm() in the createBook method. This method creates a reactive form to be able to create a new book.
    • The group method of FormBuilder, fb creates a FormGroup.
    • Added Validators.required static method which validates the relevant form element.

Create the DOM elements of the form

Open book-list.component.html in app\books\book-list folder and replace <ng-template #abpBody> </ng-template> with the following code part:

<ng-template #abpBody>
  <form [formGroup]="form">
    <div class="form-group">
      <label for="book-name">Name</label><span> * </span>
      <input type="text" id="book-name" class="form-control" formControlName="name" autofocus />
    </div>

    <div class="form-group">
      <label for="book-price">Price</label><span> * </span>
      <input type="number" id="book-price" class="form-control" formControlName="price" />
    </div>

    <div class="form-group">
      <label for="book-type">Type</label><span> * </span>
      <select class="form-control" id="book-type" formControlName="type">
        <option [ngValue]="null">Select a book type</option>
        <option [ngValue]="booksType[type]" *ngFor="let type of bookTypeArr"> {{ type }}</option>
      </select>
    </div>

    <div class="form-group">
      <label>Publish date</label><span> * </span>
      <input
        #datepicker="ngbDatepicker"
        class="form-control"
        name="datepicker"
        formControlName="publishDate"
        ngbDatepicker
        (click)="datepicker.toggle()"
      />
    </div>
  </form>
</ng-template>
  • This template creates a form with Name, Price, Type and Publish date fields.
  • We've used NgBootstrap datepicker in this component.

Datepicker requirements

Open book.module.ts file in app\book folder and replace the content as below:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookRoutingModule } from './book-routing.module';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>

@NgModule({
  declarations: [BookListComponent],
  imports: [
    CommonModule,
    BookRoutingModule,
    SharedModule,
    NgbDatepickerModule, //<== added this line ==>
  ],
})
export class BookModule {}
  • We imported NgbDatepickerModule to be able to use the date picker.

Open book-list.component.ts file in app\book\book-list folder and replace the content as below:

import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
  providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
})
export class BookListComponent implements OnInit {
  @Select(BookState.getBooks)
  books$: Observable<BookDto[]>;

  booksType = BookType;

  //added bookTypeArr array
  bookTypeArr = Object.keys(BookType).filter(
    (bookType) => typeof this.booksType[bookType] === 'number'
  );

  loading = false;

  isModalOpen = false;

  form: FormGroup;

  constructor(private store: Store, private fb: FormBuilder) {}

  ngOnInit() {
    this.get();
  }

  get() {
    this.loading = true;
    this.store
      .dispatch(new GetBooks())
      .pipe(finalize(() => (this.loading = false)))
      .subscribe(() => {});
  }

  createBook() {
    this.buildForm();
    this.isModalOpen = true;
  }

  buildForm() {
    this.form = this.fb.group({
      name: ['', Validators.required],
      type: [null, Validators.required],
      publishDate: [null, Validators.required],
      price: [null, Validators.required],
    });
  }
}
  • We imported NgbDateNativeAdapter, NgbDateAdapter

  • We added a new provider NgbDateAdapter that converts Datepicker value to Date type. See the datepicker adapters for more details.

  • We added bookTypeArr array to be able to use it in the combobox values. The bookTypeArr contains the fields of the BookType enum. Resulting array is shown below:

    ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
    

    This array was used in the previous form template in the ngFor loop.

Now, you can open your browser to see the changes:

New book modal

Saving the book

Open book-list.component.ts file in app\book\book-list folder and replace the content as below:

import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==>
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
  providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
  @Select(BookState.getBooks)
  books$: Observable<BookDto[]>;

  booksType = BookType;

  bookTypeArr = Object.keys(BookType).filter(
    (bookType) => typeof this.booksType[bookType] === 'number'
  );

  loading = false;

  isModalOpen = false;

  form: FormGroup;

  constructor(private store: Store, private fb: FormBuilder) {}

  ngOnInit() {
    this.get();
  }

  get() {
    this.loading = true;
    this.store
      .dispatch(new GetBooks())
      .pipe(finalize(() => (this.loading = false)))
      .subscribe(() => {});
  }

  createBook() {
    this.buildForm();
    this.isModalOpen = true;
  }

  buildForm() {
    this.form = this.fb.group({
      name: ['', Validators.required],
      type: [null, Validators.required],
      publishDate: [null, Validators.required],
      price: [null, Validators.required],
    });
  }

  // <== added save ==>
  save() {
    if (this.form.invalid) {
      return;
    }

    this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => {
      this.isModalOpen = false;
      this.form.reset();
      this.get();
    });
  }
}
  • We imported CreateUpdateBook.
  • We added save method

Open book-list.component.html in app\book\book-list folder and add the following abp-button to save the new book.

<ng-template #abpFooter>
  <button type="button" class="btn btn-secondary" #abpClose>
      {{ 'AbpAccount::Close' | abpLocalization }}
  </button>
    
  <!--added save button-->
  <button class="btn btn-primary" (click)="save()" [disabled]="form.invalid">
        <i class="fa fa-check mr-1"></i>
        {{ 'AbpAccount::Save' | abpLocalization }}
  </button>
</ng-template>

Find the <form [formGroup]="form"> tag and replace below content:

<form [formGroup]="form" (ngSubmit)="save()"> <!-- added the ngSubmit -->
  • We added the (ngSubmit)="save()" to <form> element to save a new book by pressing the enter.
  • We added abp-button to the bottom area of the modal to save a new book.

The final modal UI looks like below:

Save button to the modal

Updating a book

CreateUpdateBook action

Open the book.actions.ts in app\book\state folder and replace the content as below:

import { CreateUpdateBookDto } from '../models';

export class GetBooks {
  static readonly type = '[Book] Get';
}

export class CreateUpdateBook {
  static readonly type = '[Book] Create Update Book';
  constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
  • We added id parameter to the CreateUpdateBook action's constructor.

Open the book.state.ts in app\book\state folder and replace the save method as below:

@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
  if (action.id) {
    return this.bookService.updateByIdAndInput(action.payload, action.id);
  } else {
    return this.bookService.createByInput(action.payload);
  }
}

BookListComponent

Open book-list.component.ts in app\book\book-list folder and inject BookService dependency by adding it to the constructor and add a variable named selectedBook.

import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../services'; // <== imported BookService ==>

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
  providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
  @Select(BookState.getBooks)
  books$: Observable<BookDto[]>;

  booksType = BookType;

  bookTypeArr = Object.keys(BookType).filter(
    (bookType) => typeof this.booksType[bookType] === 'number'
  );

  loading = false;

  isModalOpen = false;

  form: FormGroup;

  selectedBook = {} as BookDto; // <== declared selectedBook ==>

  constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==>

  ngOnInit() {
    this.get();
  }

  get() {
    this.loading = true;
    this.store
      .dispatch(new GetBooks())
      .pipe(finalize(() => (this.loading = false)))
      .subscribe(() => {});
  }

  // <== this method is replaced ==>
  createBook() {
    this.selectedBook = {} as BookDto; // <== added ==>
    this.buildForm();
    this.isModalOpen = true;
  }

  // <== added editBook method ==>
  editBook(id: string) {
    this.bookService.getById(id).subscribe((book) => {
      this.selectedBook = book;
      this.buildForm();
      this.isModalOpen = true;
    });
  }

  // <== this method is replaced ==>
  buildForm() {
    this.form = this.fb.group({
      name: [this.selectedBook.name || '', Validators.required],
      type: [this.selectedBook.type || null, Validators.required],
      publishDate: [
        this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null,
        Validators.required,
      ],
      price: [this.selectedBook.price || null, Validators.required],
    });
  }

  save() {
    if (this.form.invalid) {
      return;
    }

    //<== added this.selectedBook.id ==>
    this.store
      .dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
      .subscribe(() => {
        this.isModalOpen = false;
        this.form.reset();
        this.get();
      });
  }
}
  • We imported BookService.
  • We declared a variable named selectedBook as BookDto.
  • We injected BookService to the constructor. BookService is being used to retrieve the book data which is being edited.
  • We added editBook method. This method fetches the book with the given Id and sets it to selectedBook object.
  • We replaced the buildForm method so that it creates the form with the selectedBook data.
  • We replaced the createBook method so it sets selectedBook to an empty object.
  • We added selectedBook.id to the constructor of the new CreateUpdateBook.

Add "Actions" dropdown to the table

Open the book-list.component.html in app\book\book-list folder and replace the <div class="card-body"> tag as below:

<div class="card-body">
  <abp-table
    [value]="books$ | async"
    [abpLoading]="loading"
    [headerTemplate]="tableHeader"
    [bodyTemplate]="tableBody"
    [rows]="10"
    [scrollable]="true"
  >
  </abp-table>
  <ng-template #tableHeader>
    <tr>
      <th>{{ "::Actions" | abpLocalization }}</th>
      <th>{{ "::Name" | abpLocalization }}</th>
      <th>{{ "::Type" | abpLocalization }}</th>
      <th>{{ "::PublishDate" | abpLocalization }}</th>
      <th>{{ "::Price" | abpLocalization }}</th>
    </tr>
  </ng-template>
  <ng-template #tableBody let-data>
    <tr>
      <td>
        <div ngbDropdown container="body" class="d-inline-block">
          <button
            class="btn btn-primary btn-sm dropdown-toggle"
            data-toggle="dropdown"
            aria-haspopup="true"
            ngbDropdownToggle
          >
            <i class="fa fa-cog mr-1"></i>{{ "::Actions" | abpLocalization }}
          </button>
          <div ngbDropdownMenu>
            <button ngbDropdownItem (click)="editBook(data.id)">
              {{ "::Edit" | abpLocalization }}
            </button>
          </div>
        </div>
      </td>
      <td>{{ data.name }}</td>
      <td>{{ booksType[data.type] }}</td>
      <td>{{ data.publishDate | date }}</td>
      <td>{{ data.price }}</td>
    </tr>
  </ng-template>
</div>
  • We added a th for the "Actions" column.
  • We added button with ngbDropdownToggle to open actions when clicked the button.
  • We have used to NgbDropdown for the dropdown menu of actions.

The final UI looks like as below:

Action buttons

Open book-list.component.html in app\book\book-list folder and find the <ng-template #abpHeader> tag and replace the content as below.

<ng-template #abpHeader>
    <h3>{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}</h3>
</ng-template>
  • This template will show Edit text for edit record operation, New Book for new record operation in the title.

Deleting a book

DeleteBook action

Open book.actions.ts in app\book\state folder and add an action named DeleteBook.

export class DeleteBook {
  static readonly type = '[Book] Delete';
  constructor(public id: string) {}
}

Open the book.state.ts in app\book\state folder and replace the content as below:

import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';

export class BookStateModel {
  public book: PagedResultDto<BookDto>;
}

@State<BookStateModel>({
  name: 'BookState',
  defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
  @Selector()
  static getBooks(state: BookStateModel) {
    return state.book.items || [];
  }

  constructor(private bookService: BookService) {}

  @Action(GetBooks)
  get(ctx: StateContext<BookStateModel>) {
    return this.bookService.getListByInput().pipe(
      tap((booksResponse) => {
        ctx.patchState({
          book: booksResponse,
        });
      })
    );
  }

  @Action(CreateUpdateBook)
  save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
    if (action.id) {
      return this.bookService.updateByIdAndInput(action.payload, action.id);
    } else {
      return this.bookService.createByInput(action.payload);
    }
  }

  // <== added DeleteBook action listener ==>
  @Action(DeleteBook)
  delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
    return this.bookService.deleteById(action.id);
  }
}
  • We imported DeleteBook .

  • We added DeleteBook action listener to the end of the file.

Delete confirmation popup

Open book-list.component.ts inapp\book\book-list folder and inject the ConfirmationService.

Replace the constructor as below:

import { ConfirmationService } from '@abp/ng.theme.shared';
//...

constructor(
    private store: Store, 
    private fb: FormBuilder,
    private bookService: BookService,
    private confirmation: ConfirmationService // <== added this line ==>
) { }
  • We imported ConfirmationService.
  • We injected ConfirmationService to the constructor.

See the Confirmation Popup documentation

In the book-list.component.ts add a delete method :

import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==>

import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>

//...

delete(id: string) {
    this.confirmation
        .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
        .subscribe(status => {
        if (status === Confirmation.Status.confirm) {
            this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get());
        }
    });
}

The delete method shows a confirmation popup and subscribes for the user response. DeleteBook action dispatched only if user clicks to the Yes button. The confirmation popup looks like below:

bookstore-confirmation-popup

Add a delete button

Open book-list.component.html in app\book\book-list folder and modify the ngbDropdownMenu to add the delete button as shown below:

<div ngbDropdownMenu>
  <!-- added Delete button -->
    <button ngbDropdownItem (click)="delete(data.id)">
        {{ 'AbpAccount::Delete' | abpLocalization }}
    </button>
</div>

The final actions dropdown UI looks like below:

bookstore-final-actions-dropdown

Next Part

See the next part of this tutorial.

In this document
Mastering ABP Framework
Mastering ABP Framework Book

Build maintainable .NET solutions by following software development best practices using ABP.

See Details