Accessing directives located in child components in Angular
Tags: angular, javascript, rxjs, webdev
Published: 2023-10-16 16:14:42

---
title: Accessing directives located in child components in Angular
published: true
description: 
tags: Angular,JavaScript,RxJS,webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ob6pp4ygl7n4xi2e28m.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-10-14 18:55 +0000
---
## 📜 Scenario

In my application I had a directive that would track unsaved changes in a template-driven form. I won't bother you with the implementation of this directive, but its interface was as follows:

```ts
@Directive({
  selector: 'form[appUnsavedChangesTracker]',
  standalone: true,
  exportAs: 'unsavedChangesTracker',
})
export class UnsavedChangesTrackerDirective<T = any> {
  isDirty$: Observable<Boolean>;

  setInitialValue(value: T): void;
}
```

There were two components. First, the smart component, which would provide data:

```ts
@Component({
  selector: `app-smart-component`,
  standalone: true,
  imports: [FormComponent],
  template: `
    <app-form-component
      [formModel]="formModel$ | async"
      (save)="saveData($event)"
    >
    </app-form-component>
  `
})
export class SmartComponent {
  apiService = inject(ApiService);

  formModel$ = this.apiService.getData();

  saveData(data: FormModel) {
    this.apiService.save(data).pipe(
      // when data saved successfully we need to access the
      // UnsavedChangesTrackerDirective and call setInitialValue method
    ).subscribe();
  }
}
```

Second, a dumb component containing the form:

```ts
@Component({
  selector: `app-form-component`,
  standalone: true,
  imports: [FormsModule, UnsavedChangesTrackerDirective],
  template: `
    <form appUnsavedChangesTracker (ngSubmit)="saveForm()">
      <input [ngModel]="formModel.name" name="name" />

      // other input controls removed for brievety

      <button type="submit">Save</button>
    </form>
  `
})
export class FormComponent {
  @Input() formModel: FormModel;

  @Output() save = new EventEmitter<FormModel>();

  @ViewChild(NgForm) ngForm: NgForm;

  saveForm() {
    if (this.ngForm.valid) {
      this.save.emit(this.ngForm.value);
    }
  }
}
```
If you'd like to learn more about the Smart/Dumb component approach, watch [this video](https://youtu.be/4WBV-7PJ0jM?si=yQ15bAytz4jezlhd) by Joshua Morony.

Here we encounter a problem: the `UnsavedChangesTrackerDirective` is attached to the `<form>`, which is located in a child component. However, the parent smart component needs to access the `UnsavedChangesTrackerDirective` to update the initial form value when data is saved to the server.

![Diagram showing where the UnsavedChangesTrackerDirective is located relatively to the other components](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tlqfry7dw3azrwagooo0.png)

We can't simply query for the `UnsavedChangesTrackerDirective` directive via `@ViewChild()` since it's not located in the view of the parent component. We need another way.

## 💡 Initial solution

There is a saying coined in a story by Francis Bacon:

> If the mountain will not come to Muhammad, then Muhammad must go to the mountain

A reference to the directive located in the child component cannot be injected from the parent. However, a directive located in the child component can inject anything up the injection tree. So, we could:

* Create a dedicated service provided somewhere higher in the injection tree, for example, at the level of the parent component.
* Inject that new service into the directive located in the child component.
* Then the directive could "register" itself within the injected service.

![A diagram showing where UnsavedChangesTrackerService would be injected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/crrf84sog1u147o2baas.png)

So, here's the service where the directive will "register" itself:

```ts
@Injectable()
export class UnsavedChangesTrackerService {
  private tracker$$ = new BehaviorSubject<UnsavedChangesTrackerDirective | undefined>(undefined);
  tracker$ = this.tracker$$.asObservable();

  setTracker(tracker: UnsavedChangesTrackerDirective) {
    this.tracker$$.next(tracker);
  }
}
```

Now the directive can inject the service and "register" itself:

```ts
export class UnsavedChangesTrackerDirective<T = any> {
  service = inject(UnsavedChangesTrackerService, { optional: true })
  
  constructor() {
    if (this.service) {
      this.service.setTracker(this);
    }
  }

  // rest of the functionality is removed for brievety
}
```

Notice how we used a `BehaviorSubject` inside the service and "nexted" the instance of the directive into it. The reason for this is that the parent component, along with the provided service, will instantiate first, and the child component will instantiate later. Consequently, there will be a brief moment when the reference to the directive will be `undefined`. Whether or not this is a problem for you depends on when the tracker directive is being accessed. Using a `BehaviorSubject` eliminates this issue.

Now, we can update the parent smart component to provide the `UnsavedChangesTrackerService` and use the referenced directive from it:

```ts
@Component({
  selector: `app-smart-component`,
  standalone: true,
  imports: [CommonModule, FormComponent],
  providers: [UnsavedChangesTrackerService],
  template: `
    <app-form-component
      [formModel]="formModel$ | async"
      (save)="saveData($event)"
    >
    </app-form-component>
  `
})
export class SmartComponent {
  apiService = inject(ApiService);
  unsavedChangesTrackerService = inject(UnsavedChangesTrackerService);

  formModel$ = this.apiService.getData();

  saveData(data: FormModel) {
    this.apiService.save(data).pipe(
      // when data saved successfully we need to access the
      // UnsavedChangesTrackerDirective and call setInitialValue method
      withLatestFrom(this.unsavedChangesTrackerService.tracker$),
      tap(([_, tracker]) => {
        tracker.setInitialValue(data);
      })
    ).subscribe();
  }
}
```

## ⚡ Reusable solution

In the previous example, we figured out how to access a directive used in a child component from a parent component. We achieved this by using a service to register a reference to the directive. This was possible because we had access to the directive's code and could customize it according to our needs. But what if we don't own the directive that we need access?

In such cases, we can still employ a similar approach, but we must make the service more generic so that it can register any type of directive.

```ts
@Injectable()
export class RefTrackerService<T> {
  private ref$$ = new BehaviorSubject<T | undefined>(undefined);
  ref$ = this.ref$$.asObservable();

  private definedRef$ = this.ref$.pipe(
    filter((x): x is T => x != null)
  );

  setRef(ref: T) {
    this.ref$$.next(ref);
  }

  withDefinedRef<Source>(): OperatorFunction<Source, [Source, T]> {
    return (source$) =>
      source$.pipe(
        switchMap((val) => {
          return this.definedRef$.pipe(map((ref) => [val, ref] as [Source, T]));
        })
      );
  }
}
```

There are a few additional features in this version compared to the one we saw earlier, especially the `withDefinedRef` function. This function is quite similar to the built-in RxJS `withLatestFrom` function, but don't worry about it for now. There will be an example of how it's used later.

Now that we have a service, we need a method to set the reference to any directive in that service. Angular provides  a standard way to access references to directives or components from the template. In our case, we'll want to write something like this:

```html
<form
  unsavedChangesTracker
  #ref="unsavedChangesTracker"
  [trackRef]="ref"
  (ngSubmit)="saveForm()"
>
  <input [ngModel]="formModel.name" name="name" />

  <!-- other input controls removed for brievety -->

  <button type="submit">Save</button>
</form>
```

Notice the `#ref` variable. We're defining a variable called "ref" which will contain an instance of `UnsavedChangesTrackerDirective`.

Also, observe the `[trackRef]="ref"`. We'll create a custom directive that allows us to save that "ref" variable in the `RefTrackerService`. Here's the "trackRef" directive:

```ts
@Directive({
  selector: '[trackRef]',
  standalone: true,
})
export class TrackRefDirective<T = unknown> {
  private refTrackerService = inject(RefTrackerService);

  @Input({ required: true }) trackRef!: T;

  ngOnInit() {
    this.refTrackerService.setRef(this.trackRef);
  }
}
```

Now the parent component can be updated like so:

```ts
@Component({
  selector: 'app-smart-component',
  standalone: true,
  imports: [CommonModule, FormComponent],
  providers: [RefTrackerService],
  template: `
  <ng-container *ngIf="formModel$ | async as formModel">
    <app-form-component
      [formModel]="formModel"
      (save)="saveData($event)"
    >
    </app-form-component>
  </ng-container>
  `,
})
export class SmartComponent {
  apiService = inject(ApiService);
  refTrackerService = inject(RefTrackerService<UnsavedChangesTrackerDirective>)

  formModel$ = this.apiService.getData();

  saveData(data: FormModel) {
    this.apiService.saveData(data).pipe(
      this.refTrackerService.withDefinedRef(),
      tap(([_, unsavedChangesTrackerDirective]) => {
        unsavedChangesTrackerDirective.setInitialValue(data);
      })
    ).subscribe();
  }
}
```

The final working code can be found on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-squcfz?file=src%2Fmain.ts).

## ️⚙️ A note on the architecture

There's something I don't quite like about the code example above - the `saveData()` method and everything within it.

* The component contains logic for saving data, which makes the component much "smarter" than it needs to be, and this will make it harder to unit-test.
* Additionally, there is some imperative code in  the `subscribe()` call.

**To understand why I believe these are problematic areas, please read my next article: "Dumb Components Are Overrated" (coming soon).**