9

In my situation, I have a component that should behave differently if it is inside a specific component. So I want to look through the parents to find the component of the correct type, which works well through dependency injection in the simple case:

Child Component

@Component({
    selector: 'spike-child',
    template: `<div>I am the child, trying to find my parent. 
        <button (click)="log()">Test</button></div>`
})
export class ChildComponent {
    // Get Parent through dependency injection; or null if not present.
    public constructor(@Optional() private parent: ParentComponent) { }

    public log(): void {
        console.log('Parent', this.parent);
        console.log('Parent.Id', this.parent && this.parent.id);
    }
}

Parent Component

@Component({
    selector: 'spike-parent',
    template: `
    <div>
        <div>I am the parent of the content below, ID = {{ id }}:</div>
        <ng-content></ng-content>
    </div>`
})
export class ParentComponent {
  @Input()
  public id: number;
}

Usage, works

<spike-parent [id]="1">
  <spike-child></spike-child>
</spike-parent>

Unfortunately, this does not work anymore if we add one more indirection through content projection like this:

ProjectedContent Component

@Component({
    selector: 'spike-projected-content',
    template: '<spike-parent [id]="2"><ng-content></ng-content></spike-parent>'
})
export class ProjectedContentComponent { }

Usage, does not work

  <spike-projected-content>
    <spike-child></spike-child>
  </spike-projected-content>

Obviously, the child will again be inside a parent at runtime, but now it always gets null as the injected parameter. I understand that content that is projected keeps the original context (including the injector chain), which is definitely almost always helpful, but is there some way to tackle this situation? I read about ngComponentOutlet which looks like it might help, but I didn't exactly see how it could fit. I also didn't find any questions that take this last step from this situation. I guess I could query the DOM to achieve the result, but I obviously would like to avoid that and use Angular mechanics for this.

Thank you so much!

Dennis
  • 14,210
  • 2
  • 34
  • 54
  • Can you not pass down an attribute from the parent? Feels weird having a reversed direction of data flow – DavidZ Jul 16 '18 at 17:48
  • I looked into this quite a bit. Very well written question. The problem is basically that `spike-parent` is not the parent of `spike-child` - in terms of component structure. In the DOM it is, but that's only because the HTMLElement can be inserted anywhere we/Angular chooses. I think they simply cannot be referenced via DI in a parent-child as one they aren't in a parent-child structure – Drenai Jul 18 '18 at 09:25

3 Answers3

4

I can only see two ways (except DOM hacking):

  1. Switch to using templates. Ticket is still open: 14935. Currently it is impossible to override injector in viewContainerRef.createEmbeddedView, however there is option to pass context information (example context-parameter-in-angular, or use NgTemplateOutlet).
  2. Use DI to be able to find projected child components and call their methods. Each child component provides itself using common token {provide: Token, useExisting: forwardRef(() => Child)}, it allows to use @ContentChildren(Token) and get list of all projected children, and call any method/setter.
kemsky
  • 14,727
  • 3
  • 32
  • 51
  • The forwardRef pattern is new to me. Good to know – Drenai Jul 16 '18 at 23:31
  • That first bullet point looks interesting, unfortunately I don't have time right now to examine this. But as your second point already improves my situation, I will accept your answer and come back to that first idea later - thanks a lot!! – Dennis Jul 19 '18 at 10:28
  • This is needed if you want to make code more generic, i.e. all children could implement common interface, without token (or base abstract class) you can not write generic `@ContentChildren` query which finds all supported children. This pattern was described somewhere on anguler issue tracker. – kemsky Jul 19 '18 at 10:39
1

From a component perspective, ChildComponent is a sibling of ParentComponent, they are not in a parent child relationship. The are both components that have a shared parent ProjectedContentComponent

In the DOM their HTMLElement representation is in a parent/child order, but that is separate from the component structure. We can detatch() the HTML of any component and place it anywhere in the DOM - which is what the ng-content is doing

  1. Use the shared parent (ProjectedContentComponent) to pass references between the siblings: StackBlitz Demo
Drenai
  • 11,315
  • 9
  • 48
  • 82
  • Thanks a lot, this provided some new ideas. It is still a trade-off for me: By inspecting the DOM, I don't have that intermediate component (`ParentComponent`) to know about any of this. In my context though, it might be reasonable to go that way. Thanks! – Dennis Jul 19 '18 at 10:25
  • 2nd stackblitz link is a 404 – Kevin Beal Dec 18 '20 at 15:57
0

To solve my immediate problem at hand, I now went with querying the DOM. This feels really hacky and I would greatly appreciate other suggestions, but I also want to share my solution with anyone who has a similar case.

NOTE: My solution only finds out if my component is inside some other component. It does not get a reference to the instance of that component, as I did not need that in my case.

Given the following function:

private hasParent(element: ElementRef, selector: string): boolean {
    let parent = element.nativeElement;
    while(parent = parent.parentElement) {
        if (parent.matches(selector)) { return true; }
    }

    return false;
}

We can find out if our component is inside some specific other component like this:

public constructor(element: ElementRef) {
    const isChildOfSpikeParent = this.hasParent(element, 'spike-parent');

I also tried an approach like the following to eliminate the hard-coded selector, but it did not work well with AOT, so I discarded that:

function getSelectorOfComponent(componentType: Function): string {
    // Reading annotations, see: https://stackoverflow.com/questions/46937746/how-to-retrieve-a-components-metadata-in-angular
    const decorators: any[] = (<any>componentType).__annotations__;
    const componentDecorator = decorators && decorators.find(d => d.ngMetadataName === 'Component');

    return componentDecorator && componentDecorator.selector;
}
Dennis
  • 14,210
  • 2
  • 34
  • 54