AG Grid 数据在组件之间持续存在导致重复节点错误

AG Grid data is persisting between components causing duplicate node errors

编辑:我试图在可重用组件中手动使用网格 api destroy 函数,而父组件实际上调用了 ag 网格(在模板中)。 ag-grid 保留旧数据仍然存在问题。

我正在为我们的应用程序使用一个可重复使用的组件(它有 50 个奇怪的表,所以我们已经封装了工具以便 update/change 更容易)。我已经编码了一些过滤器持久性和行持久性(所以如果你点击一个项目,导航到细节,然后回来,它会带你回到那个项目并选择它。对我们非常大的网格很有用)

问题是,当我从一个没有设置 getRowNodeId 回调的组件转到我设置回调的组件时,它吓坏了,说有重复的节点 ID(b/c 它仍然引用来自旧网格。属性 在那个数据集中不存在,所以一切都是空的)。

我试过手动销毁 aggrid onDestroy 的实例,但这似乎没有任何作用。我已经尝试在 init 上将行数据设置为空数组,但这似乎已经破坏了 init 逻辑(我有一些用于第一次数据绘制的逻辑)并且通常 ag 网格的初始加载逻辑似乎决定它立即完成.如果我这样做,持久性逻辑将不再有效。我也试过将回调设置延迟到第一次数据绘制之后,但它似乎没有使用回调。

关于如何在 ag 网格解析新网格数据时防止 getRowNodeId 溢出的任何想法? (或者正确清除之前的 ag grid 实例。它似乎在某处保留了对它的引用。)

网格是如何设置的

                    <grid-tools pagination = "true" IdField = "RouteId" agGrid = "true" persistFilters = "true" [gridEnum]="gridEnum" [agDataGrid]="transMatrixAgGrid" standardTools = "true"  [filterOptions]="filterOptions"></grid-tools>

                </div>
            </div>
            <div class="row add-top add-bottom">
                <div class="col-xs-12" style="width:100%;height:500px;">

                    <mat-progress-bar mode="indeterminate" *ngIf="loading"></mat-progress-bar>

                    <ag-grid-angular #transMatrixAgGrid class="ag-theme-material agGrid"
                        [rowData]="routes" 
                        [gridOptions] = "gridOptions"
                        (rowClicked) = "rowClick($event)" 
                        (selectionChanged)="updateSelectedItems($event)"                                                    
                        [frameworkComponents] = "frameworkComponents"                             
                        [columnDefs]="agColumns">
                    </ag-grid-angular>

                </div>

在组件中,保存对模板变量的引用。

@ViewChild("transMatrixAgGrid") transMatrixAgGrid;

重复使用的组件。我们正在从 devextreme 过渡,因此有一些适用于 ag 网格场景的条件逻辑。我不想尝试删除其中的一些代码,以防有人试图将此代码粘贴到 IDE 中。这是当前的工作状态。

import { Component, OnInit, Input, OnDestroy } from "@angular/core";
import { MatBottomSheet, MatBottomSheetRef } from "@angular/material";
import { ToolsOptionsMenu } from "./ToolsOptionsMenu/ToolsOptionsMenu.component";
import { DxDataGridComponent } from "devextreme-angular";
import * as _ from "lodash";
import FilterOption from "@classes/FilterOptions.class";
import { utc } from 'moment';
import { FormControl } from '@angular/forms';

// Careful about modifications. Large grids can become slow if this isn't efficient
export function compareDates(filterLocalDateAtMidnight, cellValue) {
    var dateAsString = cellValue;
    if (dateAsString == null) return 0;

    // In the example application, dates are stored as dd/mm/yyyy
    // We create a Date object for comparison against the filter date
    var dateParts = dateAsString.split("T")[0].split("-");
    var day = Number(dateParts[2]);
    var month = Number(dateParts[1]) - 1;
    var year = Number(dateParts[0]);
    var cellDate = new Date(year, month, day);

    // Now that both parameters are Date objects, we can compare
    if (cellDate < filterLocalDateAtMidnight) {
        return -1;
    } else if (cellDate > filterLocalDateAtMidnight) {
        return 1;
    } else {
        return 0;
    }
}

//Ag-grid date formatter.
export function dateFormatter(dateIn) {

    return dateIn.value ? utc(dateIn.value).format("MM/DD/YYYY") : "";
}

//Default grid options
export var gridOptions = {
    rowSelection: "multiple",
    pagination: true,
    rowMultiSelectWithClick: true,
    animateRows: true,
    floatingFilter: true,
    rowBuffer: 20
}

//Default checkbox column options
export var checkboxColumnOptions = {
    field: 'RowSelect',
    width: 50,
    headerName: ' ',
    headerCheckboxSelection: true,
    headerCheckboxSelectionFilteredOnly: true,
    checkboxSelection: true,
    suppressMenu: true,
    sortable: false,
}

@Component({
    selector: "grid-tools",
    templateUrl: "./grid-tools.component.html",
    styleUrls: ["./grid-tools.component.scss"]
})
export class GridToolsComponent implements OnInit, OnDestroy {

    maxSelection = 30000;
    _filterOptions: Array<FilterOption> = []
    currentlySelected = [];
    currentPage = 1;
    pageControl: FormControl;
    storageKey = "";
    selectionStorageKey = "";

    ALL_FILTER = "ALL";
    currentFilter = this.ALL_FILTER;

    //!!!Using filterOptions.push will not trigger this!!!!
    @Input() set filterOptions(value: Array<FilterOption>) {

        // value.splice(0, 0, new FilterOption());
        value.push(new FilterOption());
        this._filterOptions = value;

    };

    get filterOptions() {
        return this._filterOptions;
    };

    @Input() dataGrid: DxDataGridComponent;
    @Input() agDataGrid;
    @Input() agGrid: boolean = false;
    @Input() standardTools: boolean = false;
    @Input() persistGridViewChild?;
    @Input() hideToolsButton = true; //This is until we make the change completely

    @Input() hideExport = false;
    @Input() hideColumnCustomization = false;
    @Input() hideClearFilters = false;
    @Input() hideResetGrid = false;
    @Input() persistFilters = false;
    @Input() pagination = false;
    @Input() gridEnum = null;
    @Input() IdField = null; //Required for navigating to the last selected row   

    constructor(private bottomSheet: MatBottomSheet) {

        this.filterOptions = [];
    }

    ngOnDestroy() {
        // console.log("Destroying component");
        // if (this.agDataGrid) {

        //     this.agDataGrid.api.destroy();
        // }
    }

    ngOnInit() {

        this.pageControl = new FormControl(this.currentPage);
        this.storageKey = "agGrid-" + this.gridEnum;
        this.selectionStorageKey = "agGrid-" + this.gridEnum + "-selection";

        if (this.dataGrid) {

            this.dataGrid.onContentReady.subscribe(result => {
                this.quickFilterSearch();
            });

            this.dataGrid.filterValueChange.subscribe(filterValue => {
                this.quickFilterSearch();
            });


            this.dataGrid.selectedRowKeysChange.subscribe(
                (selections) => {
                    this.currentlySelected = selections;
                }
            )
        }

        if (this.agDataGrid) {

            if (this.IdField) {

                this.agDataGrid.getRowNodeId = (data) => {
                    return data ? data[this.IdField] : data["Id"];
                };
            }

            this.agDataGrid.gridReady.subscribe(
                () => {

                }
            )

            if (this.pagination) {

                this.pageControl.valueChanges
                    .subscribe(newValue => {
                        if (newValue == undefined) {
                            newValue = 1;
                        }
                        this.agDataGrid.api.paginationGoToPage(newValue - 1);
                    });

                this.agDataGrid.paginationChanged.subscribe(($event) => {
                    this.onPaginationChanged($event);
                })

            }



            this.agDataGrid.selectionChanged.subscribe(
                (event) => {

                    this.currentlySelected = this.agDataGrid.api.getSelectedRows();
                }
            );

            this.agDataGrid.rowClicked.subscribe(
                (event) => {

                    if (this.persistFilters) {

                        this.storeSelectedItem(event.node);

                    }
                }
            )

            this.agDataGrid.rowSelected.subscribe(
                (event) => {

                    if (this.persistFilters) {
                        if (event.node.isSelected()) {

                            this.storeSelectedItem(event.node);
                        }
                        else {
                            if (this.agDataGrid.api.getSelectedRows().length == 0) {
                                localStorage.setItem(this.selectionStorageKey, null);
                            }
                        }
                    }
                }
            )

            this.agDataGrid.filterChanged.subscribe(
                (event) => {
                    let currentFilter = this.agDataGrid.api.getFilterModel();
                    if (Object.keys(currentFilter).length == 0) {
                        this.currentFilter = 'ALL';
                    }

                    if (this.persistFilters) {
                        this.setPersistedFilterState(currentFilter);
                    }

                    this.quickFilterSearch();
                }
            )

            this.agDataGrid.firstDataRendered.subscribe(
                (event) => {



                    if (this.persistFilters) {
                        this.restoreFilters();
                        this.restoreSelection();
                    }
                }
            )
        }
    }

    storeSelectedItem(node) {
        let selectedItem = {
            Id: node.id,
            data: node.data
        }
        localStorage.setItem(this.selectionStorageKey, JSON.stringify(selectedItem));
    }

    onPaginationChanged($event) {
        this.currentPage = this.agDataGrid.api.paginationGetCurrentPage()
        this.pageControl.patchValue(this.currentPage + 1);
        this.pageControl.updateValueAndValidity({ emitEvent: false, onlySelf: true });
    }

    restoreSelection() {
        try {

            let selection: any = localStorage.getItem(this.selectionStorageKey);

            if (selection) {

                selection = JSON.parse(selection);

                let node = this.agDataGrid.api.getRowNode(selection.Id);
                node.setSelected(true);
                this.agDataGrid.api.ensureNodeVisible(node, "middle");

            }
        }
        catch (error) {
            console.log("Something wrong with getting " + this.selectionStorageKey + " local storage");
        }

    }

    restoreFilters() {

        let filterModel = localStorage.getItem(this.storageKey);

        if (filterModel) {
            filterModel = JSON.parse(filterModel);

            //TODO: Manually compare to incoming filter options and see if something matches.
            this.currentFilter = '';
        }

        this.agDataGrid.api.setFilterModel(filterModel);
    }

    setPersistedFilterState = (filterValue: any): void => {

        const stringifiedState: string = JSON.stringify(filterValue);
        localStorage.setItem(this.storageKey, stringifiedState);
    };

    resetColumns() {
        if (this.persistGridViewChild) {

            this.persistGridViewChild.resetColumnDefaults();
        }

        if (this.agDataGrid) {
            this.agDataGrid.columnApi.resetColumnState();
        }
    }

    customGrid() {
        this.dataGrid.instance.showColumnChooser();
    }

    export() {
        if (this.currentlySelected.length < this.maxSelection) {

            let selectionOnly = this.currentlySelected.length == 0 ? false : true;

            if (this.agDataGrid) {
                this.agDataGrid.api.exportDataAsCsv({
                    onlySelected: selectionOnly
                });
            }

            if (this.dataGrid) {

                this.dataGrid.instance.exportToExcel(selectionOnly);
            }
        }
    }

    clearAgSelections() {
        this.agDataGrid.api.clearSelections();
    }

    clearSelections() {
        if (this.agDataGrid) {
            this.agDataGrid.api.deselectAll();
        }
        else {

            this.dataGrid.selectedRowKeys = [];
        }
    }

    quickFilterSearch() {
        let state: any = {};

        if (this.agDataGrid) {
            state = _.cloneDeep(this.agDataGrid.api.getFilterModel());
        }
        else {
            state = _.cloneDeep(this.dataGrid.instance.state());
        }


        if (this.agDataGrid) {
            //TODO
        }
        else {
            this.currentFilter = ""; //If a custom filter is created by the user, we don't want any of the chips to be highlighted.

            if (state.filterValue == null) {
                this.currentFilter = this.ALL_FILTER;
            } else {
                _.map(this._filterOptions, (option: any) => {
                    let isEqual = _.isEqual(option.filter, state.filterValue);

                    if (isEqual) {
                        this.currentFilter = option.label;
                    }
                });
            }
        }
    }

    isFilterActive(incomingFilter) {
        return this.currentFilter == incomingFilter;
    }

    showToolsOptions() {
        this.bottomSheet.open(ToolsOptionsMenu, {
            data: {
                grid: this.dataGrid,
                persistGrid: this.persistGridViewChild
            }
        });
    }

    public filterGrid(filterOptions = new FilterOption()) {
        if (this.dataGrid) {

            this.currentFilter = filterOptions.label;

            const state: any = _.cloneDeep(this.dataGrid.instance.state());
            state.filterValue = filterOptions.filter; // New filterValue to be applied to grid.
            this.dataGrid.instance.state(state);

            //The state mechanism seems to not work if persistance is not active on the grid. 
            //This grid is stupid.
            if (!this.persistGridViewChild) {

                this.dataGrid.instance.clearFilter();
            }
        }

        if (this.agDataGrid) {
            this.currentFilter = filterOptions.label;
            this.agDataGrid.api.setFilterModel(filterOptions.filter);

            if (this.persistFilters) {

                this.setPersistedFilterState(filterOptions.filter);
            }
        }
    }
}

编辑:我已经停止使用自定义回调,但它仍在使用。 firstDataRendered 函数实际上是用旧数据触发的。不理想:(

我不确定这是否是 ag-grid 的错误,但最终我尝试重新使用组件中的 gridOptions。

网格工具组件

//Default grid options
export var gridOptions = {
    rowSelection: "multiple",
    pagination: true,
    rowMultiSelectWithClick: true,
    animateRows: true,
    floatingFilter: true,
    rowBuffer: 20
}

使用网格工具的组件

import { gridOptions } from '@app/grid-tools/grid-tools.component';

正在赋值

this.gridOptions = gridOptions;

模板

<ag-grid-angular #transMatrixAgGrid class="ag-theme-material agGrid"
                            [rowData]="routes" 
                            [gridOptions] = "gridOptions"                            
                            [columnDefs]="agColumns">
                        </ag-grid-angular>

不知何故,这导致 ag-grid 保留了以前网格实例的数据。我通过简单地使用

解决了这个问题

this.gridOptions = Object.assign({}, gridOptions)

我假设 ag-grid 正在修改参考并且其中包含有关数据的信息。