3

How to call main component function from a child component when I have deep nested hierarchy?

enter image description here

I have 3 components, button component I'm including inside browser component and browser component I'm including inside main component.

On click of button I need to call a function which is inside main component.

Button Component

@Component({
    selector: 'cb-button',
    templateUrl: 'cb-button.component.html',
    styleUrls: ['cb-button.component.scss']
})

export class CbButtonComponent {

     @Output() onClick: EventEmitter<any> = new EventEmitter();

     onBtnClick(): void {
        this.onClick.emit();
    }
}

button component html

<div (click)="onBtnClick()">
    <button>btn</button>
</div>

browser component

@Component({
    selector: 'topology-browser',
    templateUrl: 'topology-browser.component.html',
    styleUrls: ['topology-browser.component.scss']
})

export class TopologyBrowserComponent {

   @Input('campus') campus: Campus;
}

browser component html

<div>
<h1>browser title</h1>
<cb-button (click)="editCampus()"></cb-button>
</div>

and finally in main component i'm including browser component

main-component.ts

editCampus() {
  alert('clicked');
}

html

<topology-browser [campus]="campus"></topology-browser>

when I click button I'm getting below error

Errorself.parentView.context.editCampus is not a function

Pratap A.K
  • 4,337
  • 11
  • 42
  • 79
  • 2
    You should use a shared service if you want to pass data several layers deep – Michael Kang Mar 30 '17 at 03:07
  • Preferred method is use a shared service and inject it in the child constructor. Duplicate of http://stackoverflow.com/a/42405146/3103979 – Dan Mar 30 '17 at 03:09

4 Answers4

3
  • If you know what type the root component is, you can use Pengyy's method.
  • If you don't know what type in advance but you can customize the root component to your needs, a shared service is a good way.

  • A more generic way is to get the root component by injecting ApplicationRef (not actually tested myself):

constructor(app:ApplicationRef, injector: Injector) {
  app.components[0].someMethodOnMainComponent();
}

https://angular.io/docs/ts/latest/api/core/index/ApplicationRef-class.html

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
2

As I know there are two ways for your situation:

  • inject the parent component to child component
  • use EventEmitter

Since you have more than two layers, injecting MainComponent to ButtonComponent is much more simply to implement:

import {Component, Inject, forwardRef} from '@angular/core';

// change the MainComponent to your real component name
constructor(@Inject(forwardRef(() => MainComponent)) private main:MainComponent)
Pengyy
  • 37,383
  • 15
  • 83
  • 73
  • 1
    You shouldn't need `forwardRef` if `ButtonComponent` and `MainComponent` are not declared within the same file. – Günter Zöchbauer Mar 30 '17 at 06:48
  • The import you've used is for beta :-) – eko Mar 30 '17 at 06:58
  • @echonax oops, sorry for I just copied from an simple test app which been made a long time ago, I'll update for that, thanks. – Pengyy Mar 30 '17 at 07:04
  • @GünterZöchbauer did i misunderstood something about `circular dependency`?, I have been keep this in mind after read this issue.https://github.com/angular/angular/issues/3216 – Pengyy Mar 30 '17 at 07:06
  • @Pengyy according to the doc skipping self will resolve the circular dependency with `@SkipSelf` and you can put an `@Optional` for a null check. So it would be something like `constructor(@Skipself() @Optional() private main:MainComponent)` More info on: https://angular.io/docs/ts/latest/cookbook/dependency-injection.html#!#find-the-parent-in-a-tree-of-parents – eko Mar 30 '17 at 07:09
  • The comment https://github.com/angular/angular/issues/3216#issuecomment-169325097 (and the one below that seem to say the same what I said in above comment). – Günter Zöchbauer Mar 30 '17 at 07:10
0
export class CbButtonComponent {

     @Output() onClick: EventEmitter<string> = new EventEmitter<string>();

     onBtnClick(): void {
        this.onClick.emit('clicked');
    }
}

In your browser component

HTML

<div>
    <h1>browser title</h1>
    <cb-button (onclick)="buttonclicked($event)"></cb-button>
</div>

Component

@Output() buttonClick: EventEmitter<string> = new EventEmitter<string>();

buttonclicked(clickedEvent){
   this.buttonClick.emit('clicked');
}

In your parent Main component

<topology-browser [campus]="campus" (buttonClick)="buttonComponentClicked($event)"></topology-browser>

Component code

buttonComponentClicked(buttonClicked){
       /// method calls goes her

}

By this way you are notifying the browser component with the click which is captured again in the main component and handled

Emin Laletovic
  • 4,084
  • 1
  • 13
  • 22
Aravind
  • 40,391
  • 16
  • 91
  • 110
0

You may try the below. I have not tried this but am guessing it might work. The button component emits an event which is being caught by the main component.

You may also pass data via a common service and injecting that service into the child component and calling a service function which updates a service observable which is subscribed by the main component

Your main component

@Component({
  template `
    <button-comp (onClick)="mainMethod($event)"></button-comp>
  `,
  directives: [ ButtonComponent ]
})
export class MainComponent {
  (...)

  mainMethod(event) {
    console.log(event)
  }
}

your button component

@Component({
    selector: 'cb-button',
    templateUrl: 'cb-button.component.html',
    styleUrls: ['cb-button.component.scss']
})

export class CbButtonComponent {

     @Output() onClick: EventEmitter<any> = new EventEmitter();

     onBtnClick(): void {
        this.onClick.emit('hello');
    }
}

button component html

<div (click)="onBtnClick()">
    <button>btn</button>
</div>