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:
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 (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 ');
}
// 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>
https://i.sstatic.net/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);
}
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)
);