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

Angular 4 数据网格(支持排序、过滤和导出 CSV)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2017年7月10日

CPOL

10分钟阅读

viewsIcon

140066

downloadIcon

5627

本文档旨在帮助您了解在 Angular 4 中开发的简单数据网格的架构和用法。

Content

引言

在本文中,我将解释带有排序、格式化和导出 CSV 功能的简单数据网格控件。我假设您也阅读了我之前的 Angular 2 和 Angular 4 文章,如果还没有,请先阅读它们,因为我将基于上一篇文章的项目来添加数据网格控件。

排序和部分格式化逻辑取自以下文章(感谢 Cory Shaw

背景

这将是一个简单的数据网格控件,它将接收 datacolumnsbuttons 和几个布尔变量来控制数据网格的布局。由于我将继续使用之前文章中的 用户管理 项目,因此如果您是 Angular 2/4 新手,我强烈建议您先阅读它们。

与之前的文章相比,我将不再一步一步地解释开发过程,因为所有概念,例如 InputOutput 变量、Angular 4 的 If-Else 等,都已在之前的文章中解释过。

开始吧

  1. 请下载文章中 附件的项目,解压并打开 solution 文件。
  2. 重新生成 solution 以下载所有 NuGet 和客户端包以用于 Angular,运行项目并转到 用户管理 屏幕。您应该会看到以下屏幕:
  3. 此屏幕与上一篇文章 《Angular 2 到 Angular 4,集成 Angular Material UI 组件》 的屏幕几乎相同,除了 导出到 Excel 按钮和 排序 上/下图标。好消息是这是一个数据网格控件,可用于任何类型的数据,并且可以通过 input 变量轻松处理 导出添加/编辑/删除 按钮、它们的 标题搜索 控件的可见性。让我们先了解 UserComponents 并查看我们传递给数据控件的变量,然后我们再探讨 DataGrid 组件。
UserComponent
//Grid Vars start
    columns: any[] = [
        {
            display: 'First Name',
            variable: 'FirstName',
            filter: 'text',
        },
        {
            display: 'Last Name',
            variable: 'LastName',
            filter: 'text'
        },
        {
            display: 'Gender',
            variable: 'Gender',
            filter: 'text'
        },
        {
            display: 'Date of Birth',
            variable: 'DOB',
            filter: 'date'
        }
    ];
    sorting: any = {
        column: 'FirstName',
        descending: false
    };
    hdrbtns: any[] = [];
    gridbtns: any[] = [];
    initGridButton() {

        this.hdrbtns = [
            {
                title: 'Add',
                keys: [''],
                action: DBOperation.create,
                ishide: this.isREADONLY

            }];
        this.gridbtns = [
            {
                title: 'Edit',
                keys: ['Id'],
                action: DBOperation.update,
                ishide: this.isREADONLY
            },
            {
                title: 'X',
                keys: ["Id"],
                action: DBOperation.delete,
                ishide: this.isREADONLY
            }

        ];

    }
    //Grid Vars end
  1. 编辑 app->Components->user.component.ts
  2. 您可以看到我们引入了数据网格变量,这些变量不言自明。在 initGridButton 方法中,我们初始化了 Header 和 Grid 按钮,按钮对象具有 titlekey(s)actionishide 属性。如果您有多个键需要使用,请添加逗号分隔的键,例如:keys: ['Id','DOB']。我们为什么要有 initGridButton 方法而不是直接赋值,这将在以后的文章中看到。这种方法将帮助我们在从数据库读取数据后管理 只读 角色。请自行实验添加更多列、更改排序变量、添加更多 Header 或 Grid 按钮。
  3. 另一个有趣的方法是 gridaction,它订阅了 click 事件,当您单击任何 hdrbtnsgridbtns 时(请记住 Output 变量)就会调用它。它以 GridAction 对象作为参数,该对象来自 DataGrid 组件,并在 click 方法中填充。gridaction.values[0].value 包含第一个键值,如果您有多个键,您可以像 gridaction.values[1].value 这样获取值,依此类推。
      gridaction(gridaction: any): void {
    
            switch (gridaction.action) {
                case DBOperation.create:
                    this.addUser();
                    break;
                case DBOperation.update:
                    this.editUser(gridaction.values[0].value);
                    break;
                case DBOperation.delete:
                    this.deleteUser(gridaction.values[0].value);
                    break;
            }
        }
UserComponent 模板
  1. 编辑 app->Components 文件夹中的 user.component.html
  2. 您会看到代码相当简洁,只有一个 data-grid 控件,我们向其传递了在 UserComponent 类中定义的所有变量。
      <data-grid [columns]="columns"
                     [data]="users"
                     [gridbtns]="gridbtns"
                     [hdrbtns]="hdrbtns"
                     [sort]="sorting"
                     [isshowfilter]=true
                     [isExporttoCSV]=true
                     [exportFileName]="exportFileName"
                     [filter] = userfilter
                     (btnclick)="gridaction($event)">
          </data-grid>
DataGrid Component
  1. 既然我们已经了解了传递给 Data Grid 组件的变量,让我们来了解一下数据网格控件。展开 app->Shared 文件夹,您会在其中找到 datagrid 文件夹,其中包含数据网格控件的所有组件和样式表。展开此文件夹,然后双击 datagrid.component.ts 文件。
  2. 第一个 import 语句不言自明,我们已经在之前的文章中学习了 InputOutPutEventEmitter 装饰器。在第二个和第三个 import 语句中,我们添加了自定义类 DataGridUtil,该类具有 导出到 Excel 功能,以及 Format 类,该类具有数据格式函数,例如 DateTextCSV(逗号分隔)string。我们将在接下来的步骤中研究这些类。
import { Component, Input, Output, EventEmitter} from '@angular/core';
import {DataGridUtil} from './datagrid.util'
import { Format } from './format';
  1. 接下来,我们创建了一个 interface,其中包含操作字符串(例如 AddUpdateDelete 或我们传递给 gridbtnshdrbtns 对象的任何内容)。我们将 GridAction 接口作为 Output 变量发送。如果您查看 GridAction 接口,我们会有一个 action 字符串和一个(键值对)values 变量,这就是它的工作原理。您将传递 gridbtns 集合,其中包含按钮 titlekey(s) 名称(例如主键或唯一键)。当用户单击任何记录的 EditDelete 按钮时,GridAction(包含键名和值或键名和值列表,取决于您发送的键数)将被发送回父组件(User Component),您可以在其中使用它来进行更新或删除操作。这将在接下来的步骤中变得清晰。
    export interface GridAction {
        action: string,
        values: {
            key: string,
            value: string
        }[]
    }
  2. 接下来是组件 meta data,即 selector 名称、style sheet 和 html template。如果您不喜欢我的简单网格,请随意修改样式表。
    @Component({
        selector: 'data-grid',
        styleUrls: ['app/shared/datagrid/datagrid.style.css'],
        templateUrl: 'app/shared/datagrid/datagrid.component.html'
    })
  3. 在组件的元数据之后,是实际的类体。有九个 Input 变量用于加载数据、添加操作按钮、启用只读、导出到 Excel 以及显示搜索过滤器选项。
    1. columns: any[]:包含要显示的列列表,以及 titleformat
    2. data: any[]:要显示的数据。
    3. sort: any:用于在首次加载时对数据进行排序的列名和顺序(asc/desc)。
    4. gridbtns: any[]:包含网格中按钮列表(在本例中为编辑和删除),以及按钮 title、要关联的 key(s)hide/showaction 字符串。无论您在 gridbtns 对象中发送什么操作和键字符串值,它都将添加到上面给出的 GridAction 中并发送回 User Component,其中键值将用于 CRUD 或任何其他操作。
    5. hdrbtns: any[]:描述与 gridbtns 相同,只是它将显示在网格顶部,在本例中为 Add 按钮。
    6. isshowfilter: boolean:控制 Search 条的显示/隐藏。
    7. isExporttoCSV: boolean:控制 Export to Excel 按钮的显示/隐藏。
    8. exportFileName: stringExport to CSV 文件的名称,后面会附加当前日期和时间。
    9. filter: any:将用于过滤数据的 pipe 对象。我们在上一篇文章中学习了 pipe
  4. Output 装饰器中,我们定义了一个变量 btnclick,每当 gridbtnshdrbtns 中的任何按钮被单击时,它都会 emit GridAction 对象。
     @Output() btnclick: EventEmitter<GridAction> = new EventEmitter<GridAction>();
  5. 接下来是三个局部变量,用于获取 data 的克隆并处理 Search 功能,这是我们在上一篇文章中开发的相同 Search 组件。唯一的区别是我们将其作为数据网格的一部分,并将 UserFilterPipe 作为输入参数来处理其功能。
        pdata: any[];
        listFilter: string;
        searchTitle: string = "Search:";
  6. 接下来,我们实现了 ngOnChanges 事件,该事件在 data 变量加载数据时发生。此数据被分配给局部变量 pdata,因为每次搜索时,我们仍然需要原始数据。如果我们直接对 data 变量应用搜索过滤器,我们就会丢失 data 变量中的原始数据。过滤或搜索逻辑定义在 criteriaChange 方法中。
     ngOnChanges(changes: any) {
            if (JSON.stringify(changes).indexOf("data") != -1)
                this.pdata = this.data;
            this.criteriaChange(this.listFilter);
        }
  7. selectedClass 方法用于控制网格上的上/下图标,每次单击排序图标时都会调用 changeSortingchangeSorting 绑定到每个列,它接受列名,检查网格是否已按同一列排序,如果是,则切换 sort,否则将单击的列名存储在 sort 变量中,该变量被发送到 orderby pipe 以执行排序逻辑。
     selectedClass(columnName: string): any {
            return columnName == this.sort.column ? 'sort-' + this.sort.descending : false;
        }
    
        changeSorting(columnName: string): void {
            var sort = this.sort;
            if (sort.column == columnName) {
                sort.descending = !sort.descending;
            } else {
                sort.column = columnName;
                sort.descending = false;
            }
        }
    
        convertSorting(): string {
            return this.sort.descending ? '-' + this.sort.column : this.sort.column;
        }
  8. 接下来是 click 方法,当 hdrbtnsgridbtns 列表中的任何按钮被单击时,都会调用该方法。从 HTML templates 传递的参数是被单击的 btn 对象和当前 row。在函数体内,我们创建了 GridAction 类型变量,从 btn 对象中获取所有 keys,从 row 对象中搜索 key(s) value,并将它(们)推送到 GridAction 的 values(键值对)变量中。您可以为每个按钮关联任意数量的键。我将在接下来的步骤中展示如何发送 keys 名称。
     click(btn: any, row: any): void {
            let keyds = <GridAction>{};
            keyds.action = btn.action;
    
            if (row != null) {
                keyds.values = [];
                btn.keys.forEach((key: any) => {
                    keyds.values.push({ key: key, value: row[key] });
                });
            }
            this.btnclick.emit(keyds);
        }
  9. 如果您阅读了我之前的文章,您可能会熟悉 criteriaChange 方法,它用于在网格内搜索。我们通过将原始 data 和输入 search 字符串传递给输入过滤器变量来显式调用其 transform 方法。这就是为什么我创建了局部变量 pdata 来保存原始 data 变量,希望现在您能理解。
      criteriaChange(value: any) {
            if (this.filter != null) {
                if (value != '[object Event]') {
                    this.listFilter = value;
                    this.pdata = this.filter.transform(this.data, this.listFilter);
                }
            }
        }
  10. 下一个方法是 exporttoCSV,它接受 data 变量,根据输入的 column 变量过滤特定列的值,调用 Format 类的 transform 方法将数据格式化(例如文本、日期、csv 等),数据导出对象加载完成后,它被发送到 DataGridUtil 类的 downloadcsv 方法。
     exporttoCSV() {
            let exprtcsv: any[] = [];
            (<any[]>JSON.parse(JSON.stringify(this.data))).forEach(x => {
                var obj = new Object();
                var frmt = new Format();
                for (var i = 0; i < this.columns.length; i++) {
                    let transfrmVal = frmt.transform(x[this.columns[i].variable], 
                                      this.columns[i].filter);
                    obj[this.columns[i].display] = transfrmVal;
                }
                exprtcsv.push(obj);
            }
            );
            DataGridUtil.downloadcsv(exprtcsv, this.exportFileName);
        }
DataGrid 模板
  1. 编辑 app->shared->datagrid 文件夹中的 datagrid.component.html。让我们一步一步地理解它。
  2. 以下代码用于根据 isshowfilter 布尔输入变量和数据加载来显示 Search 控件。其余部分与上一篇文章相同,即更改事件等。
    <div *ngIf="isshowfilter && data">
        <search-list [title]="searchTitle" (change)="criteriaChange($event)"></search-list>
    </div>
  3. 下一段代码是显示 header 按钮并附加 click 事件。我们循环遍历 hdrbtns 列表,检查 ishide 按钮是否为 false,并将单个 hdrbtns 项目作为参数传递,以检查 action<ng-container> 是一个逻辑容器,可用于对节点进行分组,但不会作为节点渲染到 DOM 树中。
    <div *ngIf="data" class="add-btn-postion">
        <div>
            <ng-container *ngFor="let hdrbtn of hdrbtns">
                <button *ngIf="!hdrbtn.ishide" type="button" class="btn btn-primary" 
                 (click)="click(hdrbtn,null)">{{hdrbtn.title}}</button>
            </ng-container>
           <button *ngIf="isExporttoCSV && (data!=null && data.length>0)" 
            type="button" class="btn btn-primary" (click)="exporttoCSV()">Export to Excel</button>
        </div>
    </div>
  4. 接下来,整个表格用于数据网格,让我们理解其中的重要部分。
    1. 我们循环遍历列列表,附加 click 事件,该事件调用 changeSorting 方法,并将其作为数据列名称作为参数。然后循环遍历 gridbtns 以创建用于操作按钮的空 td(以使 Header 和数据行的 td 数量相等)。
                     <tr>
                      <th *ngFor="let column of columns" [class]="selectedClass(column.variable)"
                          (click)="changeSorting(column.variable)">
                          {{column.display}}
                      </th>
                      <ng-container *ngFor="let btn of gridbtns">
                          <td *ngIf="!btn.ishide"></td>
                      </ng-container>
                  </tr>
    2. tbody 块中,我们循环遍历实际数据,并应用 orderby 过滤器,该过滤器从 convertSorting 方法获取参数。convertSorting 方法从输入 sort 变量获取 sort column。在第二次循环中,我们遍历 columns,从每个数据 row 中获取单个列的值,同时调用 format pipe。请记住,我们为每一列定义了 format。在第三次循环中,就像 Header 按钮一样,我们循环遍历 Grid 按钮,通过提供当前按钮和当前行(以获取键)作为参数来附加 click 事件,并检查 ishide 属性以检查是否显示当前按钮。
               <tr *ngFor="let row of pdata | orderby : convertSorting()">
                      <td *ngFor="let column of columns">
                          {{row[column.variable] | format : column.filter}}
                      </td>
                      <ng-container *ngFor="let btn of gridbtns">
                          <td *ngIf="!btn.ishide">
                              <button type="button" class="btn btn-primary" 
                               (click)="click(btn,row)">{{btn.title}}</button>
                          </td>
                      </ng-container>
                  </tr>
DataGridUtil Class
  1. 编辑 app->shared->datagrid 文件夹中的 datagrid.util.ts
  2. DataGridUtil 包含三个功能,它们非常不言自明,第一个是 downloadcsv,它调用 converttoCSV 方法将 data 对象转换为 CSV,并调用 createFileName 将日期和时间附加到文件名中以使其唯一。此下载功能已在 IE 11、Firefox 和 Chrome 中进行了测试。
     public static downloadcsv(data: any, exportFileName: string) {
            var csvData = this.convertToCSV(data);
    
            var blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" });
    
            if (navigator.msSaveBlob) { // IE 10+
                navigator.msSaveBlob(blob, this.createFileName(exportFileName))
            } else {
                var link = document.createElement("a");
                if (link.download !== undefined) { // feature detection
                    // Browsers that support HTML5 download attribute
                    var url = URL.createObjectURL(blob);
                    link.setAttribute("href", url);
                    link.setAttribute("download", this.createFileName(exportFileName));
                    //link.style = "visibility:hidden";
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                }
            }
        }
    
        private static convertToCSV(objarray: any) {
            var array = typeof objarray != 'object' ? JSON.parse(objarray) : objarray;
    
            var str = '';
            var row = "";
    
            for (var index in objarray[0]) {
                //Now convert each value to string and comma-separated
                row += index + ',';
            }
            row = row.slice(0, -1);
            //append Label row with line break
            str += row + '\r\n';
    
            for (var i = 0; i < array.length; i++) {
                var line = '';
                for (var index in array[i]) {
                    if (line != '') line += ','
                    line += JSON.stringify(array[i][index]);
                }
                str += line + '\r\n';
            }
            return str;
        }
    
        private static createFileName(exportFileName: string): string {
            var date = new Date();
            return (exportFileName +
                date.toLocaleDateString() + "_" +
                date.toLocaleTimeString()
                + '.csv')
        }
Format Pipe
  1. 编辑 app->shared->datagrid 文件夹中的 format.cs
  2. Format 是实现 PipeTransform interfacepipe。我们处理 textdateCSV 数据。如果您需要,请随意添加更多格式。
export class Format implements PipeTransform {
    datePipe: DatePipe = new DatePipe('yMd');
    transform(input: any, args: any): any {
        if (input == null) return '';
        var format = '';
        var parsedFloat = 0;
        var pipeArgs = args.split(':');
        for (var i = 0; i < pipeArgs.length; i++) {
            pipeArgs[i] = pipeArgs[i].trim(' ');
        }

        switch (pipeArgs[0].toLowerCase()) {
            case 'text':
                return input;
            case 'date':
                return this.getDate(input);
            case 'csv':
                if (input.length == 0)
                    return "";
                if (input.length == 1)
                    return input[0].text;
                let finalstr: string = "";
                for (let i = 0; i < input.length; i++) {
                    finalstr = finalstr + input[i].text + ", ";
                }
                return finalstr.substring(0, finalstr.length - 2);
            default:
                return input;
        }
    }

    private getDate(date: string): any {
        return new Date(date).toLocaleDateString();
    }
}
OrderBy Pipe
  1. 编辑 app->shared->datagrid 并编辑 orderby.ts 文件。
  2. 我直接从这篇 文章 中获取了这个 pipe,所以您可以从那里阅读它的描述。

摘要

这是一个非常简单的数据网格,可以帮助您显示带有操作按钮的格式化数据。尽管如此,仍有很大的改进空间,例如级联、PDF 导出、就地编辑(虽然我个人不喜欢)、分页等。还有一件事我真的很想改进,那就是摆脱输入 filter 变量,并使其对任何数据通用,因为我们有足够的输入数据信息,例如 列名格式

历史

  • 创建于 2017/9/7

参考

© . All rights reserved.