Navigate the component tree with DI

From Get docs
Angular/docs/10/guide/dependency-injection-navtree


Navigate the component tree with DI

Application components often need to share information. You can often use loosely coupled techniques for sharing information, such as data binding and service sharing, but sometimes it makes sense for one component to have a direct reference to another component. You need a direct reference, for instance, to access values or call methods on that component.

Obtaining a component reference is a bit tricky in Angular. Angular components themselves do not have a tree that you can inspect or navigate programmatically. The parent-child relationship is indirect, established through the components' view objects.

Each component has a host view, and can have additional embedded views. An embedded view in component A is the host view of component B, which can in turn have embedded view. This means that there is a view hierarchy for each component, of which that component's host view is the root.

There is an API for navigating down the view hierarchy. Check out Query, QueryList, ViewChildren, and ContentChildren in the API Reference.

There is no public API for acquiring a parent reference. However, because every component instance is added to an injector's container, you can use Angular dependency injection to reach a parent component.

This section describes some techniques for doing that.

Find a parent component of known type

You use standard class injection to acquire a parent component whose type you know.

In the following example, the parent AlexComponent has several children including a CathyComponent:

@Component({
  selector: 'alex',
  template: `
    <div class="a">
      <h3>{{name}}</h3>
      <cathy></cathy>
      <craig></craig>
      <carol></carol>
    </div>`,
})
export class AlexComponent extends Base
{
  name = 'Alex';
}

Cathy reports whether or not she has access to Alex after injecting an AlexComponent into her constructor:

@Component({
  selector: 'cathy',
  template: `
  <div class="c">
    <h3>Cathy</h3>
    {{alex ? 'Found' : 'Did not find'}} Alex via the component class.<br>
  </div>`
})
export class CathyComponent {
  constructor( @Optional() public alex?: AlexComponent ) { }
}

Notice that even though the @Optional qualifier is there for safety, the confirms that the alex parameter is set.

Unable to find a parent by its base class

What if you don't know the concrete parent component class?

A re-usable component might be a child of multiple components. Imagine a component for rendering breaking news about a financial instrument. For business reasons, this news component makes frequent calls directly into its parent instrument as changing market data streams by.

The app probably defines more than a dozen financial instrument components. If you're lucky, they all implement the same base class whose API your NewsComponent understands.

Looking for components that implement an interface would be better. That's not possible because TypeScript interfaces disappear from the transpiled JavaScript, which doesn't support interfaces. There's no artifact to look for.

This isn't necessarily good design. This example is examining whether a component can inject its parent via the parent's base class.

The sample's CraigComponent explores this question. Looking back, you see that the Alex component extends (inherits) from a class named Base.

export class AlexComponent extends Base

The CraigComponent tries to inject Base into its alex constructor parameter and reports if it succeeded.

@Component({
  selector: 'craig',
  template: `
  <div class="c">
    <h3>Craig</h3>
    {{alex ? 'Found' : 'Did not find'}} Alex via the base class.
  </div>`
})
export class CraigComponent {
  constructor( @Optional() public alex?: Base ) { }
}

Unfortunately, this doesn't work. The confirms that the alex parameter is null. You cannot inject a parent by its base class.

Find a parent by its class interface

You can find a parent component with a class interface.

The parent must cooperate by providing an alias to itself in the name of a class interface token.

Recall that Angular always adds a component instance to its own injector; that's why you could inject Alex into Cathy earlier.

Write an alias provider—a provide object literal with a useExisting definition—that creates an alternative way to inject the same component instance and add that provider to the providers array of the @Component() metadata for the AlexComponent.

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

Parent is the provider's class interface token. The forwardRef breaks the circular reference you just created by having the AlexComponent refer to itself.

Carol, the third of Alex's child components, injects the parent into its parent parameter, the same way you've done it before.

export class CarolComponent {
  name = 'Carol';
  constructor( @Optional() public parent?: Parent ) { }
}

Here's Alex and family in action.

Find a parent in a tree with @SkipSelf()

Imagine one branch of a component hierarchy: Alice -> Barry -> Carol. Both Alice and Barry implement the Parent class interface.

Barry is the problem. He needs to reach his parent, Alice, and also be a parent to Carol. That means he must both inject the Parent class interface to get Alice and provide a Parent to satisfy Carol.

Here's Barry.

const templateB = `
  <div class="b">
    <div>
      <h3>{{name}}</h3>
      <p>My parent is {{parent?.name}}</p>
    </div>
    <carol></carol>
    <chris></chris>
  </div>`;

@Component({
  selector:   'barry',
  template:   templateB,
  providers:  [{ provide: Parent, useExisting: forwardRef(() => BarryComponent) }]
})
export class BarryComponent implements Parent {
  name = 'Barry';
  constructor( @SkipSelf() @Optional() public parent?: Parent ) { }
}

Barry's providers array looks just like Alex's. If you're going to keep writing alias providers like this you should create a helper function.

For now, focus on Barry's constructor.

constructor( @SkipSelf() @Optional() public parent?: Parent ) { }
constructor( @Optional() public parent?: Parent ) { }

It's identical to Carol's constructor except for the additional @SkipSelf decorator.

@SkipSelf is essential for two reasons:

  1. It tells the injector to start its search for a Parent dependency in a component above itself, which is what parent means.

  2. Angular throws a cyclic dependency error if you omit the @SkipSelf decorator.

    Circular dependency in DI detected for BethComponent. Dependency path: BethComponent -> Parent -> BethComponent

Here's Alice, Barry, and family in action.

Parent class interface

You learned earlier that a class interface is an abstract class used as an interface rather than as a base class.

The example defines a Parent class interface.

export abstract class Parent { name: string; }

The Parent class interface defines a name property with a type declaration but no implementation. The name property is the only member of a parent component that a child component can call. Such a narrow interface helps decouple the child component class from its parent components.

A component that could serve as a parent should implement the class interface as the AliceComponent does.

export class AliceComponent implements Parent

Doing so adds clarity to the code. But it's not technically necessary. Although AlexComponent has a name property, as required by its Base class, its class signature doesn't mention Parent.

export class AlexComponent extends Base

AlexComponent should implement Parent as a matter of proper style. It doesn't in this example only to demonstrate that the code will compile and run without the interface.

provideParent() helper function

Writing variations of the same parent alias provider gets old quickly, especially this awful mouthful with a forwardRef.

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

You can extract that logic into a helper function like the following.

// Helper method to provide the current component instance in the name of a `parentType`.
export function provideParent
  (component: any) {
    return { provide: Parent, useExisting: forwardRef(() => component) };
  }

Now you can add a simpler, more meaningful parent provider to your components.

providers:  [ provideParent(AliceComponent) ]

You can do better. The current version of the helper function can only alias the Parent class interface. The application might have a variety of parent types, each with its own class interface token.

Here's a revised version that defaults to parent but also accepts an optional second parameter for a different parent class interface.

// Helper method to provide the current component instance in the name of a `parentType`.
// The `parentType` defaults to `Parent` when omitting the second parameter.
export function provideParent
  (component: any, parentType?: any) {
    return { provide: parentType || Parent, useExisting: forwardRef(() => component) };
  }

And here's how you could use it with a different parent type.

providers:  [ provideParent(BethComponent, DifferentParent) ]

© 2010–2020 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v10.angular.io/guide/dependency-injection-navtree