0

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 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>

5
  • I can't see @Output() selectedRowsEvent = new EventEmitter<any[]>(); in Milestone component. Commented Jan 9, 2023 at 4:40
  • @Benny yes, because this component does not even get data passed. i tried it with the console log Commented Jan 9, 2023 at 8:40
  • But <app-milestone-search (selectedRowsEvent)="onNotify($event)" class="nvt-search" [selectedGigaArea]="selectedGigaArea"></app-milestone-search> requires this @Output to exist in Milestone Commented Jan 9, 2023 at 9:10
  • ok. i can add it. yet, how can pass the data from the Table component to Milestone component? Commented Jan 9, 2023 at 14:30
  • You're already doing it in Table component like this: this.selectedRowsEvent.emit(this.selectedRows);. Do the same in Milestone, and complete the chain of notification from Table through Milestone to Dashboard Commented Jan 9, 2023 at 15:54

1 Answer 1

0

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.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);
      }
    
    • 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)
    );
    
    
    
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.