这篇教程我们来讲述一下如何构建一个常见的列表页面,这个页面通常会包含增删改查的功能。
第一步,我们先加载一个简单的subject的页面。
- 首先我们新建一个subject的组件,同样可以利用idea的快捷键来创建,在app文件夹上右键,依次选择New->Angular Schematic->component,输入subject,ok确认。
- 接着给这个subject组件添加一个路由,我们希望通过/home/subject这样的路径来访问这个页面,并且这个subject的页面是通过home组件的<router-outlet></router-outlet>的路由入口加载进来的,所以需要把subject的路由添加到home的子路由中。因此修改app-routing.module.ts如下
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: '/home' },
{ path: 'home', component: HomeComponent, canActivate: [LoginGuard], children: [
{path: 'subject', component: SubjectComponent}] },
{ path: 'login', component: LoginComponent}
];
- 修改home.component.html中的左侧菜单栏部分
<ul nz-menu nzTheme="dark" nzMode="inline" [nzInlineCollapsed]="isCollapsed">
<li nz-submenu nzOpen nzTitle="师生管理中心" nzIcon="user">
<ul>
<li nz-menu-item nzMatchRouter>
<a routerLink="subject">科目</a>
</li>
</ul>
</li>
</ul>
这里只保留了一个一级菜单“师生管理中心”,下面包含一个“科目”的二级菜单,链接到subject页面,这里的subject是一个相对路径,是相对于当前路径/home而言的,如果要用完整路径的话就要在最前面加上"/"即"/home/subject"。
现在运行项目,登录成功后进入home页面,再点击科目就跳转到了"/home/subject"路径,我们可以看到右侧出现了"subject works!"字样,说明成功加载了subject页面。
第二步,和上一篇教程中的login功能类似,我们需要先创建一个处理http请求的service。在service路径下面新建一个subject.service.ts的文件。按照后台api的格式发送增删改查的请求。
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
import qs from 'qs';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' })
};
@Injectable({
providedIn: 'root'
})
export class SubjectService {
subjectUrl = '/studentmanage/subject/';
getSubjects(
pageIndex: number = 0,
pageSize: number = 10,
name: string
): Observable<any> {
const params = new HttpParams()
.append('index', `${pageIndex}`)
.append('size', `${pageSize}`)
.append('name', name);
return this.http.get(`${this.subjectUrl}subjects`, {
params
});
}
deleteSubject(
id: string
): Observable<any> {
const params = new HttpParams().append('id', id);
return this.http.post(`${this.subjectUrl}deleteSubject`, params, httpOptions);
}
editSubject(
id: string,
name: string
): Observable<any> {
const options = { id, name };
return this.http.post(`${this.subjectUrl}editSubject`, qs.stringify(options), httpOptions);
}
addSubject(
name: string
): Observable<any> {
const options = { name };
return this.http.post(`${this.subjectUrl}addSubject`, qs.stringify(options), httpOptions);
}
constructor(private http: HttpClient) {}
}
这里关于请求参数,我用了两种写法,分别是上一篇教程中login service中用的qs库和angular自带的HttpParams,这两种用法效果是一致的,都是为了拼接url中的请求参数。
第三步,我们要改造模板视图subject.component.html和组件的逻辑代码subject.component.ts。这部分的代码主要参考ng-zorro的官方文档表格中的远程加载数据和可编辑行两部分。
- 修改subject.component.html
<nz-table #ajaxTable
nzShowSizeChanger
[nzFrontPagination]="false"
[nzData]="subjects"
[nzLoading]="loading"
[nzTotal]="total"
[(nzPageIndex)]="pageIndex"
[(nzPageSize)]="pageSize"
(nzPageIndexChange)="searchData()"
(nzPageSizeChange)="searchData(true)" class="my-table">
<thead>
<tr>
<th nzWidth="5%">#</th>
<th>科目名称</th>
<th nzWidth="20%">操作</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of ajaxTable.data;let i = index">
<td>{{i+1}}</td>
<td>
<ng-container *ngIf="!editCache[data.id].edit; else nameInputTpl">
{{ data.name }}
</ng-container>
<ng-template #nameInputTpl>
<input type="text" nz-input [(ngModel)]="editCache[data.id].data.name" />
</ng-template>
</td>
<td>
<div class="editable-row-operations">
<ng-container *ngIf="!editCache[data.id].edit; else saveTpl">
<a (click)="startEdit(data.id)">编辑</a>
<nz-divider nzType="vertical"></nz-divider>
<a nz-popconfirm nzTitle="确定要删除吗?" (nzOnConfirm)="delete(data.id)">删除</a>
</ng-container>
<ng-template #saveTpl>
<a nz-popconfirm nzTitle="确定要保存吗?" (nzOnConfirm)="saveEdit(data.id)">保存</a>
<nz-divider nzType="vertical"></nz-divider>
<a (click)="cancelEdit(data.id)">取消</a>
</ng-template>
</div>
</td>
</tr>
</tbody>
</nz-table>
- 修改subject.component.ts
import { Component, OnInit } from '@angular/core';
import {SubjectService} from '../service/subject.service';
@Component({
selector: 'app-subject',
templateUrl: './subject.component.html',
styleUrls: ['./subject.component.scss']
})
export class SubjectComponent implements OnInit {
title = '科目';
subjects = [];
pageIndex = 1;
pageSize = 5;
total = 1;
loading = true;
editCache: { [key: string]: any } = {};
searchValue = '';
constructor(private subjectService: SubjectService) {}
ngOnInit(): void {
this.searchData();
}
searchData(reset: boolean = false): void {
if (reset) {
this.pageIndex = 1;
}
this.loading = true;
this.subjectService.getSubjects(this.pageIndex - 1, this.pageSize, this.searchValue)
.subscribe(result => this.onSuccess(result));
}
onSuccess(result: any) {
this.loading = false;
console.log('result: ' + JSON.stringify(result));
const data = result.data;
this.subjects = data.subjects;
this.total = data.total;
this.updateEditCache();
}
startEdit(id: string): void {
this.editCache[id].edit = true;
}
cancelEdit(id: string): void {
const index = this.subjects.findIndex(item => item.id === id);
this.editCache[id] = {
data: { ...this.subjects[index] },
edit: false
};
}
saveEdit(id: string): void {
this.subjectService.editSubject(id, this.editCache[id].data.name).subscribe(result => {
const index = this.subjects.findIndex(item => item.id === id);
Object.assign(this.subjects[index], this.editCache[id].data);
this.editCache[id].edit = false;
});
}
updateEditCache(): void {
this.subjects.forEach(item => {
this.editCache[item.id] = {
edit: false,
data: { ...item }
};
});
}
delete(id: string): void {
this.subjectService.deleteSubject(id).subscribe(result => this.searchData());
}
}
这里我们就讲一下用到的一些angular的模板语法
(1)angular模板支持几乎所有的html元素和语法,除了<script>,还有<html>、<body> 和 <base>这些元素是无用的,其他都是和原生html一样使用。例如我们这里使用了表格元素<tr><td>等。在这个基础上,angular模板还有扩展的元素和语法,如<nz-table>这个就是扩展的元素,是由ng-zorro这个UI框架定义的。
(2)以“#”开头的“#ajaxTable”表示模板引用变量,就是指这个nz-table本身。到下面表格正文就引用了这个变量<tr *ngFor="let data of ajaxTable.data;let i = index">
(3)未加任何括号的属性代表一个无绑定的常量属性如nzShowSizeChanger,它是一个布尔类型的属性常量,值为true。同样还有<nz-divider nzType="vertical"></nz-divider>种的nzType等。
(4)加了[]的属性代表了该属性被绑定到了一个""中的变量或者常量上,比如[nzData]="subjects"这个属性,这里的subjects实际上是一个变量,在subject.component.ts中我们将它声明为一个数组,把从后台获取的subjects数据赋值给它,这样模板视图就可以根据subjects的值更新界面。
(5)加了()的代表的是绑定的事件,""里面一般都是写该事件发生时调用的方法。例如<a (click)="startEdit(data.id)">编辑</a>这里是一个最典型点击事件,点击这个链接后调用startEdit方法。
(6)像[(nzPageIndex)]这种加了[()]的则代表了双向绑定的属性,从符号上可以看出来,代表了属性绑定和事件绑定的结合,既绑定了这个属性,又可以随时监听这个属性的变化。
(7)<tr *ngFor="let data of ajaxTable.data;let i = index">
<td>{{i+1}}</td>
<td>
<ng-container *ngIf="!editCache[data.id].edit; else nameInputTpl">
{{ data.name }}
</ng-container>
因为这几句有上下文关系,所以放到一起看,里面包含了几个语法点:
a. *ngFor:这是一个结构指令,类似于js代码中的for循环语句,引号中的这一小段看起来像js代码的官方解释为微语法(microsyntax)—— 由 Angular 解释的一种小型语言。
b. *ngIf:同样是一个结构指令,从名称看类似于js中的if选择语句,当然引号中的也同样可以看作是微语法。
c. 插值与模板表达式:就是把变量或者表达式插入到文本标记中,用{{···}}表示,如{{i+1}}和{{ data.name }}。
d. 表达式上下文:这里我们可以看到在微语法语句中可以引用上面用“#”标记的模板引用变量,而微语法中可以声明变量data和i,在下文的插值和模板表达式中又可以引用这两个变量,这里都是有上下文关系的。表达式中的上下文变量是由模板变量(ajaxTable)、指令的上下文变量(data)和组件的成员(subject)叠加而成的。 这三者的优先级是依次递减的。
到这里我们这个subject列表的删改查功能其实已经完成了,目前还缺少一个增的功能。
第三步,添加一个subjectForm的组件。
- 在subject路径下新建一个subjectForm的component,修改subject-form.component.html
<nz-page-header nzBackIcon [nzTitle]="title">
</nz-page-header>
<form nz-form [formGroup]="subjectForm" (ngSubmit)="submitForm()">
<nz-form-item>
<nz-form-label [nzSpan]="3" nzRequired nzFor="name">科目名称</nz-form-label>
<nz-form-control [nzSpan]="8" nzErrorTip="请输入科目名称">
<input type="text" nz-input formControlName="name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzSpan]="8" [nzOffset]="4">
<button nz-button nzType="primary">提交</button>
</nz-form-control>
</nz-form-item>
</form>
这里的写法我们基本也是参考ngzorro的官方文档的表单Form部分。
- 修改subject-form.component.ts
import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {Router} from '@angular/router';
import {SubjectService} from '../../service/subject.service';
import { Location } from '@angular/common';
@Component({
selector: 'app-subject-form',
templateUrl: './subject-form.component.html',
styleUrls: ['./subject-form.component.scss']
})
export class SubjectFormComponent implements OnInit {
title = '新增科目';
subjectForm: FormGroup;
submitForm(): void {
for (const i in this.subjectForm.controls) {
this.subjectForm.controls[i].markAsDirty();
this.subjectForm.controls[i].updateValueAndValidity();
if (this.subjectForm.controls[i].invalid) {
return;
}
}
this.subjectService.addSubject(this.subjectForm.value.name)
.subscribe(result => this.location.back());
}
constructor(private fb: FormBuilder, private subjectService: SubjectService,
private location: Location) {}
ngOnInit(): void {
this.subjectForm = this.fb.group({
name: [null, [Validators.required]]
});
}
}
在上一篇教程讲述登录功能的时候我们就已经用到了表单提交的功能,这边也是类似的。
- 在app-routing.module.ts中添加路由,
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: '/home' },
{ path: 'home', component: HomeComponent, canActivate: [LoginGuard], children: [
{path: 'subject', component: SubjectComponent},
{path: 'subject/addSubject', component: SubjectFormComponent}] },
{ path: 'login', component: LoginComponent}
];
并且在subject.component.html的最上方添加一个新增的按钮,点击可以跳转到新增的页面
<button nz-button nzType="primary" class="add-button" routerLink="addSubject">新增</button>
完成这些之后,我们可以重启项目,登录之后选中科目,尝试一下增删改的功能。
代码依然可以参考https://github.com/ahuadoreen/studentmanager-cli。