65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Angular 构建带有 CRUD 操作和高级列过滤的数据表

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2020 年 12 月 29 日

CPOL

5分钟阅读

viewsIcon

45532

downloadIcon

932

一个包含 CRUD 操作、列过滤、表单对话框、确认对话框和 BehaviorSubject 的 Angular 应用程序

 

引言

本文的主要目标是学习如何

  • 通过 CRUD 操作操作 mat-table
  • 在 mat-table 上添加列过滤
  • 创建表单对话框
  • 创建确认对话框
  • 使用 BehaviorSubject

为了实现这些目标,我们将创建一个用于 persons(人员)管理的应用程序。

必备组件

要很好地理解本文,您应该对 Angular、JavaScript/Typescript、HTML 和 CSS 有一些了解。

在我们开始之前,我们需要设置 Angular 环境。为此,我建议您访问 Angular 官方文档

创建新应用程序并设置 Angular Material

在此演示中,我使用了Angular 版本 9

  1. 首先,我们需要通过运行以下命令行来创建一个新的 Angular 应用程序
    ng new angular-datatable
  2. 接下来,安装 Angular Material,为您的 UI 组件获得精美的设计,方法是运行此命令行
    ng add @angular/material
  3. app.module.ts 中声明所有需要的 Angular Material 组件模块
       imports: [
        BrowserModule,
        BrowserAnimationsModule,
        
        CdkTableModule,
        MatTableModule,
        MatPaginatorModule,
        MatSortModule,
        MatMenuModule,
        MatIconModule,
        MatButtonModule,
        MatDialogModule,
        ReactiveFormsModule,
        MatInputModule,
        MatSelectModule
      ],

创建模型

  1. 首先,我们必须定义一个名为 Person 的实体类。它包含以下属性
    • Id:唯一标识符
    • FirstName:名字
    • 年龄
    • Job:一个人可能拥有的工作名称,例如:牙医、软件开发人员...
      export class Person {
    
      id?: number;
      firstName: string;
      age: number;
      job: string;
    
      constructor(id: number = null, 
                  firstName: string = '', age: number = 0, job: string = '') {
        this.id = id;
        this.firstName = firstName;
        this.age = age;
        this.job = job;
      }
    }
  2. 然后,我们需要为我们的项目声明一个 persons(人员)数组,该数组仅在客户端运行。这些数据就像本地数据存储。
    import { Person } from "../models/person";
    
    export const personsData: Person[] = [
      new Person(1, 'person 1', 30, 'Software Developer'),
      new Person(2, 'person 2', 33, 'Dentist'),
      new Person(3, 'person 3', 32, 'Physician Assistant'),
      new Person(4, 'person 4', 33, 'Software Developer'),
      new Person(5, 'person 5', 34, 'Software Developer'),
      new Person(6, 'person 6', 25, 'Nurse'),
      new Person(7, 'person 7', 36, 'Software Developer'),
      new Person(8, 'person 8', 27, 'Physician'),
      new Person(9, 'person 9', 28, 'Software Developer'),
      new Person(10, 'person 10', 28, 'Software Developer')
    ]

实现 CRUD 操作

为了管理人员的数据存储,我们需要为此创建一个 Angular 服务。

ng generate service person.service

该服务包含

  • persons$BehaviorSubject<Person[]> 类型,这种可观察对象用于将接收到的消息推送到所有订阅者。在我们的示例中,我们使用它在 CRUD 操作后刷新数据表
  • persons:包含我们数据存储的副本,在每次 CRUD 操作后都会更新
  • getAll():返回可用人员列表
  • edit(person: Person):替换现有实体的某些属性并刷新显示的列表
  • remove(id: number):从数据存储中删除现有实体并刷新数据表的显示条目
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { personsData } from '../constants/persons-static-data';
import { Person } from '../models/person';

@Injectable({
  providedIn: 'root'
})

export class PersonService {

  persons$: BehaviorSubject<Person[]>;
  persons: Array<Person> = [];

  constructor() {
    this.persons$ = new BehaviorSubject([]);
    this.persons = personsData;
  }

  getAll() {
    this.persons$.next(this.persons);
  }

  add(person: Person) {
    this.persons.push(person);
  }

  edit(person: Person) {
    let findElem = this.persons.find(p => p.id == person.id);
    findElem.firstName = person.firstName;
    findElem.age = person.age;
    findElem.job = person.job;
    this.persons$.next(this.persons);
  }

  remove(id: number) {
   
    this.persons = this.persons.filter(p => {
      return p.id != id
    });

    this.persons$.next(this.persons);
  }
}

显示数据

  1. app/components 文件夹内运行以下命令行创建 DataTableComponent
    ng g c data-table

    此组件是主组件,它包含显示和管理 persons(人员)列表的数据表,并通过自定义过滤器(稍后实现)进行列过滤。要详细了解 mat-table,您可以访问此链接

  2. 接下来,我们需要通过编辑 data-table.component.html 来准备 HTML 模板
    <div class="mat-elevation-z8">
      <table mat-table [dataSource]="dataSource" matSort class="mat-elevation-z8">
        <ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
          <th mat-header-cell *matHeaderCellDef>
            <div style="display: flex; align-items: center;">
              <span mat-sort-header>{{column}}</span>
             
            </div>
          </th>
          <td mat-cell *matCellDef="let element"> {{element[column]}} </td>
        </ng-container>
        <ng-container [matColumnDef]="'actions'">
          <th mat-header-cell *matHeaderCellDef> actions </th>
          <td mat-cell *matCellDef="let element">
            <button mat-icon-button (click)="edit(element)">
              <mat-icon mat-icon-button color='primary'>edit</mat-icon>
            </button>
            <button mat-icon-button (click)="delete(element['id'])">
              <mat-icon mat-icon-button color="warn">delete</mat-icon>
            </button>
          </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
        <tr mat-row *matRowDef="let row; columns: columnsToDisplay;"></tr>
      </table>
    
      <mat-paginator [pageSize]="5" 
      [pageSizeOptions]="[5, 10, 50]" showFirstLastButtons></mat-paginator>
    
    </div>
  3. 然后,我们应该通过编辑 data-table.component.ts 来进行实现部分
    import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
    import { Subscription } from 'rxjs';
    
    import { MatPaginator } from '@angular/material/paginator';
    import { MatTableDataSource } from '@angular/material/table';
    import { MatSort } from '@angular/material/sort';
    import { MatDialog } from '@angular/material/dialog';
    import { ConfirmationDialogComponent } _
             from '../confirmation-dialog/confirmation-dialog.component';
    import { PersonFormDialogComponent } _
             from '../person-form-dialog/person-form-dialog.component';
    import { PersonService } from 'src/app/core/services/person.service';
    import { Person } from 'src/app/core/models/person';
    
    @Component({
      selector: 'app-data-table',
      templateUrl: './data-table.component.html',
      styleUrls: ['./data-table.component.scss']
    })
    export class DataTableComponent implements OnInit, OnDestroy, AfterViewInit {
      @ViewChild(MatPaginator) paginator: MatPaginator;
      @ViewChild(MatSort) sort: MatSort;
    
      public displayedColumns: string[] = ['firstName', 'age', 'job'];
      public columnsToDisplay: string[] = [...this.displayedColumns, 'actions'];
    
      /**
       * it holds a list of active filter for each column.
       * example: {firstName: {contains: 'person 1'}}
       *
       */
      public columnsFilters = {};
    
      public dataSource: MatTableDataSource<person>;
      private serviceSubscribe: Subscription;
    
      constructor(private personsService: PersonService, public dialog: MatDialog) {
        this.dataSource = new MatTableDataSource<person>();
      }   
    
      edit(data: Person) {
        const dialogRef = this.dialog.open(PersonFormDialogComponent, {
          width: '400px',
          data: data
        });
    
        dialogRef.afterClosed().subscribe(result => {
          if (result) {
            this.personsService.edit(result);
          }
        });
      }
    
      delete(id: any) {
        const dialogRef = this.dialog.open(ConfirmationDialogComponent);
    
        dialogRef.afterClosed().subscribe(result => {
          if (result) {
            this.personsService.remove(id);
          }
        });
      }
    
      ngAfterViewInit(): void {
        this.dataSource.paginator = this.paginator;
        this.dataSource.sort = this.sort;
      }
    
      /**
       * initialize data-table by providing persons list to the dataSource.
       */
      ngOnInit(): void {
        this.personsService.getAll();
        this.serviceSubscribe = this.personsService.persons$.subscribe(res => {
          this.dataSource.data = res;
        })
      }
    
      ngOnDestroy(): void {
        this.serviceSubscribe.unsubscribe();
      }
    }
    
    </person></person>

    为了将数据加载到 mat-table 中,我们需要从 persons 服务获取列表。为此,我们需要调用 persons 服务的 getAll 方法并订阅 persons$ 可观察对象。
    我们还准备了用于删除和编辑操作的空方法。

删除现有人员

  1. app/components 文件夹内运行以下命令行创建 ConfirmationDialogComponent
    ng g c confirmation-dialog

    此组件用于向希望执行关键操作(如 delete 操作)的用户显示确认操作对话框。

    为了在此对话框中显示此组件,我们使用MatDialog 服务
    MatDialog 服务负责显示对话框、将数据传递到对话框以及配置它。

    当此对话框打开时,用户将获得两个选择

    • Yes:它通过向 afterClosed 可观察对象返回 true 并通过调用 person 服务的 delete 方法来刷新数据存储来确认操作。

    • No:它通过向 afterClosed 可观察对象返回 false 来拒绝操作。

  2. 接下来,我们需要编辑 confirmation-dialog.component.html 的模板文件
    <h1 mat-dialog-title>Confirm action</h1>
    <div mat-dialog-content>Are you sure to want remove this item ?</div>
    <div mat-dialog-actions class="mt-15">
      <button mat-raised-button color="primary" 
      [mat-dialog-close]="true" cdkFocusInitial>Yes</button>
      <button mat-raised-button mat-dialog-close>No</button>
    </div>
  3. app.module.ts 中将此组件声明为 entryComponent
     entryComponents: [ConfirmationDialogComponent]
  4. data-table.component.ts 中实现删除操作
      delete(id: any) {
        const dialogRef = this.dialog.open(ConfirmationDialogComponent);
    
        dialogRef.afterClosed().subscribe(result => {
          if (result) {
            this.personsService.remove(id);
          }
        });
      }
  5. 最后,当您运行此应用程序时,您应该能够在确认操作后删除现有用户。

更新现有人员

  1. 在 app/components 文件夹内运行以下命令行创建 PersonFormDialogComponent
    ng g c person-form-dialog

    此组件将在表单对话框中显示选定人员的数据,用户可以对其属性进行一些更改。
    为了创建此表单,我们使用响应式表单方法来深入控制表单状态并以有效的方式进行输入验证。

    当用户单击数据表行中的编辑图标时,选定的人员将通过使用 MatDialog 服务MAT_DIALOG_DATA 被注入到表单对话框组件中。
    如果表单有效,用户可以保存更改,结果将传递给 afterClosed 可观察对象,由 persons 服务的 edit 方法进行处理。
    对于我们的示例,我们假设所有表单控件都是必填字段,否则用户将无法保存更改。

  2. 接下来,我们需要构建我们的模板 person-form-dialog.component.html
    <h1 mat-dialog-title>Edit Person</h1>
    
    <div mat-dialog-content>
      <form [formGroup]="formInstance">
        <div>
          <mat-form-field class="fullWidth" appearance="outline">
            <mat-label>first Name *</mat-label>
            <input matInput type="text" 
            name="firstName" formControlName="firstName">
            <mat-error *ngIf="
            formInstance.controls['firstName']?.errors?.required">field required</mat-error>
          </mat-form-field>
        </div>
    
        <div class="mt-5">
          <mat-form-field class="fullWidth" appearance="outline">
            <mat-label>Age *</mat-label>
            <input matInput type="number" name="age" formControlName="age" />
            <mat-error *ngIf="
            formInstance.controls['age']?.errors?.required">field required</mat-error>
          </mat-form-field>
        </div>
    
        <div class="mt-5">
    
          <mat-form-field class="fullWidth" appearance="outline">
            <mat-label>Job *</mat-label>
            <mat-select name="job" formControlName="job">
              <mat-option value="Software Developer">Software Developer</mat-option>
              <mat-option value="Physician">Physician</mat-option>
              <mat-option value="Dentist">Dentist</mat-option>
              <mat-option value="Nurse">Nurse</mat-option>
            </mat-select>
            <mat-error *ngIf="formInstance.controls['job']?.errors?.required">field required
            </mat-error>
          </mat-form-field>
    
        </div>
      </form>
    </div>
    
    <div class="mt-5" mat-dialog-actions>
      <button mat-raised-button color="primary" 
      [disabled]="formInstance.dirty && formInstance.errors" (click)="save()"
        cdkFocusInitial>Yes</button>
      <button mat-raised-button mat-dialog-close>No</button>
    
    </div>
  3. 编辑 person-form-dialog.component.ts
    import { Component, Inject, OnInit } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
    import { Person } from 'src/app/core/models/person';
    
    @Component({
      selector: 'app-form-dialog',
      templateUrl: './person-form-dialog.component.html',
      styleUrls: ['./person-form-dialog.component.scss']
    })
    
    export class PersonFormDialogComponent implements OnInit {
      formInstance: FormGroup;
    
      constructor(public dialogRef: MatDialogRef<PersonFormDialogComponent>,
        @Inject(MAT_DIALOG_DATA) public data: Person) {
        this.formInstance = new FormGroup({
          "id":  new FormControl('', Validators.required),
          "firstName": new FormControl('', Validators.required),
          "age": new FormControl('', Validators.required),
          "job": new FormControl('', Validators.required),
        });
    
        this.formInstance.setValue(data);
      }
    
      ngOnInit(): void {
    
      }
    
      save(): void {
        this.dialogRef.close(Object.assign(new Person(), this.formInstance.value));
      }
    }
  4. data-table.component.ts 中实现 edit 方法
      edit(data: Person) {
        const dialogRef = this.dialog.open(PersonFormDialogComponent, {
          width: '400px',
          data: data
        });
    
        dialogRef.afterClosed().subscribe(result => {
          if (result) {
            this.personsService.edit(result);
          }
        });
      }
  5. 最后,当我们运行应用程序时,我们应该得到这个结果

实现列过滤

想法是通过在特定列中搜索某些值来过滤数据。
我们的文本过滤器是累加的,我们可以组合来自不同列的多个搜索条件。
对于我们的示例,我们将实现这些 string(字符串)比较方法,这些方法不区分大小写

  • contains:数据应包含搜索值的子字符串
  • equals:数据应等于搜索值
  • greater than:数据应大于搜索值
  • less than:数据必须小于搜索值
  • end with:数据必须以搜索值结尾
  • start with:数据必须以搜索值开头

每列同时只能包含一种类型的过滤器。

要做到这一点,我们应该

  1. 修改 data-table.component.html 来添加下拉过滤器

    此过滤器是一个 Angular mat-menu,其中包含可用过滤操作的列表、一个输入文本过滤器以及用于清除列过滤器或添加新过滤器的按钮。

    <div class="mat-elevation-z8">
      <table mat-table [dataSource]="dataSource" matSort class="mat-elevation-z8">
        <ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
          <th mat-header-cell *matHeaderCellDef>
            <div style="display: flex; align-items: center;">
              <span mat-sort-header>{{column}}</span>
              <button mat-icon-button>
                <mat-icon mat-icon-button color="primary" [matMenuTriggerFor]="menu"
                  [matMenuTriggerData]="{columnName: column}">filter_list </mat-icon>
              </button>
            </div>
          </th>
          <td mat-cell *matCellDef="let element"> {{element[column]}} </td>
        </ng-container>
    
        <ng-container [matColumnDef]="'actions'">
          <th mat-header-cell *matHeaderCellDef> actions </th>
          <td mat-cell *matCellDef="let element">
            <button mat-icon-button (click)="edit(element)">
              <mat-icon mat-icon-button color='primary'>edit</mat-icon>
            </button>
    
            <button mat-icon-button (click)="delete(element['id'])">
              <mat-icon mat-icon-button color="warn">delete</mat-icon>
            </button>
          </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
        <tr mat-row *matRowDef="let row; columns: columnsToDisplay;"></tr>
      </table>
      <mat-paginator [pageSize]="5" 
      [pageSizeOptions]="[5, 10, 50]" showFirstLastButtons></mat-paginator>
    </div>
    
    <!-- menu for column filtering-->
    
    <mat-menu #menu="matMenu" class="matMenu">
      <ng-template matMenuContent let-dataColumnName="columnName">
        <div class="flex-column" (click)="$event.stopPropagation();">
          <div class="mb-5">
            <mat-form-field class="fullWidth" appearance="outline">
              <mat-label>Choose a filter *</mat-label>
              <mat-select #selectedOperationFilter [value]="'contains'">
                <mat-option value="contains" select>Contains</mat-option>
                <mat-option value="equals">Equals</mat-option>
                <mat-option value="greaterThan">Greater than</mat-option>
                <mat-option value="lessThan">Less than</mat-option>
                <mat-option value="endWith">End with</mat-option>
                <mat-option value="startWith">Start With</mat-option>
              </mat-select>
            </mat-form-field>
    
          </div>
    
          <div class="mb-5 fullWidth">
            <mat-form-field class="fullWidth" appearance="outline">
              <mat-label>write a value*</mat-label>
              <input matInput #searchValue type="text">
            </mat-form-field>
          </div>
    
          <div class="fullWidth flex-row mb-5 flex-justify-space-between">
            <button [disabled]="!searchValue.value" mat-raised-button color="primary"
              class="flex-row flex-align-center btn-filter-action"
              (click)="applyFilter(dataColumnName, 
                       selectedOperationFilter.value,  searchValue.value)">
    
              <mat-icon>check</mat-icon>
              <label>filter</label>
            </button>
    
            <button mat-raised-button 
             class="flex-row flex-align-center btn-filter-action" color="warn"
              (click)="clearFilter(dataColumnName)">
              <mat-icon>clear</mat-icon>
              <label>reset</label>
            </button>
          </div>
        </div>
      </ng-template>
    </mat-menu>
  2. data-table.component.ts 中实现与过滤器相关的操作
      private filter() {
    
        this.dataSource.filterPredicate = (data: Person, filter: string) => {
    
          let find = true;
    
          for (var columnName in this.columnsFilters) {
    
            let currentData = "" + data[columnName];
    
            //if there is no filter, jump to next loop, otherwise do the filter.
            if (!this.columnsFilters[columnName]) {
              return;
            }
    
            let searchValue = this.columnsFilters[columnName]["contains"];
    
            if (!!searchValue && currentData.indexOf("" + searchValue) < 0) {
    
              find = false;
              //exit loop
              return;
            }
    
            searchValue = this.columnsFilters[columnName]["equals"];
    
            if (!!searchValue && currentData != searchValue) {
              find = false;
              //exit loop
              return;
            }
    
            searchValue = this.columnsFilters[columnName]["greaterThan"];
    
            if (!!searchValue && currentData <= searchValue) {
              find = false;
              //exit loop
              return;
            }
    
            searchValue = this.columnsFilters[columnName]["lessThan"];
    
            if (!!searchValue && currentData >= searchValue) {
              find = false;
              //exit loop
              return;
            }
    
            searchValue = this.columnsFilters[columnName]["startWith"];
    
            if (!!searchValue && !currentData.startsWith("" + searchValue)) {
              find = false;
              //exit loop
              return;
            }
    
            searchValue = this.columnsFilters[columnName]["endWith"];
    
            if (!!searchValue && !currentData.endsWith("" + searchValue)) {
              find = false;
              //exit loop
              return;
            }
          }
          return find;
    
        };
    
        this.dataSource.filter = null;
        this.dataSource.filter = 'activate';
    
        if (this.dataSource.paginator) {
          this.dataSource.paginator.firstPage();
        }
      }
    
      /**
    
       * Create a filter for the column name and operate the filter action.
    
       */
    
      applyFilter(columnName: string, operationType: string, searchValue: string) {
    
        this.columnsFilters[columnName] = {};
        this.columnsFilters[columnName][operationType] = searchValue;
        this.filter();
      }
    
      /**
    
       * clear all associated filters for column name.
    
       */
    
      clearFilter(columnName: string) {
        if (this.columnsFilters[columnName]) {
          delete this.columnsFilters[columnName];
          this.filter();
        }
      }  
  3. 最终结果应该如下所示

运行应用程序

尝试下载源代码,并执行以下步骤

  • 解压源代码,然后使用 CMD 命令导航到文件夹路径。
  • 通过运行 npm install 下载 npm 包。
  • 通过运行 npm start 运行应用程序。

参考文献

关注点

希望您喜欢这篇文章。感谢您浏览我的帖子,请尝试下载源代码,并随时留下您的问题和评论。

历史

  • v1 2020 年 12 月 19 日:初始版本
© . All rights reserved.