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

agGrid for Angular (缺失手册)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020 年 5 月 1 日

CPOL

20分钟阅读

viewsIcon

35286

downloadIcon

380

开始使用 agGrid for Angular

引言

我最近在学习如何在 Angular 9 应用程序中使用 agGrid,尽管它非常流行,但我发现过程非常艰难。

agGrid 网站看起来很棒,它有企业版,演示效果也非常酷……但一旦你开始使用它,你就会发现自己需要大量地搜索 Google、冥思苦想,甚至借酒消愁。agGrid 网站不允许像我这样的开发者发表评论或寻求帮助,这也就不足为奇了……

本文将指导你完成创建新的 Angular 应用、添加 agGrid,然后介绍你将面临的一些问题。

我们的最终结果将是这个网格,其中包含自定义的日期、复选框和引用数据下拉列表的渲染器。

要学习本教程,我希望你具备以下知识:

  • Angular
  • TypeScript
  • HTML
  • 已安装 Visual Studio Code

让我们开始吧!

房间里的 agElephant

如果你足够感兴趣阅读本文,那么你很可能知道 agGrid 已经 提供了一个网页,展示了如何与 Angular 配合设置 agGrid。你可以在此链接找到它。

啊,算了。

我现在就停笔去酒吧吗?很遗憾,不行。

尽管 agGrid 的网站看起来光鲜亮丽,但它故意避开了许多你在学习 agGrid 时会遇到的问题。

在本教程中,我将从 Web 服务加载一些“真实世界”的数据,这将巧妙地展示这些问题,并告诉你如何解决它们。这是一个示例记录

{
   "id": 3000,
   "jobRol_ID": 1001,
   "firstName": "Michael",
   "lastName": "Gledhill",
   "imageURL": 
   "https://process.filestackapi.com/cache=expiry:max/resize=width:200/FYYq9KL6TnqtOT6TuQ3g",
   "dob": "1980-12-25T00:00:00",
   "bIsContractor": false,
   "managerID": null,
   "phoneNumber": "044 123 4567",
   "bWheelchairAccess": false,
   "startDate": "2020-02-17T00:00:00",
   "updateTime": "2019-10-18T00:00:00",
   "updatedBy": "mike"
},

看起来很标准,对吧?但是,开箱即用,你将立即遇到 agGrid 的问题

  • agGrid 没有提供一种以友好的“dd/MMM/yyyy”格式显示日期的方法。你必须自己编写日期格式化程序,以及一个让用户选择新日期的控件。(说真的?!
  • 在编辑一行数据时,agGrid 将每个值都视为 string。因此,你不会看到布尔值的复选框控件,而是会看到一个带有“true”或“false”字符串的文本框。
  • 在我的记录(上文)中,jobRol_ID 实际上是一个外键值,链接到如下的引用数据
    {
        "id": 1000,
        "name": "Accountant"
    },
    {
        "id": 1001,
        "name": "Manager"
    },

    对于这个单元格,我希望网格显示此 ID 的引用数据文本值。当我编辑它时,我希望出现一个引用数据字符串的弹出窗口。当用户选择一个 string 时,我希望我的记录用它的ID值更新。

那么 agGrid 的文档是如何解决这些问题的呢?它避开了它们。在它们的演示中,日期值(总是?)已经预先格式化为“dd/mm/yyyy”字符串,它们避免提及复选框(除了用它们来选择整行),对于下拉列表,它们只使用字符串……而不是引用数据“id”。

我使用 agGrid & Angular 的经历非常痛苦和令人沮丧,这是我在刚开始时希望拥有的文章。

所有源代码都包含在附加的.zip文件中,但我强烈建议你创建自己的 Angular 项目,并在阅读文章时复制代码块。

让我们开始吧!

让我们先来看看我们要创建什么。

我在 Azure 上设置了一个基本的 WebAPI,其中包含几个端点。在本教程中,我们只使用这两个GET端点

你可以在这里查看 Swagger 页面

为了保持简单,在这个示例 Web 应用程序中,我们要做的是显示我们的员工列表,并允许你编辑它们。使用现代的、最新的 Angular 网格库,这应该很简单,不是吗?

这是数据库架构。如我之前所说,这足以展示你在编写完整的企业应用程序时会遇到的问题。

你会注意到我在这里包含了我的源代码的完整文本。不,我不是在试图充实这篇文章。我强烈建议你复制粘贴这里的内容,而不是下载完整的源代码(它包含在文章的开头)。agGrid 和 Angular 的变化如此之快,这是确保将来一切都能正常工作的最安全的方式。

我还建议你一步一步地完成本教程,并检查它是否正常工作。Angular 经常更新,并微妙地破坏现有代码,这是一个令人讨厌的习惯。

1. 创建 Angular 应用程序

这并非旨在成为一个 Angular 教程,所以我会快速跳过这一部分。坐稳了!

在你喜欢的命令提示符中,创建一个新的 Angular 应用,并在 Visual Studio Code 中打开它。

ng new Employees --routing=true --style=scss  
cd Employees
code .

现在,在 Visual Studio Code 中,点击“Terminal”>“New Terminal”(如果还没有打开终端窗口),然后让我们安装 agGrid、Bootstrap 和 rxjs

npm install --save ngx-bootstrap bootstrap rxjs-compat
npm install --save ag-grid-community ag-grid-angular 
npm install --save ag-grid-enterprise

好的。我们的框架已安装,让我们来写一些代码……

2. 添加“模型”

在 Visual Studio Code 中,进入 src\app 文件夹,然后创建一个新文件夹 models。在这里,我们将创建两个文件,用于包含表示我们两个数据库表的类。首先,employee.ts

export class Employee {
    id: number;
    dept_id: number;
    jobRol_ID: number;
    firstName: string;
    lastName: string;
    imageURL: string;
    dob: Date;
    bIsContractor: boolean;
    managerID: number;
    phoneNumber: string;
    bWheelchairAccess: boolean;
    startDate: Date;
    xpos: number;
    ypos: number;
    updateTime: Date;
    updatedBy: string;
}

接下来,创建一个文件 jobRole.ts:

export class JobRole {
    id: number;
    name: string;
    imageURL: string;
}

正如我所说,你可以点击以下两个 URL,查看我们将从 Web 服务下载的 JSON,它们与这些类中定义的字段相对应。

3. 添加“服务”

在编写 Angular 代码时,总是忍不住直接在 Component 中加载数据。但将此代码保存在单独的服务中,然后注入到需要它的组件中,这样更有意义。

所以,在你的 src\app 文件夹中,让我们创建一个名为 services 的新文件夹。在这个文件夹中,添加一个名为 app.services.ts 的新文件

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Employee } from '../models/employee';
import { JobRole } from '../models/jobRole';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class EmployeesService {
    readonly rootURL = 'https://mikesbank20200427060622.azurewebsites.net';

    constructor(private http: HttpClient) {
    }

    loadEmployees(): Observable<Employee[]> {
        var URL = this.rootURL + '/api/Employees';
        return this.http.get<Employee[]>(URL)
            .catch(this.defaultErrorHandler());
    }

    loadJobRoles(): Observable<JobRole[]> {
        var URL = this.rootURL + '/api/JobRoles';
        return this.http.get<JobRole[]>(URL)
            .catch(this.defaultErrorHandler());
    } 

    private defaultErrorHandler() {
        return (error:any) => Observable.throw(error.json().error || 'Server error');
    }
}

4. 包含我们的依赖项

接下来,让我们切换到 app 文件夹中的 app.module.ts 文件。请注意,我们在这里告诉它我们正在使用 agGrid、Http 以及我们的 EmployeesService

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { EmployeesService } from './services/app.services';
import { AgGridModule } from 'ag-grid-angular';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    AgGridModule.withComponents([])
  ],
  providers: [EmployeesService],
  bootstrap: [AppComponent]
})
export class AppModule { }

5. 添加一些样式

只需将 styles.scss 文件中的内容替换为这个

@import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
@import "../node_modules/ag-grid-community/src/styles/ag-grid.scss";
@import 
"../node_modules/ag-grid-community/src/styles/ag-theme-alpine/sass/ag-theme-alpine-mixin.scss";

.ag-theme-alpine {
    @include ag-theme-alpine();
}

body {
    background-color:#ccc;
}
h3 {
    margin: 16px 0px;
}

6. 一点点 HTML……

删除 app.component.html 文件中的所有 HTML,并用这个替换它

<div class="row">
  <div class="col-md-10 offset-md-1">
    <h3>
      Employees
    </h3>
    <ag-grid-angular 
      style="height:450px"
      class="ag-theme-alpine" 
      [columnDefs]="columnDefs"
      [rowData]='rowData'
      [defaultColDef]='defaultColDef'
    >
    </ag-grid-angular>
  </div>
</div>

7. 以及 HTML 的 TypeScript

现在,我们需要将我们的 app.component.ts 文件更改为如下所示

import { Component } from '@angular/core';
import { EmployeesService } from './services/app.services';
import { JobRole } from './models/jobRole';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'Employees';
  rowData: any;
  columnDefs: any;
  defaultColDef: any;

  constructor(private service: EmployeesService) {

    this.service.loadJobRoles().subscribe(
      data => {
          this.createColumnDefs(data);
      });

      this.service.loadEmployees().subscribe(data => {
        this.rowData = data;
      });
  }

  createColumnDefs(jobRoles: JobRole[]) {
    this.columnDefs = [
      { headerName:"ID", field:"id", width:80 },
      { headerName:"First name", field:"firstName", width:120 },
      { headerName:"Last name", field:"lastName", width:120 },
      { headerName:"Job role", field:"jobRol_ID", width:180 },
      { headerName:"DOB", field:"dob", width:160 },
      { headerName:"Contractor?", field:"bIsContractor", width:120 },
      { headerName:"Phone", field:"phoneNumber", width:150 },
      { headerName:"Wheelchair?", field:"bWheelchairAccess", width:120 },
      { headerName:"Start date", field:"startDate", width:180 },
      { headerName:"Last update", field:"updateTime", width:180 }
    ];
    this.defaultColDef = {
      sortable: true,
      resizable: true,
      filter: true,
      editable: true
    }
  }
}

呼。现在,在你的终端窗口中,我们可以启动应用程序

ng serve 

如果一切顺利,在所有复制粘贴之后,你就可以打开浏览器,访问 https://:4200,并看到一个从我们的服务加载数据的 agGrid

刚才发生了什么?

现在,代码中发生了很多事情,你需要注意。

特别要注意的是,我们尝试定义 agGrid 的列定义之前加载了我们的 JobRoles 数据。如果我们尝试在数据准备好之前就绘制 agGrid,网格会显示,但我们无法创建 JobRole 选项的下拉列表。我们稍后会讨论这一点……

我们还让我们的服务加载 Employee 记录列表,并将其存储在 rowData 变量中,这就是我们要求 agGrid 显示的数据。

最后,我们还为网格定义了一些默认值,例如允许任何字段进行编辑、过滤和排序。所以,现在,你可以看到 agGrid 在实际运行中了——你可以拖动列重新排序,点击标题进行排序,并添加过滤。这非常酷。

以友好的格式显示日期

我很惊讶,在满怀骄傲地走到这一步后,我突然发现 agGrid 没有提供一种简单的方法来以友好的格式显示日期。

更令我惊讶的是(在撰写本文时),我找不到任何人发布过关于如何在 Angular 中以可重用方式实现此功能的文章。显示日期是使用任何类型的网格的基本要求,这本应该包含在“入门”指南的某个地方。

实现此功能的最简洁方法是

  • 创建自己的 CellRenderer 来以“dd/MMM/yyyy”等格式显示日期
  • 创建自己的 CellEditor,在编辑值时弹出日历

系好安全带……我告诉过你这不会很顺利。

首先,让我们在我们的应用中创建一个“cellRenderers”文件夹,并在其中创建一个名为 DateTimeRenderer.ts 的文件

import { Component, LOCALE_ID, Inject } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { formatDate } from '@angular/common';

@Component({
    selector: 'datetime-cell',
    template: `<span>{{ formatTheDate() }}</span>`
})
export class DateTimeRenderer implements ICellRendererAngularComp {

    params: ICellRendererParams; 
    selectedDate: Date;

    constructor(@Inject(LOCALE_ID) public locale: string) { }

    agInit(params: ICellRendererParams): void {
        this.params = params;
        this.selectedDate = params.value;
    }

    formatTheDate() {
        //  Convert our selected Date into a readable format
        if (this.selectedDate == null)
            return "";

        return formatDate(this.selectedDate, 'd MMM yyyy', this.locale);
    }

    public onChange(event) {
        this.params.data[this.params.colDef.field] = event.currentTarget.checked;
    }

    refresh(params: ICellRendererParams): boolean {
        this.selectedDate = params.value;
        return true;
    }
}

接下来,转到 app.module.ts,并将其添加到声明中

import { DateTimeRenderer } from './cellRenderers/DateTimeRenderer';

然后在 declarationsimports 部分告诉我们的 @NgModule 关于它

@NgModule({
  declarations: [
    AppComponent,
    DateTimeRenderer
  ],
  imports: [
    BrowserModule,
    AgGridModule.withComponents([DateTimeRenderer])
  ],

有了这个,我们就可以回到 app.component.ts 文件。首先,让我们包含这个新组件

import { DateTimeRenderer } from './cellRenderers/DateTimeRenderer';

我们现在可以将此渲染器添加到我们的三个日期字段中

    this.columnDefs = [
      . . . 
      { headerName:"DOB", field:"dob", width:160, cellRenderer: 'dateTimeRenderer' },
      . . .
      { headerName:"Start date", field:"startDate", 
        width:180, cellRenderer: 'dateTimeRenderer' },
      { headerName:"Last update", field:"updateTime", 
        width:180, cellRenderer: 'dateTimeRenderer' }
    ];

还有两个更改。我们还需要告诉 agGrid 我们正在使用自定义单元格渲染器。为此,我们需要添加一个新的变量

 export class AppComponent {
    . . .
    frameworkComponents = {
      dateTimeRenderer: DateTimeRenderer
    }
  
    constructor(private service: EmployeesService) {
    . . .   
 }

而在 app.component.html 文件中,我们需要告诉它使用这个变量

    <ag-grid-angular 
      [frameworkComponents]='frameworkComponents'
      . . .

有了这一切,我们终于将日期以可读的格式显示在我们的三个日期列中了。

添加单元格参数

这看起来很不错,但如果我们能以某种方式使格式更通用,那就更好了。也许,我们的美国用户希望看到日期显示为“mm/dd/yyyy”。

为此,我们可以向我们的 columnDef 记录添加一个 CellRendererParams

this.columnDefs = [
    . . .
    { headerName:"DOB", field:"dob", width:160, cellRenderer: 'dateTimeRenderer' ,
         cellRendererParams: 'dd MMM yyyy' },
    { headerName:"Start date", field:"startDate", width:180, cellRenderer: 'dateTimeRenderer',
         cellRendererParams: 'MMM dd, yyyy  HH:mm' },
    { headerName:"Last update", field:"updateTime", width:140, cellRenderer: 'dateTimeRenderer',
         cellRendererParams: 'dd/MM/yyyy' }

现在,我们只需要让我们的 CellRenderer 使用这些参数,如果它们存在的话。回到 DateTimeRenderer.ts 文件,我们将添加一个带有默认值的 dateFormat 字符串,并在初始化时,我们将检查是否指定了一个要使用的参数

export class DateTimeRenderer implements ICellRendererAngularComp {

    params: ICellRendererParams; 
    selectedDate: Date;
    dateFormat = 'd MMM yyyy';

    agInit(params: ICellRendererParams): void {
        this.params = params;
        this.selectedDate = params.value;

        if (typeof params.colDef.cellRendererParams != 'undefined') {
            this.dateFormat = params.colDef.cellRendererParams;
        }
    }

现在,我们只需要在 formatTheDate 函数中使用它

formatTheDate() {
    //  Convert a date like "2020-01-16T13:50:06.26" into a readable format
    if (this.selectedDate == null)
        return "";

    return formatDate(this.selectedDate, this.dateFormat, this.locale);
}

瞧,只需付出很小的努力,我们就创建了一个可重用的日期时间渲染器,我们的开发人员可以轻松实现它,并选择他们自己的日期格式

这不是很酷吗?嗯,直到我们烦人的用户尝试编辑日期。

我们接下来将处理这个问题。

将日期绑定到 Angular Material 的 DatePicker

上面的解决方案对于显示日期来说很好,但如果我们尝试编辑其中一个日期,我们又会回到文本框。用户体验不佳。

为了改善我们的用户体验,让我们将 Angular Material 添加到我们的项目中,并展示如何让Material 的 DatePicker 控件在我们编辑日期时出现。

首先,我们需要将 Angular Material 添加到我们的项目中

npm install --save @angular/material @angular/cdk @angular/animations hammerjs

接下来,在 styles.scss 文件中,添加一些 imports

@import "../node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css";
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';

……我们还需要添加一些额外的样式……

.mat-calendar-body-active div {
    border: 2px solid #444 !important;
    border-radius: 50% !important;
}

.mat-calendar-header {
    padding: 0px 8px 0px 8px !important;
}

接下来,我们需要告诉我们的 app.module.ts 文件,我们将使用 DatePicker

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from "@angular/material/core";
import { MatInputModule } from '@angular/material/input';

我们需要将 Angular Material 库放入它们自己的模块中……

@NgModule({
  imports: [
    MatDatepickerModule,
    MatNativeDateModule,
    MatInputModule
  ],
  exports: [
    MatDatepickerModule,
    MatNativeDateModule,
    MatInputModule
  ]
})
export class MaterialModule { }

然后将这个新的 MaterialModule 导入到我们应用的模块中

  imports: [
    BrowserAnimationsModule,      
    MaterialModule,
    . . .

我总是对向我的应用程序添加新库感到有点紧张,所以此时,我建议你打开 app.component.html 文件,并在我们的 agGrid之后添加几行 HTML,只是为了测试 DatePicker 是否正常工作。

<input matInput [matDatepicker]="picker">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>

假设这对你来说工作正常,让我们将 DatePicker<mat-calendar> 控件添加到新的 CellRenderer 中。在 cellRenderers 文件夹中,创建一个名为 DatePickerRenderer.ts 的新文件:

import { Component, LOCALE_ID, Inject, ViewChild } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { formatDate } from '@angular/common';
import { MatDatepickerModule, MatDatepicker } from '@angular/material/datepicker';

@Component({
    selector: 'datetime-cell',
    template: `<mat-calendar [startAt]="thisDate" (selectedChange)="onSelectDate($event)">
               </mat-calendar>`
})
export class DatePickerRenderer implements ICellRendererAngularComp {
    params: ICellRendererParams;
    thisDate: Date;

    agInit(params: ICellRendererParams): void {
        this.params = params;

        var originalDateTime = this.params.data[this.params.colDef.field];
        if (originalDateTime == null)
            this.thisDate = new Date();     //  Default value is today
        else
            this.thisDate = new Date(originalDateTime);
    }

    getValue() {
        //  This gets called by agGrid when it closes the DatePicker control.
        //  agGrid uses it to get the final selected value.
        var result = new Date(this.thisDate).toISOString();
        return result;
    }

    isPopup() {
        //  We MUST tell agGrid that this is a popup control, to make it display properly.
        return true;
    }

    public onSelectDate(newValue) {
        // When we select a date, we'll store this, 
        // then get agGrid to close the Calendar control.
        this.thisDate = newValue;
        this.params.api.stopEditing();
    }

    refresh(params: ICellRendererParams): boolean {
        return true;
    }
}

这一切都很简单。我们创建一个 <mat-calendar> 对象,其初始日期为 thisDate。当我们选择一个不同的日期时,我们会更新 thisDate 值并让 agGrid 关闭我们的弹出窗口。

时区

值得一提的是,如果你选择使用不同的 DatePicker 库,请检查所选值是否不包含 timezone 部分。以 primeNg 为例,我发现我会点击1980 年 12 月 25 日,但它实际上返回的值是

1980-25-12T01:00:00

啊。在这种情况下,我需要添加一些特殊代码来获取我选择的值,获取我机器的timezone,然后将选定的日期按此timezone偏移。

createDateString(dateStr) {
    //  You will ONLY need this, if you DatePicker returns a date, with a timezone section.
    //
    //  This converts a date in this format:
    //      Tue Mar 12 1985 00:00:00 GMT+0100 (Central European Standard Time)
    //  into a string in this format:
    //      "1985-03-12T01:00:00"
    // 
    var tzoffset = (new Date()).getTimezoneOffset() * 60000;
    var currentDate = new Date(dateStr);
    var withTimezone = new Date(currentDate.getTime() - tzoffset);
    var localISOTime = withTimezone.toISOString().slice(0, 19).replace("Z", "");
    return localISOTime;
}

不过,在这个例子中,我们不需要这个函数,因为 <mat-calendar> 确实返回了我们在选定Date的午夜的Date对象。

回到我们的代码。

现在我们需要告诉我们的应用程序关于我们新的 DatePickerRenderer 组件。进入 app.module.ts 文件,并导入它

import { DatePickerRenderer } from './cellRenderers/DatePickerRenderer';

然后将其添加到 declarations

  declarations: [
    DatePickerRenderer, 
    . . .
  ],

并添加到我们的 imports

  imports: [
    BrowserModule,      
    . . .
    AgGridModule.withComponents([DateTimeRenderer, DatePickerRenderer])
  ],

现在,它已经可以用于我们的组件了。

所以让我们进入 app.component.ts,并在那里导入它

import { DatePickerRenderer } from './cellRenderers/DatePickerRenderer';

……将其添加到我们的 frameworkComponents 列表中……

  frameworkComponents = {
    dateTimeRenderer: DateTimeRenderer,
    datePickerRenderer: DatePickerRenderer
  }

……现在,我们终于可以将 cellEditor 属性添加到我们三个日期列中的每一个了……

  { headerName:"DOB", field:"dob", width:160, 
       cellRenderer: 'dateTimeRenderer', cellRendererParams: 'dd/MMM/yyyy  HH:mm',
       cellEditor: 'datePickerRenderer' },

呼。只是为了给网格控件添加一个基本的日期选择器,就需要做这么多工作。

当然,一旦你在应用程序的一个地方完成了它,你就只需要在你想在其他网格中重用这个组件时重复最后三个步骤。

在继续之前,请检查所有这些是否正常工作。双击一个日期,确保日历出现,选择一个日期,然后检查所选日期是否现在显示在你的网格中。

将布尔字段绑定到复选框

我们的下一个问题是 agGrid 将布尔值显示为“true”或“false”字符串。当你编辑它们时,它只显示一个文本框

是的,那真的很糟糕。

可惜,要将其变成复选框,我们必须编写另一个单元格渲染器。

在我们的项目中,让我们创建一个新文件夹 CellRenderers,并在其中添加一个名为 CheckboxRenderer.ts 的新文件

import { Component } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';

@Component({
    selector: 'checkbox-cell',
    template: `<input type="checkbox" [checked]="params.value" (change)="onChange($event)">`
})
export class CheckboxRenderer implements ICellRendererAngularComp {

    public params: ICellRendererParams; 

    constructor() { }

    agInit(params: ICellRendererParams): void {
        this.params = params;
    }

    public onChange(event) {
        this.params.data[this.params.colDef.field] = event.currentTarget.checked;
    }

    refresh(params: ICellRendererParams): boolean {
        return true;
    }
}

和以前一样,因为我们创建了一个新的 CellRenderer,我们需要

  • app.module.ts 文件中,告诉我们的 NgModule 关于它
  • 告知任何使用此 CellRenderer 的组件

所以,在 app.module.ts 中,我们需要添加一个“include”……

import { CheckboxRenderer } from './cellRenderers/CheckboxRenderer';

……并将其添加到我们的 declarationsimports 中……

@NgModule({
  declarations: [
     CheckboxRenderer,
     . . .
  ],
  imports: [
     . . .
     AgGridModule.withComponents([DateTimeRenderer, DatePickerRenderer, CheckboxRenderer])
  ],

现在,我们需要让我们的组件知道它。
让我们进入 app.component.ts 文件,并在那里包含它……

import { CheckboxRenderer } from './cellRenderers/CheckboxRenderer';

然后将其添加到我们的 frameworkComponents 部分……

  frameworkComponents = {
      dateTimeRenderer: DateTimeRenderer,
      datePickerRenderer: DatePickerRenderer,
      checkboxRenderer: CheckboxRenderer
    }

有了所有这些,我们就可以将此渲染器添加到我们的两个布尔字段中

    this.columnDefs = [
      . . .
      { headerName:"Contractor?", field:"bIsContractor", 
        width:120, cellRenderer: 'checkboxRenderer' },
      . . .
      { headerName:"Wheelchair?", field:"bWheelchairAccess", 
        width:120, cellRenderer: 'checkboxRenderer' },
      . . .
    ];

再次,有了所有组件,我们终于有了与数据绑定的复选框。

实际上,你可能会注意到的一件事是,如果你双击 checkbox,它会被替换为一个 textbox,里面有“true”或“false”。你可以通过将

this.columnDefs = [ 
    . . . 
    { headerName:"Contractor?", field:"bIsContractor", width:120, 
        cellRenderer: 'checkboxRenderer', editable: false }, 
    . . . 
    { headerName:"Wheelchair?", field:"bWheelchairAccess", width:120, 
        cellRenderer: 'checkboxRenderer', editable: false }, 
    . . . 
];

是的,有点傻。但你仍然可以勾选/取消勾选 checkbox,但这可以防止那个讨厌的文本框出现。

外键

agGrid 作者小心翼翼地避免文档化的另一个明显问题是如何将外键绑定到网格中 { id, name } 引用数据的下拉列表。

现在,我从 Web 服务收到的 Employee 记录实际上不包含每个用户的 Job Role 字符串。我怀疑你的 REST 服务数据也不会。我的 Employee 记录包含一个 jobRol_ID 值,它引用了一个特定的 JobRole 记录。

所以,显然,我们希望让 agGrid 显示“Manager字符串,而不是“1001”这个值。如果用户编辑此值,我们希望看到 JobRole “name”值的下拉列表,但当他们做出选择时,显然,jobRol_ID 值应该用新的id值更新,而不是 JobRole 名称字符串。

现在,在开发过程中,我将用两个列定义替换我原来的“Job Role”列定义,这样我就可以看到原始的 JobRol_ID 值,以及旁边的下拉列表。

{ headerName:"Job role ID", field:"jobRol_ID", width:130 },
{ 
  headerName:"Job role", field:"jobRol_ID", width:180, 
  cellEditor: 'agSelectCellEditor', 
  cellEditorParams: {
    cellHeight:30,
    values: jobRoles.map(s => s.name)
  },
  valueGetter: (params) => jobRoles.find(refData => refData.id == params.data.jobRol_ID)?.name,
  valueSetter: (params) => {
    params.data.jobRol_ID = jobRoles.find(refData => refData.name == params.newValue)?.id
  }
},

(我是认真的,你根本不知道我花了多少小时来创建这么一小段代码……我到处都找不到这样的例子。)

但它奏效了!当我编辑 Job Role 列中的某个项时,它会正确地显示(文本)选项列表,当我选择一个选项时,我可以在 Job Role ID 列中看到它已将我的记录更新为所选内容的id

如果你想要一个看起来更好的下拉列表,你可以安装 agGrid 的企业版,使用

npm install --save ag-grid-enterprise

然后你只需要在 app.module.ts 中包含它

import 'ag-grid-enterprise';

然后你只需要将 cellEditor 改为使用“agRichSelectCellEditor

{ headerName:"Job role", field:"jobRol_ID", width:180, 
    cellEditor: 'agRichSelectCellEditor',

这样,你就会有一个看起来更好的下拉列表

当你对它 all 正常工作感到满意时,别忘了删除那个“Job role ID”列。

下拉列表 - 备选方案 B

我从来没有特别满意这个下拉列表的实现。我真的想每次我的行中都有一个引用数据项时,重复以下几行代码吗?而且,正如你所看到的,唯一真正改变的是我正在绑定的字段,在这种情况下是 jobRol_ID,以及包含我的引用数据记录的数组的名称,jobRoles

{ 
    headerName:"Job role", field:"jobRol_ID", width:180, 
    cellEditor: 'agRichSelectCellEditor', 
    cellEditorParams: {
      cellHeight:30,
      values: jobRoles.map(s => s.name)
    },
    valueGetter: (params) => jobRoles.find
    (refData => refData.id == params.data.jobRol_ID)?.name,
    valueSetter: (params) => {
      params.data.jobRol_ID = jobRoles.find(refData => refData.name == params.newValue)?.id
    }
},

理想的解决方案是获取 agRichSelectCellEditor 代码并对其进行修改,使其仅接受一个数组名称,并由它处理一切……但不行,他们不允许我们这样做。

//  This would've been the ideal solution... but, we can't do this...
{ 
    headerName:"Job role", field:"jobRol_ID", width:180, 
    cellEditor: 'agRichSelectCellEditor', referenceDataArray: "jobRoles" 
}

此外,从 UI 的角度来看,企业版的下拉列表 agRichSelectCellEditor 有点奇怪。

  1. 它总是在弹出列表的顶部显示选定的项目,即使我们可以看到它就在我们刚刚单击的单元格正下方,而该单元格已经显示了该值。
    而且它的字体/样式与其他所有项目相同……很容易将其误认为是你可以单击的选项之一。它看起来很奇怪。在上例中,我们真的在弹出窗口中看到“Team Leader两次吗?
  2. 当它第一次出现时,弹出窗口在选定的项目上显示一个浅蓝色背景……但一旦你悬停在另一个项目上,浅蓝色就会消失,你“悬停”的项目现在会变成浅蓝色。等等……“浅蓝色”是表示我选择的选项,还是我“悬停”的选项?

在我的下拉列表控件中,我将修复这些问题

  • 我不会在弹出窗口顶部显示选定的项目。
  • 我将用浅蓝色突出显示当前选择,它将保持该颜色。我将使用不同的颜色来显示你悬停的项。相信我,当你使用它时,它感觉更自然。

下面是我的自定义下拉列表与 agRichSelectCellEditor 列表的(组合)图像。

 

要添加自定义下拉列表

Visual Studio Code 的终端窗口中,使用以下命令定义一个新组件,并将其注册到我们的 NgModule

ng g component cellRenderers\DropDownListRenderer --module=app.module.ts --skipTests=true 

这会在我们的 cellRenderers 文件夹中创建一个名为“DropDownListRenderer”的新文件夹,其中包含一个 TypeScript 文件、HTML 和 CSS 文件。它还会将组件注册到我们的 NgModule——但是,你仍然需要进入 app.module.tsDropDownListRendererComponent 添加到此行的末尾

  AgGridModule.withComponents([DateTimeRenderer, DatePickerRenderer, 
     CheckboxRenderer, DropDownListRendererComponent])

我们的下拉列表的 HTML 非常简单。在 cellRenderers/DropDownListRenderer 文件夹中,你需要用这个替换 .html 文件中的内容

<div class="dropDownList">
    <div class="dropDownListItem" *ngFor="let item of items" (click)="selectItem(item.id)"
        [ngClass]="{'dropDownListSelectedItem': item.id == selectedItemID}" >
        {{ item.name }}
    </div>
</div>

我们在 drop-down-list-renderer.component.scss 文件中需要一小段 CSS

.dropDownList {
    max-height:300px;
    min-width:220px;
    min-height:200px;
    overflow-y: auto;
}
.dropDownListItem {
    padding: 8px 10px;
}
.dropDownListItem:hover {
    cursor:pointer;
    background-color: rgba(33, 150, 243, 0.9);
}
.dropDownListSelectedItem {
    /* Whichever is our selected item, highlight it, and KEEP IT highlighted ! */
    background-color: rgba(33, 150, 243, 0.3) !important;
}

drop-down-list-renderer.component.ts 文件则相当简单。注意 agInit 如何检查我们是否在 cellEditorParams 属性中传递了一个引用数据记录数组给它。我们的下拉列表将显示这个数组记录中的 name 值,并将绑定到 id 值。

import { Component, OnInit } from '@angular/core';
import { ICellRendererParams } from 'ag-grid-community';
import { ICellRendererAngularComp } from 'ag-grid-angular';

@Component({
  selector: 'app-drop-down-list-renderer',
  templateUrl: './drop-down-list-renderer.component.html',
  styleUrls: ['./drop-down-list-renderer.component.scss']
})
export class DropDownListRendererComponent implements ICellRendererAngularComp {

  params: ICellRendererParams;
  items: any;
  selectedItemID: any;

  agInit(params: ICellRendererParams): void {
      this.params = params;
      this.selectedItemID = this.params.data[this.params.colDef.field];
      
      if (typeof params.colDef.cellEditorParams != 'undefined') {
          this.items = params.colDef.cellEditorParams;
      }
  }

  public selectItem(id) {
    //  When the user selects an item in our drop down list, 
    //  we'll store their selection, and ask
    //  agGrid to stop editing (so our drop down list disappears)
    this.selectedItemID = id;
    this.params.api.stopEditing();
  }

  getValue() {
    //  This gets called by agGrid when it closes the DatePicker control.
    //  agGrid uses it to get the final selected value.
    return this.selectedItemID;
  } 

  isPopup() {
    //  We MUST tell agGrid that this is a popup control, to make it display properly.
    return true;
  }
}

要使用此渲染器,我们需要进入 app.component.ts 文件,并包含它

import { DropDownListRendererComponent } 
from './cellRenderers/drop-down-list-renderer/drop-down-list-renderer.component';

……并将其添加到我们的 frameworkComponents 列表中

  frameworkComponents = {
    dateTimeRenderer: DateTimeRenderer,
    datePickerRenderer: DatePickerRenderer,
    checkboxRenderer: CheckboxRenderer,
    dropDownListRendererComponent: DropDownListRendererComponent
  }

我们现在可以在我们的列定义中使用它了

  { 
     headerName:"Job role (custom)", field:"jobRol_ID", width:180,
     valueGetter: (params) => jobRoles.find
     (refData => refData.id == params.data.jobRol_ID)?.name,
     cellEditor: 'dropDownListRendererComponent', cellEditorParams: jobRoles
  }, 

valueGetter 行删除会非常好,但令人恼火的是,在 agGrid 中,你不能定义一个单一组件来同时负责显示单元格的“查看”和“编辑”模式。相反,你必须定义单独的 cellRenderercellEditor 组件。

当然,我们可以定义一个 cellRenderer 来为我们完成这项工作,并传递一个包含我们的 jobRoles 数组的 cellRendererParams

不过,目前就这样吧。我们有了一个更好看的下拉列表,用户体验也大大提高。

设置行高

好的,agGrid 本来就不应该取代 Excel,但它确实允许你处理大量的行,而且你通常希望行高小于默认的 40 像素,这样你就可以在屏幕上看到更多内容。

好消息是 agGrid 提供了一个简单的 rowHeight 属性。

    <ag-grid-angular [rowHeight]=20

坏消息是,你实际上不应该期望行高小于 40 像素。我的网格在行高为 20 时的样子

当然,是的,你可以(而且很可能会)去写一大堆 CSS 来让它看起来正确……

.ag-cell-not-inline-editing {
    line-height: 18px !important;
}

……这是一个很大的改进……

但即便如此,也不要尝试编辑单元格,因为 agGrid 仍然使用 40 像素高的控件来编辑你的数据。注意(上图)文本框如何重叠到两行。所以,你还需要对这些控件做更多的 CSS 覆盖。说真的,我不明白为什么他们提供了 rowHeight 设置,如果他们库的一半都忽略了它。

那么,agGrid 的文档是如何处理这些问题的呢?很简单。他们的示例都有 rowHeights 大于 40 像素,并且他们关闭了大多数单元格的编辑功能。

问题解决了。(沮丧地叹气。)

我的 agGrid 圣诞愿望清单

我使用 agGrid 的这段时间非常艰难。如果 agGrid 的作者正在阅读本文,我有一些请求

  • 在你们的网站上创建一些真正的、实际的示例。例如,我们所有开发者都需要在我们的网格中显示和编辑日期……这本可以是一个如何使用 CellRendererCellEditor 的绝佳示例,并演示为什么 CellRendererParamsCellEditorParams 可以使控件更通用。
  • 在你们的网站上,如果有一个描述例如如何使用列组的网页,请在页面底部添加一个 Disqus 部分,以便开发者可以就此提出问题、发表评论并互相提供建议。是的,我知道有 GitHub 页面,但将评论特定于 agGrid 的某个主题放在该特定页面上更有意义。
  • 让我们能够创建一个单一控件来同时查看编辑单元格的数据。向网格中添加一个 checkbox 是一个明显的例子,因为它使用的 HTML 和逻辑在查看或编辑值时不会改变。
  • rowHeight”功能……要么让它在整个网格中工作(尤其是在我们编辑该单元格的值时),要么就去掉它。

总结

我真的不想写这篇文章。我花了很多时间才在 agGrid 上走到这一步,我不想再花更多时间来记录它。

但似乎没有人写过一份关于 agGrid 与 Angular 的靠谱的入门指南。这是一篇非常长的文章,但我们所做的只是介绍了一个 JSON 记录的基本查看和编辑。

正如你所见,我的 Web 服务确实有 POST/PUT 端点,如果你想进一步尝试,并将更改自动保存回数据库。

如果你觉得这篇文章有用,请留下评论。

历史

  • 2020 年 5 月 1 日:初始版本
© . All rights reserved.