6

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.

Ctfrancia
  • 1,248
  • 1
  • 15
  • 39

3 Answers3

3

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.

Luis
  • 866
  • 4
  • 7
1

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)

bryan60
  • 28,215
  • 4
  • 48
  • 65
  • great! Thanks for the input I'll have to give this a second, twice, five time read to understand all the wrapper logic and proper way to do this all. I'll be back to let you know how it is going along and which one I went for – Ctfrancia Nov 14 '19 at 16:23
  • Worked like a charm! Thanks so much!! I'm going to update post with how I refactored – Ctfrancia Nov 14 '19 at 17:29
  • glad to hear it! let me know if there are any further questions or issues – bryan60 Nov 14 '19 at 17:31
0

Create a modal service that knows both components.

 ModalService {
     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,
     });

     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();
    } 
  }

Inject the service in both components.

You might want to check multiple instance services so that you can have a new service every time it's getting injected.

Now the two components don't know each other, thus you have no circular dependency.

In order for the warning to go away, you should inject via the injector in the components

private modalService: ModalService;
public constructor(injector:Injector) {
    this.modalService = injector.get(modalService);
}
Athanasios Kataras
  • 25,191
  • 4
  • 32
  • 61
  • my guess is that this will just add another level of indirection to dependency – Normunds Kalnberzins Nov 07 '19 at 13:11
  • alright, I'll give that a try after lunch and I'll let you know what I did whether it worked as expected or not or if there was a different solution I found! Thanks a lot for the input – Ctfrancia Nov 07 '19 at 13:12
  • Why would it? No component will be coupled with the other. It's just one component creating a modal – Athanasios Kataras Nov 07 '19 at 13:12
  • hm, trying to do this gives me the error: `Cannot access 'ModalService' before initialization` I see the stack trace but it looks like I have a lot of detective work ahead of me at this moment – Ctfrancia Nov 07 '19 at 15:33
  • the reason was because I have a SharedModule, and forgot to put one component in it, So the component using the Modal Service was getting declared before the Shared Module was getting declared. – Ctfrancia Nov 07 '19 at 15:45
  • unfortunately not. Now I have a triangle of circular dependency. I am going to update what I have – Ctfrancia Nov 07 '19 at 16:01
  • Can you post a stackblitz with a relevant example? It would help a lot to find the solution. – Athanasios Kataras Nov 07 '19 at 16:04
  • sorry I can't provide a StackBlitz, I've been trying to. but it is currently (StackBlitz) bugged with Ionic 4.. However[here is a gist](https://gist.github.com/ctfrancia/5ce346b3ee228eb1d75b3306f701c71d) – Ctfrancia Nov 07 '19 at 16:37
  • Still having no luck with it. I created a shared module, that imports and provides all the services as root, but still nothing, still the same error: I am updating the post – Ctfrancia Nov 14 '19 at 15:04
  • this won't work. you can't import the components to the service and then the service to the components. that's a circle, you've just expanded the circle a little. Your options are to instantiate the modal in a parent wrapper that the child components are entirely unaware of, or to have multiple services – bryan60 Nov 14 '19 at 16:29