Angular circular dependency between 2 components
Asked Answered
H

6

12

I'm developing an Angular 7 application which allows management of entities, for example Cars and Students.

The application components can be described by the following tree:

  • Cars
  • Cars/CreateCar (dialog)
  • Students
  • Students/CreateStudent (dialog)

While creating a Car in the CreateCar dialog, the user should be able to create and assign a new student as the owner of the Car using the CreateStudent dialog.

Similarly, while creating a Student in the CreateStudent dialog, the user should be able to create and assign a new car as a property of the Student using the CreateCar dialog.

When compiling, Angular displays: "WARNING in Circular dependency detected" and I understand that this should happen.

I've tried searching for patterns to solve this such as shared services but noone seems to work.

EDIT:

The relevant part of the constructor for both Dialogs:

constructor(
  private readonly matDialog: MatDialog
) {
}

Inside CreateStudent dialog, method that opens the CreateCar dialog:

createCar(): void {
  this.matDialog
    .open(CreateCarDialogComponent)
    .afterClosed().subscribe((car: Car) => {
      // Do something with car
    });
}

Inside CreateCar dialog, method that opens the CreateStudent dialog:

createStudent(): void {
  this.matDialog
    .open(CreateStudentDialogComponent)
    .afterClosed().subscribe((student: Student) => {
       // Do something with student
     });
}

Any suggestions on solving this?

Thank you

EDIT 2:

Demo here https://stackblitz.com/edit/angular-bbfs8k

(Stackblitz doesn't seem to display the compile warning)

compilation warning

Hydroxide answered 8/3, 2019 at 12:5 Comment(7)
Is Cars and Students are separate module?Gastrotomy
are you using barrel files? Is there more of that error message?Wadding
It's important that you show how you open the dialogs. I need to know if you're using a component factory, because that tells me how to fix this.Depoliti
@PankajPrakash No. They are in the same moduleSayres
@JoeyGough No. There are no more of these warnings.Sayres
@cgTag Edited the post with the code used for opening the dialogsSayres
Thanks. I'll post an answer when I get home if no one else has a right answer. It's very easy to fix.Depoliti
D
15

The MatDialog doesn't need a direct reference to the component declaration. You only need to pass a ComponentType<any> parameter to open a dialog. So we can resolve the circular dependency (which is triggered by TypeScript) by using the Angular dependency injector.

Create a file named create-card-token.ts and define an injection token.

export const CREATE_CAR_TOKEN: InjectionToken<ComponentType<any>> =
new InjectionToken<ComponentType<any>>('CREATE_CAR_TOKEN');

In your module define the value for the above token as a provider. This is where you define what component will be used for MatDialog.

@NgModule({
    ....
    providers: [
        {provide: CREATE_CAR_TOKEN, useValue: CreateCarComponent}
    ]
}) export class MyModule {}

In the CarComponent you can now inject this token and use it to open the dialog.

@Component({...})
export class CarComponent {
     public constructor(@Inject(CREATE_CAR_TOKEN) private component: ComponentType<any>,
                        private matDialog: MatDialog) {}

     public createCar() {
         this.matDialog
            .open(this.component)
            .afterClosed().subscribe((car: Car) => {
                // Do something with car
            });
     }       
}

This will resolve the circular dependency, because the CarComponent never needs to know the type decliartion of the CreateCarComponent. Instead, it only knows that a ComponentType<any> has been injected, and the MyModule defines what component will be used.

There is one other issue. The above example uses any as the component type that will be create. If you need to gain access to the dialog instance, and call methods directly from the CarComponent, then you can declare an interface type. The key is to keep the interface in a separate file. If you export the interface from the CreateCarComponent file you go back to having circular dependencies.

For example;

  export interface CreateCarInterface {
       doStuff();
  }

You then update the token to use the interface.

export const CREATE_CAR_TOKEN: InjectionToken<ComponentType<CreateCarInterface>> =
new InjectionToken<ComponentType<CreateCarInterface>>('CREATE_CAR_TOKEN');

You can then call doStuff() from the car component like so:

@Component({...})
export class CarComponent {
     public constructor(@Inject(CREATE_CAR_TOKEN) private component: ComponentType<CreateCarInterface>,
                        private matDialog: MatDialog) {}

     public createCar() {
         const ref = this.matDialog.open(this.component);
         ref.componentInstance.doStuff();
     }       
}

You can then implement the interface in the CreateCarComponent.

@Component({..})
export class CreateCarComponent implements CreateCarInterface {
      public doStuff() {
         console.log("stuff");
      }
}

These kinds of circular references happen often with MatDialog and the CDK portal, because we often need to have a service open the dialog, and then the dialog needs to use that same service for other reasons. I've had this happen many times.

Depoliti answered 9/3, 2019 at 0:59 Comment(3)
Thank you for the detailed answer. It sure seems like an interesting solution and I might use it somewhere else. However, it won't solve my problem because my situation is not the one you described in: "we often need to have a service open the dialog, and then the dialog needs to use that same service for other reasons"Sayres
My problem is that I have 2 dialog components that need to open one another. CreateCarComponent needs to open CreateStudentComponent and vice-versa. Using the solution you suggested, in my CreateCarComponent, I would need to provide the CREATE_STUDENT_TOKEN using the value of CreateStudentComponent. Similarly, in my CreateStudentComponent, I would need to provide the CREATE_CAR_TOKEN using the value of CreateCarComponent. Therefore, a circular dependency still existsSayres
@TomásLaw CREATE_STUDENT_TOKEN would never provide CreateStudentComponent ever. Those tokens provide only the ComponentType and an optional interface.Depoliti
H
2

Break circularities with a forward class reference (forwardRef)

The order of class declaration matters in TypeScript. You can't refer directly to a class until it's been defined.

This isn't usually a problem, especially if you adhere to the recommended one class per file rule. But sometimes circular references are unavoidable. You're in a bind when class 'A' refers to class 'B' and 'B' refers to 'A'. One of them has to be defined first.

The Angular forwardRef() function creates an indirect reference that Angular can resolve later.

The Parent Finder sample is full of circular class references that are impossible to break.

You face this dilemma when a class makes a reference to itself as does AlexComponent in its providers array. The providers array is a property of the @Component() decorator function which must appear above the class definition.

Break the circularity with forwardRef.

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

https://angular.io/guide/dependency-injection-in-action#break-circularities-with-a-forward-class-reference-forwardref

Holophrastic answered 27/3, 2020 at 15:6 Comment(0)
P
0

Suggestions:

  • You could add a flag which indicates, if the opened dialog is a parent or child. If child then the opened dialog doesn't other the create car/child dialog. It interupts the circle.
  • Prior to Create Car/Student dialog you should have a master (assignment manager) which can open those both dialogs. Within the dialogs there is no option to open another dialog.
Putty answered 8/3, 2019 at 12:52 Comment(2)
Yes interrupting the cycle to disable the possibility of having infinite dialogs is a runtime issue that can be solved using flags as you said. The problem is at compile time because both CreateCar and CreateStudent components reference each otherSayres
Having an assignment manager doesn't solve the circular dependency issue. AssignmentManager would need to reference both CreateCar and CreateStudent dialogs to be able to open them. Similarly, CreateCar and CreateStudent would need to inject AssignmentManager to be able to use it to open the new dialog.Sayres
K
0

I don't use the material but I found a solution using ng-bootstrap you can take the same idea and apply it to the material.

Basically I defined a variable in the modal called backComponent, and when I request to open the modal I define which is the backComponent, in ng-boostrap it would be like this:

open() {
   const createStudent = this.modalService.open(CreateStudentDialogComponent);
   createStudent.componentInstance.backComponent = CreateCarDialogComponent;
}

when I need to go back to the previous component like using a button to return to the modal for example I use this backComponent to return:

Kirven answered 6/8, 2020 at 13:44 Comment(0)
A
0

My Problem:

I have a table component and a button to open the dialog component of the table component. The import cycle error started to occur.

Solution

Instead of creating a separate component.ts file for the dialog component, I moved all of the dialog component.ts code into the table component. Multiple imports of the dialog component were removed and it fixed the problem.

Example

@Component({
  selector: 'table-component',
  templateUrl: 'table-component.html',
})
export class TableComponent {
  constructor(public dialog: MatDialog) {}

  openDialog(): void {
    const dialogRef = this.dialog.open(DialogComponent);

    dialogRef.afterClosed().subscribe(result => {
      console.log('The dialog was closed');
    });
  }
}

@Component({
  selector: 'dialog-component',
  templateUrl: 'dialog-component.html',
})
export class DialogComponent {
  constructor(
    public dialogRef: MatDialogRef<DialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: DialogData,
  ) {}

  onNoClick(): void {
    this.dialogRef.close();
  }
}
Annisannissa answered 2/6, 2022 at 12:25 Comment(0)
W
-1

create-car-dialog.component.ts imports create-student-dialog.component.ts which imports create-car-dialog.component.ts.

create-student-dialog.component.ts imports create-car-dialog.component.ts which imports create-student-dialog.component.ts.

That is what the error messages mean and there are your circular dependencies. You will need to rectify this in some way.

i.e. you need to stop this importing circle you have created. Perhaps you need a third component to import both of these. There is probably many ways to do it.

Wadding answered 8/3, 2019 at 16:38 Comment(2)
If that third component imports both of them, how would that third component be used in the CreateCarComponent or CreateStudentComponent without having a circular dependency?Sayres
There are many ways to achieve the same ends. if you want to inject parent component into children use the @Host decorator in the constructor. But don't do that... define createCar() and createPerson() in the parent component and pass those methods as properties into children.Wadding

© 2022 - 2024 — McMap. All rights reserved.