Angular - Can a child component reference a parent

2020-07-23 08:09发布

问题:

I am working on an angular 4 application using a primefaces primeng component, p-contextmenu. I am trying to tell a child element to use a template variable of a parent component.

app.html:

<div>
  <router-outlet></router-outlet>
  <div #contextMenuHolder></div>
</div>

mycomponent.html:

<p-contextMenu [appendTo]="contextMenuHolder" [model]="items"></p-contextMenu>

Obviously it fails as the contextMenuHolder does not exist in the child, but in its parent:

Angular: Identifier 'contextMenuHolder' is not defined. The component declaration, template variable declarations, and element references do not contain such a member

Can you reference a parent's template variable from a child component?

Edit:

Plunkr with it broken. This plunkr shows it not working, but no error messages.

回答1:

The documentation for appendTo says

Target element to attach the overlay, valid values are "body" or a local ng-template variable of another element.

Maybe a service can resolve the issue :

@Injectable()
export class ContextMenuHolder {
  contextMenu: any; // TODO set a type (HTMLElement?)

  getContextMenu() {
    return this.contextMenu;
  }

  setContextMenu(contextMenu: any) {
    this.contextMenu = contextMenu;
  }
}

In your app.ts, you inject the service and set the value. In your component.ts, you inject the service and get the value.

I did not tested it but it should work. If the contextMenu can change, you will have to use event listeners or observable.



回答2:

Thanks to Ludovic Guillaume I was able to find a solution:

https://plnkr.co/edit/kwnkSKDPFs1Bp2xOHqIu

child.ts:

import {Component, NgModule, VERSION} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import {ContextMenuHolderService} from './context-menu-holder.service'

@Component({
  selector: 'child-comp',
  template: `
    <div>
      <h2>Hello child</h2>
      <span #mySpan>this bit has a context menu</span> <br>
      <span #parentSpan>this bit is the target for the parent span.</span>

      <p-contextMenu [target]="mySpan" [appendTo]="parentContext" [model]="items"></p-contextMenu>
      <p-contextMenu [target]="parentSpan" [appendTo]="parentContext" [model]="itemsForParent"></p-contextMenu>
    </div>
  `,
})

export class ChildComponent {
  private items: MenuItem[];
  parentContext: any;

   constructor(private cmhs : ContextMenuHolderService) {

    }

    ngOnInit() {
      this.items = [{ label: 'mySpans context menu' }];
      this.itemsForParent =  [{ label: 'parent context menu items' }];
      console.log('child init', this.cmhs.getContextMenuParent())
      this.parentContext = this.cmhs.getContextMenuParent().nativeElement;
    }
}

Here, the child component has built the context menu with the items it wants in the menu. This menu needs to reside in the parent (sometimes this is necessary for styling or positioning reasons). The child has a parentContext object that will be set during it's onInit phase of the lifecycle.

parent (app.ts):

//our root app component
import {Component, NgModule, VERSION, ViewChild} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import {ChildComponent} from './child'
import {ContextMenuModule,MenuItem} from 'primeng/primeng'
import {ContextMenuHolderService} from './context-menu-holder.service'

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      <div #parentContextTarget>This is in the parent component and should have a context menu</div>
      <div #parentContextWrapper></div>
      <child-comp></child-comp>
    </div>
  `,
})


export class App {
  name:string;
  @ViewChild('parentContextWrapper') parentContextWrapper;

  constructor(private cmhs : ContextMenuHolderService) {
    this.name = `Angular! v${VERSION.full}`
    // console.log('parent constructor')
  }

  ngOnInit(){
    console.log('parent init - parent context wrapper', this.parentContextWrapper)
    this.cmhs.setContextMenuParent(this.parentContextWrapper)
  }


}

The parent sets the object in the service during it's onInit stage. Initially I thought this had to be during the afterViewInit, but this ended up being too late in the lifecycle.

service:

import {Injectable} from '@angular/core';

@Injectable()
export class ContextMenuHolderService {
  contextMenuParent: any; // TODO set a type (HTMLElement?)

  getContextMenuParent() {
    console.log('returning cmp', this.contextMenuParent)
    return this.contextMenuParent;
  }

  setContextMenuParent(contextMenuParent: any) {
    console.log('settin context menu parent', contextMenuParent)
    this.contextMenuParent = contextMenuParent;
  }
}