0

Let's say I have a component, <app-card> that encapsulates some information into a region on-screen. I'd like to use directives to individually add functionality as needed. For example, if I'd like the card to have a checkbox, I can simply apply the CardIsCheckableDirective to the component, like so:

<app-card [appCardIsCheckable]></app-card>

Now say I'd also like this component to be draggable and droppable, using mousedown and mouseup events, so I create a CardIsDraggableDirective, which can also be applied to the component host:

<app-card [appCardIsCheckable] [appCardIsDraggable]></app-card>

Unfortunately, the implementation details of the checkable directive and the draggable directive conflict, because of a need to distinguish between the click event and the mousedown/mouseup events, so it's not unreasonable to imagine the checkable directive modifies its behaviour when the draggable directive is present, and vice versa.

CardIsCheckableDirective can constructor inject CardIsDraggableDirective like so:

public constructor(@Optional() @Host() public cardIsDraggable: CardIsDraggableDirective) {}

If the cardIsDraggable property is not null, then the directive knows the other directive is also bound on the host component, and it can modify it's functionality. However, as soon as you perform the inverse injection, by providing CardIsCheckableDirective to CardIsDraggableDirective, Angular throws the following error:

NG0200: Circular dependency in DI detected for CardIsCheckableDirective. Find more at https://angular.io/errors/NG0200

This support article on Angular's website tacitly implies your architecture is incorrect by suggesting a refactoring of your service structure to break the circular dependency. However, this is two directives both simply needing knowledge of whether the opposing directive exists on the host component; which is in my view a reasonable request.

How can this circular dependency loop be broken while preserving the required functionality?

marked-down
  • 9,958
  • 22
  • 87
  • 150
  • Can you modify your question to include the directive's implementations? – John Sep 12 '21 at 08:44
  • @John The implementation details of the directives are not relevant to the question. – marked-down Sep 13 '21 at 00:36
  • I disagree, a shared service may or may not be an option depending on the implementation. "Unfortunately, the implementation details of the checkable directive and the draggable directive conflict", I'd like to know how. – John Sep 13 '21 at 07:29

1 Answers1

1

You can use injection tokens, declared in separate files. Given that you only check for the presence of these directives, and you don't need to interact with the instances themselves (in which case you would need interfaces (in separate files too, to avoid circular dependencies).

const DRAGGABLE = new InjectionToken('DRAGGABLE');
const CHECKABLE = new InjectionToken('CHECKABLE');

You can modify your directives like so:

@Directive({
  // ...
  providers: [
    {
      provide: CHECKABLE,
      useExisting: CardIsCheckableDirective,
    }
  ]
})
public class CardIsCheckableDirective {
  constructor(@Inject(DRAGGABLE) @Optional() @Host() public draggable: any) {}
}
@Directive({
  // ...
  providers: [
    {
      provide: DRAGGABLE,
      useExisting: CardIsDraggableDirective,
    }
  ]
})
public class CardIsDraggableDirective {
  constructor(@Inject(CHECKABLE) @Optional() @Host() public checkable: any) {}
}

It might also work if you put both injection tokens in the same file, as this should not lead to circular dependency.

Octavian Mărculescu
  • 4,312
  • 1
  • 16
  • 29