英文:
Angular does not detect changes through nested components
问题
I have found this problem that is like mine. Yet, my implementation does not work despite following the steps.
I have the following component structure:
- Dashboard
- ActionButton
- Milestone
- Table
- SupplierSearch
- Table
I have tried to pass array selectedRows
from Table to Dashboard, and then to ActionButton using the CustomEvent
and elRef.nativeElement.dispatchEvent
. When I tried to console.log to see if it is passed to any parent components (Dashboard or Milestone) from Dashboard/Milestone/Table, the array simply does not get passed.
Please note that my code is super dirty right now because I have been trying to resolve this issue for almost a day and tried many ways to resolve it. Please focus on my way to implement this mentioned solution (CustomEvent
elRef.nativeElement.dispatchEvent
).
I really appreciate the Stackoverflow community for the shared knowledge, thus, please don't downgrade this post if my English is bad or something is inherently wrong with my problem.
Table
import {
Component,
ElementRef,
EventEmitter,
Input,
Output,
TemplateRef,
} from '@angular/core';
import { TableColumnHeader } from './models/table-column-header';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
})
export class TableComponent {
@Input() rowData;
@Input() headers: TableColumnHeader[] = [];
@Input() columnTemplate: TemplateRef<any>;
@Input() loading: boolean = false;
@Output() selectedRowsEvent = new EventEmitter<any[]>();
selectedRows = [];
constructor(private elRef: ElementRef) {}
onRowSelect(event) {
this.selectedRows.push(event.data);
this.selectedRowsEvent.emit(this.selectedRows);
const evt = new CustomEvent('myCustomEvent', {
bubbles: true,
detail: event,
});
this.elRef.nativeElement.dispatchEvent(evt);
console.log(this.selectedRows);
console.log(event);
console.log('from table onRowSelected ');
}
onRowUnselect(event) {
this.selectedRows = this.selectedRows.filter(
(x) => x.nvtAreaName !== event.data.nvtAreaName
);
this.selectedRowsEvent.emit(this.selectedRows);
console.log(this.selectedRows);
console.log('from table onRowUnselected ');
}
}
Milestone
import {
AfterViewInit,
Component,
Inject,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { TableColumnHeader } from '../../table/models/table-column-header';
import { NvtAreaDataSource } from '../../../services/nvt-area-data-source.service';
import { AreaProgramDataSource } from '../../../services/area-program-data-source.service';
import { MilestoneTableColumn } from '../../../models/business/milestone-table-column';
import { TableComponent } from '../../table/table.component';
@Component({
selector: 'app-milestone-search',
templateUrl: './milestone-search.component.html',
styleUrls: ['./milestone-search.component.scss'],
})
export class MilestoneSearchComponent
implements OnInit, OnChanges, AfterViewInit
{
@Input() selectedGigaArea: string;
milestoneData = [];
loading: false;
private tableComponent!: TableComponent;
selectedRows = [];
@Input() pSelectableRows = [];
@ViewChild(TableComponent)
columnHeaders: TableColumnHeader[] = [
{ value: 'ONKZ', id: 'onkz', sortable: true },
{ value: 'NVT', id: 'nvtAreaName', sortable: true },
{ value: 'STATUS', id: 'status', sortable: true },
{ value: 'ARVM_START', id: 'arvMStart', date: true },
{ value: 'EXP.ROUGH_START', id: 'expRoughStart', date: true },
{ value: 'EXP.ROUGH_END', id: 'expRoughEnd', date: true },
{ value: 'EXP.FINE_START', id: 'expFineStart', date: true },
{ value: 'EXP.FINE_END', id: 'expFineEnd', date: true },
{ value: 'RM_START', id: 'rmStart', date: true },
{ value: 'AFTER_INST_START', id: 'afterInstStart', date: true },
{ value: 'AFTER_INST_END', id: 'afterInstEnd', date: true },
];
constructor(
@Inject(NvtAreaDataSource) private nvtAreaDataSource,
@Inject(AreaProgramDataSource) private areaProgramDataSource
) {}
ngOnInit(): void {
this.nvtAreaDataSource.connect().subscribe((nvtAreas) => {
this.milestoneData = [...nvtAreas];
});
this.areaProgramDataSource.connect().subscribe((areaPrograms) => {
this.milestoneData = this.mergeMilestonesData(
this.milestoneData,
areaPrograms
);
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.date) {
// this.pSelectableRows = changes.date.currentValue;
this.selectedRows = changes.data.currentValue;
}
console.log('from milestone onChanges ' + this.selectedRows.length);
}
ngAfterViewInit(): void {
this.selectedRows = this.tableComponent.selectedRows;
console.log('from ngAfterViewInit ');
}
onNotify(rowsEmitted: any[]): void {
console.log('from milestone onNotify ');
this.selectedRows = rowsEmitted;
}
mergeMilestonesData(nvtAreas, areaPrograms) {
return nvtAreas.map((nvtArea) => {
const areaProgram = areaPrograms.find(
(x) => x.nvtAreaId === nvtArea.nvtAreaId
);
if (!areaProgram) return nvtArea;
const { status, milestones } = areaProgram;
let milestonesColumns = {};
milestones.map((milestone) => {
const milestonesColumn = Object.entries(
new MilestoneTableColumn()
).reduce(
(acc, [key, value]) =>
value === milestone.milestoneType
? {
...acc,
[key]: milestone.milestoneDate,
}
: acc,
{}
);
milestonesColumns = { ...milestonesColumns, ...milestonesColumn };
});
return {
...nvtArea,
...milestonesColumns,
status,
};
});
}
}
Dashboard
import {
AfterViewInit,
Component,
OnChanges,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { TableComponent } from '../../table/table.component';
import { AreabarComponent } from '../areabar/areabar.component';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements AfterViewInit {
selectedRowsEvent($event: any) {
throw new Error('Method not implemented.');
}
@ViewChild(TableComponent)
selectedRows = [];
private tableComponent!: TableComponent;
public selectedGigaArea:
<details>
<summary>英文:</summary>
I have found this [problem][1] that is like mine. Yet, my implementation does not work despite following the steps.
I have the following component structure:
- Dashboard
- ActionButton
- Milestone
- Table
- SupplierSearch
- Table
I have tried to pass array `selectedRows` from **Table** to **Dashboard**, and then to **ActionButton** using the `CustomEvent` and `elRef.nativeElement.dispatchEvent`. When I tried to console.log to see if it is passed to any parent components(**Dashboard** or **Milestone**) from **Dashboard/Milestone/Table**, the array simply does not get passed.
Please note that my code is super dirty right now because I have been trying to resolve this issue for almost a day and tried many ways to resolve it. Please focus on the my way to implement this mentioned [solution][2] (`CustomEvent` `elRef.nativeElement.dispatchEvent`)
I really appreciate the Stackoverflow community for the shared knowledge, thus, please don't downgrade this post if my English is bad or something is inherently wrong with my problem.
Table
```typescript
import {
Component,
ElementRef,
EventEmitter,
Input,
Output,
TemplateRef,
} from '@angular/core';
import { TableColumnHeader } from './models/table-column-header';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
})
export class TableComponent {
@Input() rowData;
@Input() headers: TableColumnHeader[] = [];
@Input() columnTemplate: TemplateRef<any>;
@Input() loading: boolean = false;
@Output() selectedRowsEvent = new EventEmitter<any[]>();
selectedRows = [];
constructor(private elRef: ElementRef) {}
onRowSelect(event) {
this.selectedRows.push(event.data);
this.selectedRowsEvent.emit(this.selectedRows);
const evt = new CustomEvent('myCustomEvent', {
bubbles: true,
detail: event,
});
this.elRef.nativeElement.dispatchEvent(evt);
console.log(this.selectedRows);
console.log(event);
console.log('from table onRowSelected ');
}
onRowUnselect(event) {
this.selectedRows = this.selectedRows.filter(
(x) => x.nvtAreaName !== event.data.nvtAreaName
);
this.selectedRowsEvent.emit(this.selectedRows);
console.log(this.selectedRows);
console.log('from table onRowUnselected ');
}
// onPage(event) {
// this.selectedRows = [];
// this.selectedRowsEvent.emit(this.selectedRows);
// }
}
Table Template
<ng-template #columnTemplate let-rowObject="rowObject" let-id="id">
<ng-container [ngSwitch]="id">
<span *ngSwitchDefault>{{ rowObject[id] | translate }}</span>
</ng-container>
</ng-template>
<ng-template #dateColumnTemplate let-rowObject="rowObject" let-id="id">
<ng-container [ngSwitch]="id">
<span *ngSwitchDefault>{{ rowObject[id] | localizedDate }}</span>
</ng-container>
</ng-template>
<p-table
(onRowSelect)="onRowSelect($event)"
(onRowUnselect)="onRowUnselect($event)"
[paginator]="true"
[rows]="10"
[showCurrentPageReport]="true"
currentPageReportTemplate="{{ 'PAGINATION' | translate }}"
[rowsPerPageOptions]="[10]"
[value]="rowData"
[loading]="loading"
[tableStyle]="{ 'min-width': '79rem' }"
>
<ng-template pTemplate="header">
<tr>
<th style="width: 4rem">
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
</th>
<ng-container *ngFor="let header of headers">
<th
*ngIf="header.sortable; else simpleHeader"
[pSortableColumn]="header.id"
>
{{ header.value | translate }}
<p-sortIcon [field]="header.id"></p-sortIcon>
</th>
<ng-template #simpleHeader>
<th>
{{ header.value | translate }}
</th>
</ng-template>
</ng-container>
</tr>
</ng-template>
<ng-template pTemplate="body" let-rowObject>
<tr>
<td>
<p-tableCheckbox [value]="rowObject"></p-tableCheckbox>
</td>
<td *ngFor="let header of headers">
<ng-container
[ngTemplateOutlet]="
header?.date ? dateColumnTemplate : columnTemplate
"
[ngTemplateOutletContext]="{ rowObject: rowObject, id: header.id }"
></ng-container>
</td>
</tr>
</ng-template>
</p-table>
Milestone
import {
AfterViewInit,
Component,
Inject,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { TableColumnHeader } from '../../table/models/table-column-header';
import { NvtAreaDataSource } from '../../../services/nvt-area-data-source.service';
import { AreaProgramDataSource } from '../../../services/area-program-data-source.service';
import { MilestoneTableColumn } from '../../../models/business/milestone-table-column';
import { TableComponent } from '../../table/table.component';
@Component({
selector: 'app-milestone-search',
templateUrl: './milestone-search.component.html',
styleUrls: ['./milestone-search.component.scss'],
})
export class MilestoneSearchComponent
implements OnInit, OnChanges, AfterViewInit
{
@Input() selectedGigaArea: string;
milestoneData = [];
loading: false;
private tableComponent!: TableComponent;
selectedRows = [];
@Input() pSelectableRows = [];
@ViewChild(TableComponent)
columnHeaders: TableColumnHeader[] = [
{ value: 'ONKZ', id: 'onkz', sortable: true },
{ value: 'NVT', id: 'nvtAreaName', sortable: true },
{ value: 'STATUS', id: 'status', sortable: true },
{ value: 'ARVM_START', id: 'arvMStart', date: true },
{ value: 'EXP.ROUGH_START', id: 'expRoughStart', date: true },
{ value: 'EXP.ROUGH_END', id: 'expRoughEnd', date: true },
{ value: 'EXP.FINE_START', id: 'expFineStart', date: true },
{ value: 'EXP.FINE_END', id: 'expFineEnd', date: true },
{ value: 'RM_START', id: 'rmStart', date: true },
{ value: 'AFTER_INST_START', id: 'afterInstStart', date: true },
{ value: 'AFTER_INST_END', id: 'afterInstEnd', date: true },
];
constructor(
@Inject(NvtAreaDataSource) private nvtAreaDataSource,
@Inject(AreaProgramDataSource) private areaProgramDataSource
) {}
ngOnInit(): void {
this.nvtAreaDataSource.connect().subscribe((nvtAreas) => {
this.milestoneData = [...nvtAreas];
});
this.areaProgramDataSource.connect().subscribe((areaPrograms) => {
this.milestoneData = this.mergeMilestonesData(
this.milestoneData,
areaPrograms
);
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.date) {
// this.pSelectableRows = changes.date.currentValue;
this.selectedRows = changes.data.currentValue;
}
console.log('from milestone onChanges ' + this.selectedRows.length);
}
ngAfterViewInit(): void {
this.selectedRows = this.tableComponent.selectedRows;
console.log('from ngAfterViewInit ');
}
onNotify(rowsEmitted: any[]): void {
console.log('from milestone onNotify ');
this.selectedRows = rowsEmitted;
}
mergeMilestonesData(nvtAreas, areaPrograms) {
return nvtAreas.map((nvtArea) => {
const areaProgram = areaPrograms.find(
(x) => x.nvtAreaId === nvtArea.nvtAreaId
);
if (!areaProgram) return nvtArea;
const { status, milestones } = areaProgram;
let milestonesColumns = {};
milestones.map((milestone) => {
const milestonesColumn = Object.entries(
new MilestoneTableColumn()
).reduce(
(acc, [key, value]) =>
value === milestone.milestoneType
? {
...acc,
[key]: milestone.milestoneDate,
}
: acc,
{}
);
milestonesColumns = { ...milestonesColumns, ...milestonesColumn };
});
return {
...nvtArea,
...milestonesColumns,
status,
};
});
}
}
Milestone template
<app-table
(selectedRowsEvent)="onNotify($event)"
[pSelectableRows]="forms.get('selectedRows')"
*ngIf="milestoneData?.length"
[rowData]="milestoneData"
[headers]="columnHeaders"
[loading]="loading"
>
</app-table>
Dashboard
import {
AfterViewInit,
Component,
OnChanges,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { TableComponent } from '../../table/table.component';
import { AreabarComponent } from '../areabar/areabar.component';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements AfterViewInit {
selectedRowsEvent($event: any) {
throw new Error('Method not implemented.');
}
@ViewChild(TableComponent)
selectedRows = [];
private tableComponent!: TableComponent;
public selectedGigaArea: string;
public totalElements: number;
@ViewChild(AreabarComponent) areabarComponent: AreabarComponent;
constructor() {}
ngAfterViewInit(): void {
this.selectedRows = this.tableComponent.selectedRows;
console.log('from ngAfterViewInit ');
}
// ngOnChanges(changes: SimpleChanges): void {
// console.log('from Dashboard ngOnChanges ');
// throw new Error('Method not implemented.');
// }
onNotify(rowsEmitted: any[]): void {
console.log('from Dashboard onNotify ');
this.selectedRows = rowsEmitted;
}
}
Dashboard template
<div class="dashboard-wrapper">
<div id="giga-areas" class="giga-areas">
<app-areabar
(selectedGigaAreaEvent)="selectedGigaArea = $event"
(totalElementsChanged)="totalElements = $event"
></app-areabar>
</div>
<div class="search-wrapper">
<h1 class="page-header">{{ "SEARCH.ROLLOUT_PROJECT" | translate }}</h1>
<nav class="nav-bar">
<mat-button-toggle-group
#toggleGroup="matButtonToggleGroup"
class="toggle-btn"
>
<mat-button-toggle value="supplier" checked>{{
"SUPPLIERS" | translate
}}</mat-button-toggle>
<mat-button-toggle value="milestone">{{
"MILESTONES" | translate
}}</mat-button-toggle>
</mat-button-toggle-group>
<app-action-button (myCustomEvent)="onNotify($event)"></app-action-button>
</nav>
<div [className]="toggleGroup.value === 'supplier' || 'hide'">
<app-supplier-search
class="nvt-search"
[selectedGigaArea]="selectedGigaArea"
></app-supplier-search>
</div>
<div [className]="toggleGroup.value === 'milestone' || 'hide'">
<app-milestone-search
(selectedRowsEvent)="onNotify($event)"
class="nvt-search"
[selectedGigaArea]="selectedGigaArea"
></app-milestone-search>
</div>
<div *ngIf="!selectedGigaArea" class="infoText">
{{ "VIEW_EDIT_NVT_AREA" | translate }}
</div>
<div *ngIf="!selectedGigaArea && totalElements > 20" class="infoText">
{{ "GIGAAREA_OVERLOAD_MESSAGE" | translate }}
</div>
</div>
<app-search class="nvt-search" [selectedGigaArea]="selectedGigaArea">
</app-search>
</div>
ActionButton
import {
Component,
Inject,
Input,
OnChanges,
OnInit,
SimpleChanges,
} from '@angular/core';
import { AuthenticationProvider } from 'src/app/services/auth/auth-service.injection-token';
import { AuthService } from 'src/app/services/auth/auth.service';
import { MatDialog } from '@angular/material/dialog';
import { PopupsComponent } from '../shared/popups/popups.component';
import { Router } from '@angular/router';
const backUrl = '/home';
const createrolloutprojects = '/createrolloutprojects';
const changeMilestonesUrl = '/changemilestones';
const changeSupplierUrl = '/changesupplier';
const viewDetailsUrl = '/viewdetails';
@Component({
selector: 'app-action-button',
templateUrl: './action-button.component.html',
styleUrls: ['./action-button.component.scss'],
})
export class ActionButtonComponent implements OnInit, OnChanges {
selectedRows = [];
constructor(
public dialog: MatDialog,
@Inject(AuthenticationProvider)
private permissionService: AuthService,
private router: Router
) {}
ngOnInit(): void {
this.router.navigate([backUrl]);
console.log('from action button component ');
console.log(this.selectedRows);
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ngonchanges trigged ');
console.log(this.selectedRows);
}
getPermission(permissionKey: string): boolean {
return !this.permissionService.hasPermission(permissionKey);
}
onNotify(rowsEmitted: any[]): void {
console.log('from action button onNotify');
this.selectedRows = rowsEmitted;
}
openPopupDialog(): void {
console.log('from openPopupDialog');
const dialogRef = this.dialog.open(PopupsComponent, {
width: '900px',
height: '404px',
disableClose: true,
autoFocus: false,
data: {
title: 'CHANGE_AREA_PROGRAM_STATE.TITLE',
plainTextDescription:
'CHANGE_AREA_PROGRAM_STATE.PLAIN_TEXT_DESCRIPTION',
bulletPointDescription:
'CHANGE_AREA_PROGRAM_STATE.BULLET_POINT_DESCRIPTION',
linkText: '',
externalLink: 'https://...', //<- url belonging to lintText
info: 'CHANGE_AREA_PROGRAM_STATE.INFO',
},
});
dialogRef.afterClosed().subscribe((result) => {
console.log(result);
});
}
}
ActionButton template
<div>
<button mat-button [matMenuTriggerFor]="menu">
<!-- *ngIf="selectedRows.length > 0" -->
{{ "MENU" | translate }}
</button>
<mat-menu #menu="matMenu">
<button
mat-menu-item
(click)="openPopupDialog()"
[disabled]="getPermission('PP_AREA_PROGRAM#COMMISSION')"
>
{{ "COMMISSIONED" | translate }}
</button>
<button
mat-menu-item
(click)="openPopupDialog()"
[disabled]="getPermission('PP_AREA_PROGRAM#EXPANSION')"
>
{{ "EXPANSION.START" | translate }}
</button>
<button
mat-menu-item
(click)="openPopupDialog()"
[disabled]="getPermission('PP_AREA_PROGRAM#CANCEL')"
>
{{ "CANCEL" | translate }}
</button>
</mat-menu>
</div>
答案1
得分: 0
以下是翻译好的部分:
无法使其传递的原因
- 如果传递复杂对象,则子组件上的Ngonchanges不会触发
- 所以它停留在仪表板(父组件)处,无法传递到子组件
https://i.stack.imgur.com/NRjIa.png
- 解决方法是传递订阅对象
-
另一个原因是未传递的原因之一:
> 我们有这个视图:
>
> - 仪表板
> - 操作按钮
> - 里程碑
> - 表
> - 供应商搜索
> - 表
>
> 我一直在通过表 -> 里程碑 -> 仪表板 -> 操作按钮传递
>
> 但是我一直在UI的供应商搜索视图上选择行,因此它从表从未传递到里程碑。
解决方法
-
通过事件发射器将数组传递到最上层的父组件(仪表板)
-
然后创建Subject$(可观察对象)以将复杂数据广播到子组件
> Subject就像一个Observable,但可以广播给多个观察者。Subjects就像EventEmitters:它们维护多个监听器的注册表。 — 来源 rxjs
>代码片段
dashboard.ts selectedRows$ = new Subject<any[]>(); onNotify(rowsEmitted: any[]): void { console.log('from Dashboard onNotify'); this.selectedRows = rowsEmitted; this.selectedRowsCount = rowsEmitted.length; console.log(this.selectedRows); this.selectedRows$.next(rowsEmitted); }
- onNotify是将数组传递到父组件的最后一个函数
- 然后创建匿名观察者并订阅(
next()
)
-
然后将Subject selectedRows$传递给子组件操作按钮
dashboard.html <app-action-button [selectedRows]="selectedRows$"> </app-action-button>
-
然后创建匿名观察者并订阅
action-button.ts ngOnInit(): void { // this.router.navigate([backUrl]); console.log('from action button component '); console.log(this.selectedRows); this.selectedRows.subscribe((selectedArray) => console.log('from action button ngOnInit: ' + selectedArray) );
英文:
Reasons why I could not get it passed
- Ngonchanges on the child component does not get triggered if pass complex object
- so it stuck at the dashboard (parent component) and does not get passed to the child component
https://i.stack.imgur.com/NRjIa.png
- workaround is to pass the subscribe object
-
another reason why it was not passed:
> We have this view:
>
> - Dashboard
> - ActionButton
> - Milestone
> - Table
> - SupplierSearch
> - Table
>
> I have been passing through table -> mileston -> dashboard -> actionbutton
>
> but I have been selecting rows on the SupplierSearch view of the table on the ui. thus it has never been passed to the Milestone from the Table
>
Workaround
-
pass the array up to the parent most component (Dashboard) with event emitters
-
then create the Subject$ (observable) two broadcast the complex data to the child component
> A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners. — source rxjs
>code snippet
dashboard.ts selectedRows$ = new Subject<any[]>(); onNotify(rowsEmitted: any[]): void { console.log('from Dashboard onNotify '); this.selectedRows = rowsEmitted; this.selectedRowsCount = rowsEmitted.length; console.log(this.selectedRows); this.selectedRows$.next(rowsEmitted); }
- on notify is the last function in the chain to pass the array up to the parent component
- then it create the anonymous observer and subscribe (
next()
)
-
Subject selectedRows$ then will be passed to the child component action button
dashboard.html <app-action-button [selectedRows]="selectedRows$"> </app-action-button>
-
it will the create the anonymous observer and subscribe
action-button.ts ngOnInit(): void { // this.router.navigate([backUrl]); console.log('from action button component '); console.log(this.selectedRows); this.selectedRows.subscribe((selectedArray) => console.log('from action button ngOnInit: ' + selectedArray) );
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论