Abp vNext - 应用开发系列之增删改

在系列教程中,我们会构建一个基于ABP的Web应用程序,用于管理书籍及其作者列表

使用到的技术:

  • Entity Framework Core
  • Angular

本教程分为以下部分:

创建新书籍

本节我们完成一个基本的增删改操作

book manager

BookComponent

/src/app/book/book.component.ts中新增以下内容:

  • isModalOpen属性:用于判断模态框的打开关闭
  • createBook方法:新增按钮调用方法,打开弹窗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookService, BookDto} from '@proxy/books';

@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [ListService],
})
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;

// 是否打开弹窗
isModalOpen = false;

constructor(
public readonly list: ListService,
private bookService: BookService,
) {}

ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);

this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}

/**
* 创建图书
*/
createBook() {
this.isModalOpen = true;
}
}

打开 /src/app/book/book.component.html

  • 新增创建按钮
  • 新增模态框
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<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>
<div class="text-end col col-md-6">
<!-- 新增按钮在这里!!! -->
<div class="text-lg-end 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">
<!-- 表格 -->
</div>
</div>

<!-- 模态框组件 -->
<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>
{{ '::Close' | abpLocalization }}
</button>
</ng-template>
</abp-modal>

这个时候,点击新增按钮弹窗如下:

modal

添加弹窗表单

打开 /src/app/book/book.component.ts 新增以下内容:

  • @angular/forms导入 FormGroupFormBuilderValidators
  • 添加 form: FormGroup 变量
  • 添加 bookTypes 属性作为 BookType 枚举成员列表,在表单图书类型选项中使用
  • 注入FormBuilder 到构造函数
  • 添加 buildForm 方法到文件末尾,在 createBook 方法调用 buildForm() 方法初始化
  • 添加save 方法,用于保存表单填写数据到后台(发起请求)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; // 导入 bookTypeOptions
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // 导入

@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [ListService],
})
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;

form: FormGroup; // 定义form

// 图书类型选项
bookTypes = bookTypeOptions;

isModalOpen = false;

constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder // 注入 FormBuilder
) {}

ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);

this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}

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],
});
}

// 新增保存方法
save() {
if (this.form.invalid) {
return;
}

this.bookService.create(this.form.value).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
}
}

打开 /src/app/book/book.component.html新增以下内容:

  • 表单
  • 操作按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3>{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}</h3>
</ng-template>

<ng-template #abpBody>
<form [formGroup]="form" (ngSubmit)="save()">
<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]="type.value" *ngFor="let type of bookTypes">{{ type.key }}</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>

<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" abpClose>
{{ '::Close' | abpLocalization }}
</button>

<button class="btn btn-primary" (click)="save()" [disabled]="form.invalid">
<i class="fa fa-check mr-1"></i>
{{ '::Save' | abpLocalization }}
</button>
</ng-template>
</abp-modal>

Datepicker

在这个组件中使用了NgBootstrap datepicker,需要添加与此组件相关的依赖项,打开 /src/app/book/book.module.ts 新增以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { BookRoutingModule } from './book-routing.module';
import { BookComponent } from './book.component';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; // 导入

@NgModule({
declarations: [BookComponent],
imports: [
BookRoutingModule,
SharedModule,
NgbDatepickerModule, // 注册
]
})
export class BookModule { }

/src/app/book/book.component.ts 中使用NgbDateAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

// 导入
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';

@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [
ListService,
{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter } // 提供器
],
})

打开弹窗

弹窗

更新书籍

打开 /src/app/book/book.component.ts 新增以下内容:

  • 声明类型为 BookDtoselectedBook 变量,用于存储获取到的Book对象
  • 添加了editBook 方法,根据选择的书籍 Id 设置 selectedBook 对象
  • 替换 buildForm 方法使用 selectedBook 数据初始化表单
  • 替换 createBook 方法,设置 selectedBook 为空
  • 修改 save 方法,同时处理新增和更新操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';

@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;

selectedBook = {} as BookDto; // 定义 selectedBook

form: FormGroup;

bookTypes = bookTypeOptions;

isModalOpen = false;

constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}

ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);

this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}

createBook() {
this.selectedBook = {} as BookDto; // 创建时将selectedBook置空
this.buildForm();
this.isModalOpen = true;
}

// 编辑书籍根据选择的Id查询Book信息
editBook(id: string) {
this.bookService.get(id).subscribe((book) => {
this.selectedBook = book;
this.buildForm();
this.isModalOpen = true;
});
}

buildForm() {
// 判断selectedBook是否有值,区分是否编辑或新增
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;
}

// 判断selectedBook.id是否有值
// true:更新
// false:新增
const request = this.selectedBook.id
? this.bookService.update(this.selectedBook.id, this.form.value)
: this.bookService.create(this.form.value);

request.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
}
}

添加表格操作按钮

打开 /src/app/book/book.component.htmlngx-datatable 第一列添加 ngx-datatable-column 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<ngx-datatable-column
[name]="'::Actions' | abpLocalization"
[maxWidth]="150"
[sortable]="false"
>
<ng-template let-row="row" ngx-datatable-cell-template>
<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(row.id)">
{{ '::Edit' | abpLocalization }}
</button>
</div>
</div>
</ng-template>
</ngx-datatable-column>

在表格的第一列添加了一个 Actions 下拉菜单,如下图所示:

action

同时如下所示更改 ng-template #abpHeader 部分:

  • 根据是否有选中的值判断弹窗展示文本
1
2
3
<ng-template #abpHeader>
<h3>{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}</h3>
</ng-template>

删除书籍

打开 /src/app/book/book.component.ts 注入 ConfirmationService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...

// 新增导入
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';

constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService // 注入
) {}

// 新增删除方法
delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.delete(id).subscribe(() => this.list.get());
}
});
}

添加删除按钮

打开 /src/app/book/book.component.html 修改 ngbDropdownMenu 添加删除按钮

1
2
3
4
5
6
<div ngbDropdownMenu>
<!-- add the Delete button -->
<button ngbDropdownItem (click)="delete(row.id)">
{{ '::Delete' | abpLocalization }}
</button>
</div>

action

点击 delete 操作调用 delete 方法,然后显示一个确认弹层如下图所示:

confirm

至此就完成了对Book的增删改操作

下一节

教程下一节