Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular circular dependency warning

tl;dr: scroll down to solution

I have a circular dependency and I am getting a warning, rightfully so, however, I am managing it. The issue is that I have a chat component. In the corner you can select to see their profile page, while in their profile page you have the option to send them a message, hence the circular dependency. I am managing this by

chat.component

public async openProfile(): Promise<void> {
  this.modalCtrl.dismiss(); //closing the chat component before opening the profile modal
  const profile = await this.modalCtrl.create({
    component: ProfileComponent,
  });
  await profile.present();
} 

profile.component

public async openChat(): Promise<void> {
  this.modalCtrl.dismiss(); //closing the profile component before opening the chat modal
  const chat = await this.modalCtrl.create({
    component: ProfileComponent,
  });
  await chat.present();
} 

Is there an easier way of handling this circular dependency?

UPDATE: as per the suggestion below I tried creating a service. However now I have a three way dependency circle:

chat.component

private modalService: ModalService;

constructor(modalService: ModalService){
  this.modalService = modalService
}

public async openProfile(): Promise<void> {
  this.modalService.openProfile(this.userData);
} 

profile.component

private modalService: ModalService;

constructor(modalService: ModalService){
  this.modalService = modalService
}

public async openChat(): Promise<void> {
  this.modalService.openChat(this.userData);
}

modal.service

import { ModalController } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { ProfileComponent } from '../../components/profile/profile.component';
import { ChatComponent } from '../../components/chat/chat.component';
import { UserData } from '../../interfaces/UserData/userData.interface';

@Injectable({
  providedIn: 'root',
})
export class ModalService {
  private modal: ModalController;
  public constructor(modal: ModalController) {
    this.modal = modal;
  }

  public async openProfileComponent(user: UserData): Promise<void> {
    this.modal.dismiss();
    const profile = await this.modal.create({
      component: ProfileComponent,
      componentProps: {
        contact: user,
      },
    });

    await profile.present();
  }

  public async openChatComponent(user: UserData): Promise<void> {
    this.modal.dismiss();
    const chat = await this.modal.create({
      component: ChatComponent,
      componentProps: {
        contact: user,
      },
    });

    await chat.present();
  }

  public close(): void {
    this.modal.dismiss();
  }
}

UPDATE Stackblitz is too unstable with Ionic 4 so I can't replicate on it so here is a gist with the information and related code.

UPDATE2 I took the advice mentioned in answers but still getting the error. In order to do that, I created a shared.module.ts that looks like this:

import { UserService } from './componentServices/user/user.service';
import { ModalService } from './componentServices/modal/modal.service';
import { AuthenticationSecurityService } from './componentServices/auth_security/authentication-security.service';
import { AuthGuardService } from '../_guards/auth-guard.service';
import { ApiService } from './componentServices/api/api.service';
import { ChatService } from './components/chat/socketIO/chat.service';

@NgModule({
  imports: [CommonModule, ReactiveFormsModule, IonicModule.forRoot(), FormsModule, IonicModule],
  declarations: [
    // various components
  ],
  exports: [
    // various components and common modules
  ],
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [
        UserService,
        ModalService,
        DashboardService,
        AuthenticationSecurityService,
        AuthGuardService,
        ApiService,
        ChatService,
      ],
    };
  }
}

app.module.ts

imports: [
    SharedModule.forRoot(),
]
client:135 Circular dependency detected:
src/sharedModules/componentServices/modal/modal.service.ts -> src/sharedModules/components/profile/profile.component.ts -> src/sharedModules/componentServices/modal/modal.service.ts

client:135 Circular dependency detected:
src/sharedModules/components/chat/chat.component.ts -> src/sharedModules/components/search/search.component.ts -> src/sharedModules/components/profile/profile.component.ts -> src/sharedModules/componentServices/modal/modal.service.ts -> src/sharedModules/components/chat/chat.component.ts

client:135 Circular dependency detected:
src/sharedModules/components/profile/profile.component.ts -> src/sharedModules/componentServices/modal/modal.service.ts -> src/sharedModules/components/profile/profile.component.ts

client:135 Circular dependency detected:
src/sharedModules/components/search/search.component.ts -> src/sharedModules/components/profile/profile.component.ts -> src/sharedModules/componentServices/modal/modal.service.ts -> src/sharedModules/components/chat/chat.component.ts -> src/sharedModules/components/search/search.component.ts

SOLUTION

as @bryan60 and @Luis said there has to be a buffer, so what I did was follow the emitting route that they both had suggested. Bryan gives more code look like, where Luis gives a great responsibility summary. here is how I refactored it:

app.component.ts

  public initializeApp(): void {
    this.platform.ready().then((): void => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
      this._subToObservables();
    });
  }

  private _subToObservables(): void {
    this.modalService.openModal$.subscribe(
      async (e: ModalEmitInterface): Promise<void> => {
        const { command, data } = e;
        switch (command) {
          case 'open-profile':
            const profile = await this.modalCtrl.create({
              component: ProfileComponent,
              componentProps: {
                contact: data,
              },
            });
            await profile.present();
            break;

          case 'open-chat':
            // same as above
            break;

          default:
            break;
        }
      },
    );
  }

modalSignal.service.ts

export class ModalService {
  private openModalSubject: Subject<ModalEmitInterface> = new Subject<ModalEmitInterface>();
  public readonly openModal$: Observable<ModalEmitInterface> = this.openModalSubject.asObservable();

  private emitPayload: ModalEmitInterface;
  public openProfileComponent(user: UserData): void {
    this.emitPayload = {
      command: 'open-profile',
      data: user,
    };
    this.openModalSubject.next(this.emitPayload);
  }

  // repeat for others
}

chat.component.html

<button (click)="openProfile(user)">click me</button>

chat.component.ts

export class ChatComponent {
  public constructor(private modalSignal: ModalService){}

  private openProfile(user: UserData): void {
    this.modalSignal.openProfileComponent(user);
  }
}

thats it, although you still need to make sure that you are closing the modals or they will continue to stack.

like image 338
Ctfrancia Avatar asked Nov 07 '19 12:11

Ctfrancia


2 Answers

Been in that situation a couple of times. I end up with the same solution everytime and it scaled very well for me, so here it goes.

You will need a service (as suggested by others), but also an, let's call it, impartial player. The idea is to use the service as the communication/messaging buffer between the two interdependent components to help break the cross reference. For the sake of an example, let's assume the "App.Component".

The component and responsibilities:

Modal.Service: Receives messages to execute actions. It could be through single method receiving a string or object indicating the action or multiple methods for every action. Implementation details is up to you.

App.Component: Gets the ModalService injected and subscribes to the message event. Based on the action message, then activates the corresponding modal.

Chat.Component: Gets the Modal.Service injected and sends a message indicating the action to be performed, i.e. show the profile.

Profile.Component: Gets the Modal.Service injected and sends a message indicating the action to be performed, i.e. send a message.

This scales very well and the service can be used as a communication buffer between several other modules and/or components.

like image 105
Luis Avatar answered Sep 20 '22 14:09

Luis


it's kind of annoying but you need wrappers or multiple services. Single service won't work as you've seen because clearly you can't import your component into the service and then the service into your component. That's just a slightly bigger circle.

approach 1 is multiple services, doesn't scale great. Create a ChatModalService and a ProfileModalService and inject into their opposites. Pretty straight forward and will work if you're not doing this too much.

approach 2 is a little nicer IMO. putting page wrappers around your components that handle the modal calls and you can keep your single service approach.

create page wrapper components like this:

@Component({
  template: `<profile (openChat)="openChat()></profile>`
})
export class ProfilePageComponent {
   openChat() {
     // call your service or what have you here
   }
}

create a similar set up for the chat component and change your profile / chat components to just emit rather than call a service (or just put the buttons for calling the modals in the wrappers). Hopefully you don't have this two way modal relationship too often. but this works because the wrappers aren't imported into the components, you route to the page wrappers, but the page wrappers instantiate the components in the modals. Scales a bit better but still not ideal. The big benefit here is that while developing this app, you may find more benefits to having a page wrapper around your components if a given component can appear as a page or as a modal, since sometimes you want a component to sit in it's context differently. If you foresee a benefit to this, then take this approach. Conversely, you also could wrap your components in Modal wrappers, and instantiate those instead of the components directly. The import logic is the same and it works for the same reasons and gives the same context benefits but on the other side.

a third option is similar, set up a generic page wrapper, and change your modal service a bit so that it's just an event bus to the shared generic page wrapper. This works for the same reasons as above, and scales better, but the drawback is that you don't get the benefit of adding context to your components in the same way.

@Injectable()
export class ModalSignalService{
  private openChatSubject = new Subject()
  openChat$ = this.opopenChatSubject.asObservable()
  openChat() {
    this.openChatSubject.next()
  }
  private openProfileSubject = new Subject()
  openProfile$ = this.openProfileSubject.asObservable()
  openProfile() {
    this.openProfileSubject.next()
  }
}

then have a shared page wrapper component subscribe to the streams and handle the modal instantiation

@Component({
  template: `<router-outlet></router-outlet>` // something like this and set up routing with components as child routes
})
export class PageWrapperComponet {

  constructor(private modalSignalService: ModalSignalService) {
    this.modalSignalService.openChat$.subscribe(e => this.openChatModal()) // open modal logic here
    this.modalSignalService.openProfile$.subscribe(e => this.openProfileModal())
  }
}

If you foresee this issue coming up again and again, solve it once and for all like this. You may already even have one (you definitely have an app component which is a candidate for doing this, though maybe not the best one)

like image 38
bryan60 Avatar answered Sep 18 '22 14:09

bryan60