Home Articles

Set your parents free with Angular's dependency injection

Nov 2, 2021 ∘ Yoko Ishioka
Planes flying to goal

If you're using a modular, component-driven architecture, one of the greatest challenges you will face is how to enable communication between parent and child components/directives. On the one hand, you want each component/directive to be independently focused on their purpose. On the other hand, you need them to work together as though they were built as a cohesive unit.

Luckily for us, Angular provides everything you need for any type of situation!

If your parent view contains the children, a common approach is to use the @Input() and @Output() decorators. To communicate with the parent, you can emit custom events through your children. To communicate with the children, you can pass properties that are bound to your parent.

You can also use @ViewChild/@ViewChildren and @ContentChild/@ContentChildren to break down the parent/child walls. (I will discuss these powerful decorators in future articles.)

If your parent view doesn't contain the children directly or perhaps there is no relationship at all, you can use observables in a service to communicate data. That way, it doesn't matter what the actual relationship is because anyone who injects the service can update and subscribe to the observables.

Why use dependency injection to find a parent

I created a tasking system that displays each task in a table row component. I also created button components to take actions on the table row. However, I didn't want to include the button components in the table row component so they could be used regardless if they were in a table or not.

The problem I faced was how to assign the button components a specific ID without having to pass the ID as an input over and over. For example, here is the HTML for the table row component that iterates over an array of tasks.

    <ces-table-row *ngFor="let task of tasks; let index = index;" [id]="task.id" [index]="index">
    <ces-table-column class="justify-center">
        <ces-view-pin [active]="task.pinned"></ces-view-pin>
        <ces-view-delete></ces-view-delete>
    </ces-table-column>
    <ces-table-column class="justify-center">{{ task.id }}</ces-table-column>
    <ces-table-column class="justify-center">
        <ces-task-menu-statuses [default]="task.status"></ces-task-menu-statuses>
    </ces-table-column>
    <ces-table-column>{{ task.dateUpdated | date: 'short' }}</ces-table-column>
    <ces-table-column>{{ task.title }}</ces-table-column>
    <ces-table-column>{{ task.notes }}</ces-table-column>
</ces-table-row>

The button components I mentioned use the selectors 'ces-view-pin' and 'ces-view-delete'. I wanted these components to only be responsible for the state of the button interaction and to send a message to a service when they are pressed. That way, I could avoid having to repeat redundant code each time they are used and they would behave the same way no matter who was using them.

Since the table row component is getting the task ID, I needed a way to navigate up the hierarchy to access it. Thankfully, Angular provides an easy way to search the hierarchy.

How to search for a parent interface

Step 1: Create an abstract class

I started by creating an abstract class that contained the properties I wanted the button components to be able to access.

export abstract class View {
  abstract id: number;
  abstract index: number;
}

Step 2: Implement the abstract class and add the provider

The next step is to implement the abstract class and to provide a reference for anyone who tries to inject it.

import { Component, forwardRef, Input } from '@angular/core';
import { View } from 'projects/view/src/public-api';

@Component({
  selector: 'ces-table-row',
  templateUrl: './table-row.component.html',
  styleUrls: ['./table-row.component.scss'],
  providers: [{
    provide: View, useExisting: forwardRef(() => TableRowComponent)
  }]
})
export class TableRowComponent implements View {
  @Input() id!: number;
  @Input() index: number = -1;

  constructor() { }
}

Step 3: Inject the interface into the child component/directive

The last step is to inject the View Class using the @Optional() decorator to any child who needs to access the parent's properties/methods that was defined in the abstract class.

export class ViewDeleteComponent {

  constructor(
    protected _viewService: ViewService,
    @Optional() public view: View
  ) { }

  delete() {
    const message: ApiChange = {
      id: this.view.id,
      index: this.view.index,
      action: 'delete',
    }

    this._viewService.setChange(message);
  }
}

View this github gist if you want to see the code for all three components.