I use Ag Grid Angular to display the user list.
One of the cells is an email with a custom component. When the value is invalid it shows an error message. To display it properly I should expend the row. When I expend the row, an error despaired.
It looks like the cell renderer component updates every time after calling the onRowHeightChanged() method.
How to fix this issue?
<div class="app-users-list__action-bar">
<button
class="btn btn-primary ml-1"
(click)="changeGridMode()"
>{{gridModeBtnTitle}}</button>
</div>
<ng-container *ngIf="users$ | async"></ng-container>
<div
class="app-users-list__table"
infiniteScroll
[infiniteScrollContainer]="'.ag-body-viewport'"
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="0"
(scrolled)="onScroll()"
[scrollWindow]="false">
<ag-grid-angular
class="ag-theme-bootstrap user-list"
[suppressRowClickSelection]="true"
[gridOptions]="gridOptions"
[frameworkComponents]="frameworkComponents"
>
</ag-grid-angular>
</div>
<ngx-loading [show]="isLoading$ | async"></ngx-loading>
</section>
import { Component, OnInit } from '@angular/core';
import { NgModel } from '@angular/forms';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as fromShared from '../../../shared/models';
import * as userList from '../../actions/user-list.action';
import * as fromUserListSelectors from '../../selectors/user-list.selectors';
import * as gridComponents from '../../../shared/grid-components';
import { Organization } from '../../../organizations';
import { DEFAULT_INTERACTION_LIST_FILTER, SORT_DIRECTION } from '../../../shared/constants/filter.constant';
import { GridApi, GridOptions, GridReadyEvent, RowNode } from 'ag-grid-community';
import { Evaluator } from '../../../evaluators';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss'],
})
export class UserListComponent implements OnInit {
public users$: Observable<fromShared.User[]>;
public isLoading$: Observable<boolean>;
public isEditMode = false;
public frameworkComponents = {...gridComponents};
public gridModeBtnTitle = this.setButtonTitle(this.isEditMode);
public columnDefs = [
{
headerName: 'First Name',
field: 'firstName',
minWidth: 200,
unSortIcon: true,
suppressMovable: true,
resizable: true,
autoHeight: true,
cellRenderer: 'GridTextCellComponent',
cellRendererParams: (params) => {
return {
value: params.data.firstName,
name: 'firstName',
aqaId: 'user-firstName',
};
}
},
{
headerName: 'Last Name',
field: 'lastName',
minWidth: 200,
unSortIcon: true,
suppressMovable: true,
autoHeight: true,
cellRenderer: 'GridTextCellComponent',
cellRendererParams: (params) => {
return {
value: params.data.lastName,
name: 'lastName',
aqaId: 'user-lastName',
};
}
},
{
headerName: 'Email',
field: 'email',
minWidth: 200,
unSortIcon: true,
suppressMovable: true,
autoHeight: true,
cellRenderer: 'GridEmailCellComponent',
cellRendererParams: (params) => {
return {
value: params.data.email,
name: 'email',
aqaId: 'user-email',
};
}
},
{
headerName: 'Sso Email Id',
field: 'SsoEmail',
minWidth: 300,
unSortIcon: true,
suppressMovable: true,
autoHeight: true,
cellRenderer: 'GridEmailCellComponent',
cellRendererParams: (params) => {
return {
editable: true,
editMode: this.isEditMode,
value: params.data.ssoEmail,
name: 'ssoEmail',
placeholder: 'Enter a SSO email',
aqaId: 'user-ssoEmail',
callback: this.updateHandler.bind(this, params.data, 'ssoEmail'),
statusChange: this.statusFieldChange.bind(this, params.node)
};
}
},
{
headerName: 'Org.Admin',
field: 'orgAdmin',
maxWidth: 200,
unSortIcon: false,
suppressMovable: true,
autoHeight: true,
cellRenderer: 'GridTextCellComponent',
cellRendererParams: (params) => {
return {
value: this.setUserRoleStatus(params.data.roles, fromShared.ERole.ORG_ADMIN),
name: 'orgAdmin',
aqaId: 'user-orgAdmin',
};
}
},
{
headerName: 'Eval.Admin',
field: 'evalAdmin',
maxWidth: 200,
unSortIcon: false,
suppressMovable: true,
autoHeight: true,
cellRenderer: 'GridTextCellComponent',
cellRendererParams: (params) => {
return {
value: this.setUserRoleStatus(params.data.roles, fromShared.ERole.EVALUATION_ADMIN),
name: 'evalAdmin',
aqaId: 'user-evalAdmin',
};
}
},
{
headerName: 'Evaluator',
field: 'evaluator',
maxWidth: 200,
unSortIcon: false,
suppressMovable: true,
autoHeight: true,
cellRenderer: 'GridTextCellComponent',
cellRendererParams: (params) => {
return {
value: this.setUserRoleStatus(params.data.roles, fromShared.ERole.CAN_VOTE),
name: 'evaluator',
aqaId: 'user-evaluator',
};
}
}
];
public gridOptions = <GridOptions>{
context: {componentParent: this},
columnDefs: this.columnDefs,
rowData: [],
enableColResize: true,
enableSorting: true,
headerHeight: 40,
rowHeight: 40,
animateRows: true,
suppressLoadingOverlay: false,
onSortChanged: () => {
let sortModel = this.gridOptions.api.getSortModel();
if (sortModel.length > 0) {
this.usersFilter.direction = SORT_DIRECTION[sortModel[0].sort.toUpperCase()];
this.usersFilter.orderBy = sortModel[0].colId;
} else {
this.usersFilter.orderBy = null;
this.usersFilter.direction = DEFAULT_INTERACTION_LIST_FILTER.direction;
}
this.usersFilter.page = 0;
this.gridOptions.api.setRowData([]);
this.store.dispatch(new userList.LoadUsersByFilterAction(this.usersFilter));
},
onRowDataChanged: (params) => {
},
getRowNodeId: (data: Evaluator) => {
return data.id;
},
noRowsOverlayComponent: 'NoRowsOverlayComponent',
noRowsOverlayComponentParams: {
noRowsMessageFunc: () => {
this.isLoading$.pipe(
map(loading => loading ? '' : 'You have no Users.')
);
}
},
rowClassRules: {
'text-transparent': (params) => params.data.unused,
},
defaultColDef: {
headerName: '',
field: '',
minWidth: 100,
unSortIcon: true,
suppressMovable: true,
resizable: true,
autoHeight: true,
},
onGridReady(event: GridReadyEvent) {
event.api.sizeColumnsToFit();
},
};
private usersFilter: fromShared.UserFilterList = {
limit: 20,
page: 0,
orderBy: 'firstName',
direction: SORT_DIRECTION.ASC
};
constructor(private store: Store<Organization>) {}
public ngOnInit(): void {
this.store.dispatch(new userList.LoadUsersByFilterAction(this.usersFilter));
this.isLoading$ = this.store.pipe(select(fromUserListSelectors.selectLoading));
this.users$ = this.store.pipe(select(fromUserListSelectors.selectUsers))
.pipe(
map(users => {
const api: GridApi = this.gridOptions.api;
if (api) {
api.setRowData(users);
}
return users;
})
);
}
public onScroll() {
this.usersFilter.page++;
this.store.dispatch(new userList.LoadUsersByFilterAction(this.usersFilter));
}
public changeGridMode(): void {
this.isEditMode = !this.isEditMode;
this.gridModeBtnTitle = this.setButtonTitle(this.isEditMode);
this.gridOptions.enableSorting = !this.isEditMode;
this.gridOptions.api.refreshCells({force: true});
}
private updateHandler(item: fromShared.User, field: string, newValue: any, controller: NgModel) {
if (controller && controller.invalid) {
return;
}
item[field] = newValue;
this.store.dispatch(new userList.UpdateUserAction(item));
}
private statusFieldChange(node: RowNode, status: string, isTouched: boolean) {
if (!isTouched) {
return;
}
if (status === 'INVALID') {
if (node.rowHeight < 60) {
this.updateRowSize(node, 60);
}
} else {
setTimeout(() => this.updateRowSize(node, null));
}
}
private updateRowSize(node: RowNode, height: number) {
node.setRowHeight(height);
this.gridOptions.api.onRowHeightChanged();
}
private setButtonTitle(isEditMode: boolean): string {
return isEditMode ? 'Done' : 'Edit';
}
private setUserRoleStatus(roles: fromShared.ERole[], type: fromShared.ERole): boolean {
return roles.includes(type);
}
}