I have a general question related to the TableBodyComponent.html. It has an *ngIf that's kicking off a changed after checked error and I'm trying to figure out how the content of the if statement relates to my data.
The particular *ngIf is below the console error in the attached image (in case the image doesn't send properly it's at TableBodyComponent.html: 125). I'm not 100% sure what the content of the if statement conveys and I'm wondering if you can offer any insights as to what's getting hooked on my side that I may need to manually postpone to prevent this error.
2 Answers, 1 is accepted
There are multiple conditions related to whether a detail row needs to be displayed, whether a master-grid row is expanded, etc., for example related to the conditional display of the detail template:
https://www.telerik.com/kendo-angular-ui/components/grid/advanced-features/detail-template/#toc-conditional-display
To gain a better understanding of the inner logic and mechanics of the various Grid functionalities and rendering logic, you can obtain the whole Grid source code, and inspect it as described in the following section of our documentation:
https://www.telerik.com/kendo-angular-ui/components/installation/source-code/
For example, this is the whole code for the TableBodyComponent:
import { Component, Input, SimpleChange, NgZone, Renderer2, ElementRef, OnInit, OnDestroy } from
'@angular/core'
;
import { GroupDescriptor } from
'@progress/kendo-data-query'
;
import { ColumnBase } from
'../columns/column-base'
;
import { DetailTemplateDirective } from
'./details/detail-template.directive'
;
import { DetailsService } from
'./details/details.service'
;
import { GroupsService } from
'../grouping/groups.service'
;
import { GroupItem, Item, GroupFooterItem } from
'../data/data.iterators'
;
import { ChangeNotificationService } from
'../data/change-notification.service'
;
import { isChanged, isPresent } from
'../utils'
;
import { NoRecordsTemplateDirective } from
'./no-records-template.directive'
;
import { EditService } from
'../editing/edit.service'
;
import { LocalizationService } from
'@progress/kendo-angular-l10n'
;
import { RowClassFn } from
'./common/row-class'
;
import { SelectableSettings } from
'../selection/selectable-settings'
;
import { columnsToRender, columnsSpan } from
"../columns/column-common"
;
import { closest, closestInScope, hasClasses, isFocusable, matchesClasses, matchesNodeName } from
'./common/dom-queries'
;
import { DomEventsService } from
'../common/dom-events.service'
;
import { SelectionService } from
"../selection/selection.service"
;
import { ColumnInfoService } from
"../common/column-info.service"
;
import { FilterableSettings, hasFilterRow } from
'../filtering/filterable'
;
import { CellKeydownEvent } from
'../navigation/cell-keydown-event'
;
import { Subscription } from
'rxjs/Subscription'
;
import { NavigationService } from
'../navigation/navigation.service'
;
import { Keys } from
'../common/keys'
;
const NON_DATA_CELL_CLASSES =
'k-hierarchy-cell k-detail-cell k-group-cell'
;
const NON_DATA_ROW_CLASSES =
'k-grouping-row k-group-footer k-detail-row k-grid-norecords'
;
const IGNORE_TARGET_CLASSSES =
'k-icon'
;
const IGNORE_CONTAINER_CLASSES =
'k-widget k-grid-ignore-click'
;
const columnCellIndex = (cell, cells) => {
let cellIndex = 0;
for
(let idx = 0; idx < cells.length; idx++) {
if
(cells[idx] === cell) {
return
cellIndex;
}
if
(!hasClasses(cells[idx],
'k-hierarchy-cell k-group-cell'
)) {
cellIndex++;
}
}
};
/**
* @hidden
*/
@Component({
selector:
'[kendoGridTableBody]'
,
// tslint:disable-line:component-selector
template: `
<ng-template [ngIf]=
"editService.hasNewItem"
>
<tr class=
"k-grid-add-row k-grid-edit-row"
kendoGridLogicalRow
[logicalRowIndex]=
"addRowLogicalIndex()"
[logicalSlaveRow]=
"lockedColumnsCount > 0"
[logicalCellsCount]=
"columns.length"
[logicalSlaveCellsCount]=
"unlockedColumnsCount"
>
<ng-template [ngIf]=
"!skipGroupDecoration"
>
<td [class.k-group-cell]=
"true"
*ngFor=
"let g of groups"
role=
"presentation"
></td>
</ng-template>
<td [class.k-hierarchy-cell]=
"true"
*ngIf=
"detailTemplate?.templateRef"
kendoGridLogicalCell
[logicalRowIndex]=
"addRowLogicalIndex()"
[logicalColIndex]=
"0"
aria-selected=
"false"
>
</td>
<td *ngFor=
"let column of columns; let columnIndex = index"
kendoGridCell
[rowIndex]=
"-1"
[columnIndex]=
"lockedColumnsCount + columnIndex"
[isNew]=
"true"
[column]=
"column"
[dataItem]=
"newDataItem"
[ngClass]=
"column.cssClass"
[ngStyle]=
"column.style"
[attr.colspan]=
"column.colspan"
kendoGridLogicalCell
[logicalRowIndex]=
"addRowLogicalIndex()"
[logicalColIndex]=
"logicalColIndex(columnIndex)"
[colSpan]=
"column.colspan"
role=
"gridcell"
>
</td>
</tr>
</ng-template>
<tr *ngIf=
"data?.length === 0 || data == null"
class=
"k-grid-norecords"
>
<td [attr.colspan]=
"colSpan"
>
<ng-template
[ngIf]=
"noRecordsTemplate?.templateRef"
[templateContext]="{
templateRef: noRecordsTemplate?.templateRef
}">
</ng-template>
<ng-container *ngIf=
"!noRecordsTemplate?.templateRef"
>
{{noRecordsText}}
</ng-container>
</td>
</tr>
<ng-template ngFor
[ngForOf]=
"data"
[ngForTrackBy]=
"trackByFn"
let-item
let-rowIndex=
"index"
>
<tr *ngIf=
"isGroup(item) && isParentGroupExpanded(item) && showGroupHeader(item)"
kendoGridGroupHeader
[columns]=
"columns"
[groups]=
"groups"
[item]=
"item"
[hasDetails]=
"detailTemplate?.templateRef"
[skipGroupDecoration]=
"skipGroupDecoration"
[rowIndex]=
"rowIndex + 1"
[totalColumnsCount]=
"totalColumnsCount"
kendoGridLogicalRow
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalSlaveRow]=
"lockedColumnsCount > 0"
[logicalCellsCount]=
"columns.length"
[logicalSlaveCellsCount]=
"unlockedColumnsCount"
>
</tr>
<tr
*ngIf=
"isDataItem(item) && isInExpandedGroup(item)"
kendoGridLogicalRow
[dataRowIndex]=
"item.index"
[dataItem]=
"item.data"
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalSlaveRow]=
"lockedColumnsCount > 0"
[logicalCellsCount]=
"columns.length"
[logicalSlaveCellsCount]=
"unlockedColumnsCount"
[ngClass]=
"rowClass({ dataItem: item.data, index: item.index })"
[class.k-alt]=
"isOdd(item)"
[class.k-master-row]=
"detailTemplate?.templateRef"
[class.k-grid-edit-row]=
"editService.hasEdited(item.index)"
[attr.data-kendo-grid-item-index]=
"item.index"
[class.k-state-selected]=
"isSelectable() && isRowSelected(item)"
>
<ng-template [ngIf]=
"!skipGroupDecoration"
>
<td [class.k-group-cell]=
"true"
*ngFor=
"let g of groups"
role=
"presentation"
></td>
</ng-template>
<td [class.k-hierarchy-cell]=
"true"
*ngIf=
"detailTemplate?.templateRef"
kendoGridLogicalCell
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalColIndex]=
"0"
[dataRowIndex]=
"item.index"
[dataItem]=
"item.data"
[detailExpandCell]=
"true"
aria-selected=
"false"
>
<a class=
"k-icon"
*ngIf=
"detailTemplate.showIf(item.data, item.index)"
[ngClass]=
"detailButtonStyles(item.index)"
href=
"#"
tabindex=
"-1"
(click)=
"toggleRow(item.index, item.data)"
></a>
</td>
<td
kendoGridCell
[rowIndex]=
"item.index"
[columnIndex]=
"lockedColumnsCount + columnIndex"
[column]=
"column"
[dataItem]=
"item.data"
kendoGridLogicalCell
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalColIndex]=
"logicalColIndex(columnIndex)"
[dataRowIndex]=
"item.index"
[dataItem]=
"item.data"
[colIndex]=
"columnIndex"
[colSpan]=
"column.colspan"
role=
"gridcell"
aria-selected=
"false"
[ngClass]=
"column.cssClass"
[class.k-grid-edit-cell]=
"editService.isEditedColumn(item.index, column)"
[ngStyle]=
"column.style"
[attr.colspan]=
"column.colspan"
*ngFor=
"let column of columns; let columnIndex = index"
>
</td>
</tr>
<tr *ngIf="isDataItem(item) && isInExpandedGroup(item) && detailTemplate?.templateRef &&
detailTemplate.showIf(item.data, item.index) && isExpanded(item.index)"
[class.k-detail-row]=
"true"
[class.k-alt]=
"isOdd(item)"
kendoGridLogicalRow
[dataRowIndex]=
"item.index"
[dataItem]=
"item.data"
[logicalRowIndex]=
"logicalRowIndex(rowIndex) + 1"
[logicalSlaveRow]=
"false"
[logicalCellsCount]=
"1"
>
<td [class.k-group-cell]=
"true"
*ngFor=
"let g of groups"
></td>
<td [class.k-hierarchy-cell]=
"true"
></td>
<td [class.k-detail-cell]=
"true"
[attr.colspan]=
"columnsSpan"
kendoGridLogicalCell
[logicalRowIndex]=
"logicalRowIndex(rowIndex) + 1"
[logicalColIndex]=
"0"
[dataRowIndex]=
"item.index"
[dataItem]=
"item.data"
[colIndex]=
"0"
[colSpan]=
"totalColumnsCount - 1"
role=
"gridcell"
aria-selected=
"false"
>
<ng-template
[templateContext]="{
templateRef: detailTemplate?.templateRef,
dataItem: item.data,
rowIndex: item.index,
$implicit: item.data
}">
</ng-template>
</td>
</tr>
<tr *ngIf="isFooter(item) && (isInExpandedGroup(item) || (showGroupFooters && isParentGroupExpanded(item)))
&& !item.data.hideFooter"
[class.k-group-footer]=
"true"
kendoGridLogicalRow
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalSlaveRow]=
"lockedColumnsCount > 0"
[logicalCellsCount]=
"columns.length"
[logicalSlaveCellsCount]=
"unlockedColumnsCount"
>
<ng-template [ngIf]=
"!skipGroupDecoration"
>
<td [class.k-group-cell]=
"true"
*ngFor=
"let g of groups"
></td>
</ng-template>
<td [class.k-hierarchy-cell]=
"true"
*ngIf=
"detailTemplate?.templateRef"
kendoGridLogicalCell
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalColIndex]=
"0"
aria-selected=
"false"
>
</td>
<td kendoGridLogicalCell
[logicalRowIndex]=
"logicalRowIndex(rowIndex)"
[logicalColIndex]=
"logicalColIndex(columnIndex)"
[attr.data-skip]=
"skipGroupDecoration"
*ngFor=
"let column of footerColumns; let columnIndex = index"
>
<ng-template
[templateContext]="{
templateRef: column.groupFooterTemplateRef,
group: item.data,
field: column.field,
column: column,
$implicit: item.data?.aggregates
}">
</ng-template>
</td>
</tr>
</ng-template>
`
})
export class TableBodyComponent implements OnInit, OnDestroy {
@Input() public columns: Array<ColumnBase> = [];
@Input() public groups: Array<GroupDescriptor> = [];
@Input() public detailTemplate: DetailTemplateDirective;
@Input() public noRecordsTemplate: NoRecordsTemplateDirective;
@Input() public data: Array<GroupItem | Item | GroupFooterItem>;
@Input() public skip: number = 0;
@Input() public selectable: SelectableSettings | boolean;
@Input() public filterable: FilterableSettings;
@Input() public noRecordsText: string =
this
.localization.get(
'noRecords'
);
@Input() public skipGroupDecoration: boolean =
false
;
@Input() public showGroupFooters: boolean =
false
;
@Input() public lockedColumnsCount: number = 0;
@Input() public totalColumnsCount: number = 0;
private clickSubscription: Function;
private cellKeydownSubscription: Subscription;
private clickTimeout: any;
@Input() public rowClass: RowClassFn = () =>
null
;
constructor(
public detailsService: DetailsService,
public groupsService: GroupsService,
private changeNotification: ChangeNotificationService,
public editService: EditService,
private localization: LocalizationService,
private ngZone: NgZone,
private renderer: Renderer2,
private element: ElementRef,
private domEvents: DomEventsService,
public selectionService: SelectionService,
private columnInfoService: ColumnInfoService,
private navigationService: NavigationService
) {
this
.cellKeydownSubscription =
this
.navigationService.cellKeydown.subscribe((args) =>
this
.cellKeydownHandler(args));
}
public get newDataItem(): any {
return
this
.editService.newDataItem;
}
// Number of unlocked columns in the next table, if any
public get unlockedColumnsCount(): number {
return
this
.totalColumnsCount -
this
.lockedColumnsCount -
this
.columns.length;
}
public toggleRow(index: number, dataItem: any): boolean {
this
.detailsService.toggleRow(index, dataItem);
return
false
;
}
public trackByFn = (index: number, item: any): any => {
if
(item.type ===
'data'
&&
this
.editService.hasEdited(item.index)) {
return
item.data;
}
return
index;
}
public isExpanded(index: number): boolean {
return
this
.detailsService.isExpanded(index);
}
public detailButtonStyles(index: number): any {
const expanded =
this
.isExpanded(index);
return
expanded ?
'k-minus'
:
'k-plus'
;
}
public isGroup(item: Item | GroupItem): boolean {
return
item.type ===
'group'
;
}
public isDataItem(item: Item | GroupItem): boolean {
return
!
this
.isGroup(item) && !
this
.isFooter(item);
}
public isFooter(item: Item | GroupItem | GroupFooterItem): boolean {
return
item.type ===
'footer'
;
}
public isInExpandedGroup(item: Item): boolean {
return
this
.groupsService.isInExpandedGroup(item.groupIndex,
false
);
}
public isParentGroupExpanded(item: any): boolean {
return
this
.groupsService.isInExpandedGroup(item.index || item.groupIndex);
}
public isOdd(item: any): boolean {
return
item.index % 2 !== 0;
}
public isSelectable(): boolean {
return
this
.selectable && (<SelectableSettings>
this
.selectable).enabled !==
false
;
}
public isRowSelected(item: any): boolean {
return
this
.selectionService.isSelected(item.index);
}
public ngOnChanges(changes: { [propertyName: string]: SimpleChange }): void {
if
(isChanged(
"columns"
, changes,
false
)) {
this
.changeNotification.notify();
}
}
public logicalRowIndex(rowIndex: number): number {
let pos =
this
.skip + rowIndex;
if
(
this
.hasDetailTemplate) {
pos *= 2;
}
const absoluteRowIndex = 1 + pos;
const addRowOffset =
this
.editService.hasNewItem ? 1 : 0;
const filterRowOffset = hasFilterRow(
this
.filterable) ? 1 : 0;
const headerRowCount =
this
.columnInfoService.totalLevels + filterRowOffset + addRowOffset;
return
absoluteRowIndex + headerRowCount;
}
public addRowLogicalIndex(): number {
return
this
.columnInfoService.totalLevels + 1;
}
public logicalColIndex(colIndex: number): number {
return
this
.lockedColumnsCount + colIndex + (
this
.hasDetailTemplate ? 1 : 0);
}
public ngOnInit(): void {
this
.ngZone.runOutsideAngular(() => {
const clickHandler =
this
.clickHandler.bind(
this
);
const mousedownSubscription =
this
.renderer.listen(
this
.element.nativeElement,
'mousedown'
, clickHandler);
const clickSubscription =
this
.renderer.listen(
this
.element.nativeElement,
'click'
, clickHandler);
const contextmenuSubscription =
this
.renderer.listen(
this
.element.nativeElement,
'contextmenu'
, clickHandler);
this
.clickSubscription = () => {
mousedownSubscription();
clickSubscription();
contextmenuSubscription();
};
});
let originalNoRecordText =
this
.localization.get(
'noRecords'
);
this
.localization.changes.subscribe(() => {
if
(
this
.noRecordsText === originalNoRecordText) {
this
.noRecordsText =
this
.localization.get(
'noRecords'
);
originalNoRecordText =
this
.noRecordsText;
}
});
}
public ngOnDestroy(): void {
if
(
this
.clickSubscription) {
this
.clickSubscription();
}
this
.cellKeydownSubscription.unsubscribe();
clearTimeout(
this
.clickTimeout);
}
public get columnsSpan(): number {
return
columnsSpan(
this
.columns);
}
public get colSpan(): number {
return
this
.columnsSpan +
this
.groups.length + (
this
.hasDetailTemplate ? 1 : 0);
}
public get footerColumns(): ColumnBase[] {
return
columnsToRender(
this
.columns);
}
public showGroupHeader(item: any): boolean {
return
!item.data.skipHeader;
}
private get hasDetailTemplate(): boolean {
return
isPresent(
this
.detailTemplate);
}
private clickHandler(eventArg: any): void {
const target = eventArg.target;
const cell = closest(target, matchesNodeName(
'td'
));
const row = closest(cell, matchesNodeName(
'tr'
));
const body = closest(row, matchesNodeName(
'tbody'
));
if
(cell && !hasClasses(cell, NON_DATA_CELL_CLASSES) &&
!hasClasses(row, NON_DATA_ROW_CLASSES) &&
body ===
this
.element.nativeElement) {
this
.editService.preventCellClose();
const focusable = target !== cell && isFocusable(target,
false
);
if
(!focusable && !matchesNodeName(
'label'
)(target) && !hasClasses(target, IGNORE_TARGET_CLASSSES) &&
!closestInScope(target, matchesClasses(IGNORE_CONTAINER_CLASSES), cell)) {
const args =
this
.cellClickArgs(cell, row, eventArg);
if
(eventArg.type ===
'mousedown'
) {
this
.domEvents.cellMousedown.emit(args);
}
else
{
if
(args.isEditedColumn || !
this
.editService.closeCell(eventArg)) {
if
(eventArg.type ===
'click'
) {
this
.clickTimeout = setTimeout(() => {
this
.emitCellClick(args);
}, 0);
}
else
{
this
.emitCellClick(args);
}
}
}
}
}
}
private emitCellClick(args: any): void {
this
.domEvents.cellClick.emit(Object.assign(args, {
isEdited: args.isEditedRow || args.isEditedColumn
}));
}
private cellKeydownHandler(args: CellKeydownEvent): void {
const body = closest(args.rowElement, matchesNodeName(
'tbody'
));
if
(body !==
this
.element.nativeElement) {
return
;
}
if
(args.keyCode === Keys.enter) {
const clickArgs =
this
.cellClickArgs(args.cellElement, args.rowElement, args.originalEvent);
this
.domEvents.cellClick.emit(clickArgs);
}
}
private cellClickArgs(cell: any, row: any, eventArg: any): any {
const index = columnCellIndex(cell, row.cells);
const column = (
this
.columns as any).toArray()[index];
const columnIndex =
this
.lockedColumnsCount + index;
let rowIndex = row.getAttribute(
'data-kendo-grid-item-index'
);
rowIndex = rowIndex ? parseInt(rowIndex, 10) : -1;
const dataItem = rowIndex === -1 ?
this
.editService.newDataItem : (
this
.data as any).at(rowIndex -
this
.skip);
const isEditedColumn =
this
.editService.isEditedColumn(rowIndex, column);
const isEditedRow =
this
.editService.isEdited(rowIndex);
return
{
column: column,
columnIndex: columnIndex,
dataItem: dataItem,
isEditedColumn: isEditedColumn,
isEditedRow: isEditedRow,
originalEvent: eventArg,
rowIndex: rowIndex,
type: eventArg.type
};
}
}
I am afraid the provided information is not enough for us to determine what exactly is triggering the described error, but if you send us an isolated runnable project (or a Stackblitz example, similar to our online demos) where the described error can be observed, we will investigate it further, and try to determine what is causing and suggest a solution. Thank you in advance.
Regards,
Dimiter Topalov
Progress Telerik
Dimiter,
That's exactly the information I needed actually and in one of the cases it was a loop I was performing to open detail rows which was occurring while the loading checks were still happening for some of the other forms. This just helped me narrow my field of inquiry enough to get under it.