1

I have a single service to open a number of dialogs, some of those dialogs can open other dialogs using the same service that opened them. I'm using a dynamic dialog service from PrimeNg to open a dialog component by Type<any>.

When referencing .ts file for the type I get WARNING in Circular dependency detected. Note that everything does still actually work, just warnings in the log which are ugly.

I get that I should be having issues if I was trying to inject the type into the service, but I'm just getting the type so that the dynamic dialog can instantiate it.

Is there a way to get around this?

test.component.ts

import { Component, OnInit } from '@angular/core';

import { OpenDialogService } from './open-dialog.service';

@Component({
  selector: 'test',
  template: `<dialog></dialog>`,
  styles: [],
  providers: []
})
export class TestComponent implements OnInit {

  constructor(private openDialogService: OpenDialogService) { }

  ngOnInit() { }

  public openOther() {
    this.openDialogService.openOtherDialog();
  }
}

open-dialog.service.ts

import { Injectable } from '@angular/core';

import { Observable, of, } from 'rxjs';

import { DialogService } from 'primeng/dynamicdialog';

import { TestComponent } from './test.component';

@Injectable({ providedIn: 'root' })
export class OpenDialogService {

  constructor(private dialogService: DialogService) {
  }

  public openTestDialog(): Observable<any> {
    return this.dialogService.open(TestComponent);
  }

  public openOtherDialog(): Observable<any> {
    return of(null);
  }
}

Output:

WARNING in Circular dependency detected:
src\app\open-dialog.service.ts -> src\app\test.component.ts -> src\app\open-dialog.service.ts

Because all the issues I'm finding when googling this are related to injecting in a loop I even went ahead and tried the injector delay in the test component:

...
export class TestComponent implements OnInit {
  private _openDialogService: OpenDialogService;
  private get openDialogService(): OpenDialogService {
    return this._openDialogService
      ? this._openDialogService
      : this._openDialogService = this.injector.get<OpenDialogService>(OpenDialogService);
  }

  constructor() { }
  ...
}

But of course it doesn't actually solve it, because this isn't related to dependency injection, just file referencing.

JWrightII
  • 942
  • 11
  • 26
  • If that's actually your dialog service, in which case it doesn't have any state of its own, you can provide it at the component level as well. `@Component(providers: [OpenDialogService]}) class TestComponent`. That ought to break the cycle – Aluan Haddad Sep 09 '20 at 23:57
  • @AluanHaddad Gave that a shot but no dice. It's got to be doing something magical though, because if you look at my last example where I use the injector to retrieve it that still breaks. But interestingly enough it doesn't throw the warning with just the property `private openDialogService: OpenDialogService;`, only if I actually try and resolve it with `injector.get(OpenDialogService);` in either the constructor or a getter. – JWrightII Sep 10 '20 at 00:14
  • Putting a type annotation on an undecorated property wouldn't impact to this. – Aluan Haddad Sep 10 '20 at 00:17

3 Answers3

1

I also had this issue, I had a service able to open a dialog, itself creating a component referring this service.

I fixed this by deferring the import of the component i.e. instead of importing it in the top of the file imports, I used the async import of typescript.

instead of this (simplified code):

import { MyDialogComponent } from 'myDialog.component';

constructor(private openDialogService: OpenDialogService)

showDialog(): void {
this.openDialogService.openDialog(MyDialogComponent);
}

I now do have this:


constructor(private openDialogService: OpenDialogService)

async showDialog(): Promise<void> {
// the component is lazy imported here
const { MyDialogComponent } = await import('myDialog.component');
this.openDialogService.openDialog(MyDialogComponent);
}

Of course the best solution is to avoid having any circular dependency (when possible) which usually means splitting monolithic services into several small one.

Another alternative is to dissociate the service opening dialogs from the other services adding a Mediator. B wait for the Mediator to tell to open a dialog and A calls the mediator to open a dialog. A and B never knows each other (which breaks the circular dependency).

Flavien Volken
  • 19,196
  • 12
  • 100
  • 133
0

You are importing TestComponent into the Service, and the Service into TestComponent.

You could pass the Component as param through to the function.

this.openDialogService.openDialog(TestComponent)
hyperdrive
  • 1,786
  • 5
  • 19
  • 33
  • It's true, but the point of the extra service was to wrap up all the loose components that were made to be spawned as dialogs and put them in a single service. Especially because a lot of them require a specific object to be passed to the `{ data: any }` property of PrimeNg's DialogService. So getting a single location with all the dialogs and their required data object as the method params so we don't have to look up what random "any" object has to be passed along with them. The worst part about this is that it all does work just fine, but I just really don't want those compiler warnings. – JWrightII Sep 10 '20 at 03:49
  • You could tell angular to not complain about circular dependencies in angular.json https://stackoverflow.com/questions/50798660/suppress-circular-dependency-detected-suppress-warning-in-angular-6 – j4rey Sep 10 '20 at 04:31
0

For anyone else trying to figure this out, ended up making a new file and filling it tokens:

dialog-tokens.ts

import { InjectionToken } from '@angular/core';

export const TEST_DIALOG: InjectionToken<string> = new InjectionToken<string>('TEST_DIALOG');
export const TEST_DIALOG2: InjectionToken<string> = new InjectionToken<string>('TEST_DIALOG2');
.
.

Then providing all the dialog component ComponentType's with those tokens at the module level:

  providers: [
    { provide: TEST_DIALOG, useValue: DynamicTestDialogComponent },
    { provide: TEST_DIALOG2, useValue: DynamicTestDialog2Component},
  .
  .

This has the unfortunate side effect of having to remove the {providedIn:'root'} from all services in the feature module and provide them at the same level beneath the tokens.

But then I could finally inject the component types into the service:

@Injectable({ providedIn: 'root' })
export class OpenDialogService {

  constructor(
  @Inject(TEST_DIALOG) private dynamicTestDialogComponent: ɵComponentType<any>,
  @Inject(TEST_DIALOG2) private dynamicTestDialog2Component: ɵComponentType<any>,
  ) { }

  public openTestDialog(): Observable<any> {
    return this.dialogService.open(this.dynamicTestDialogComponent);
  }

  public openOtherDialog(): Observable<any> {
    return this.dialogService.open(this.dynamicTestDialog2Component);
  }
}

No compiler warnings and everything works as expected. Not in love with it and miss being able to just provide it all to root, but for now this the best I've got.

JWrightII
  • 942
  • 11
  • 26