angularcircular-dependencyangular-module

Solve circular dependency by "overloading" modules in Angular


I am developing part of an application where we have some custom built generic components. One of these component is a GenericForm which we use for easily creating forms, and another component is ClientAutocomplete, which is a field for searching existing clients, with an embedded button that opens a dialog with a GenericForm for quickly creating a new client if it doesn't exist.

The GenericFormComponent takes in an array of field configs, in which they specify the type of field amongst other things. I would like this GenericFormComponent to be able to reuse the ClientAutocompleteComponent, but that creates a circular dependency:

GenericFormModule -> ClientAutocompleteModule -> ClientCreationFormModule -> GenericFormModule

The issue would be solved if the ClientCreationFormModule could import the GenericFormModule without it then having to import the ClientAutocompleteModule again, since it will never be used in the ClientCreationFormComponent. My first idea was to create an alternate GenericFormModule, that imported everything BUT the ClientAutocompleteModule, and import that, but that would mean exporting the component in two Angular modules, which does not compile. Even if it did, it technically would break the GenericFormComponent, but because in that case we would not use the client autocomplete field, the ClientAutocompleteComponent would never actually be used, so it would not cause trouble.

Is there a way to achieve this?


GenericFormModule

// ...

@NgModule({
  declarations: [
    GenericFormComponent,
  ],
  imports: [
    // ...
    AutocompleteClientModule,
  ],
  exports: [GenericFormComponent],
})
export class GenericFormModule { }

GenericFormComponent

<form *ngIf="formGroup" [formGroup]="formGroup" class="form">
  <div class="is-flex is-flex-wrap-wrap">
    <ng-container *ngFor="let field of config; let index = index; trackBy: indexTrackFn">
      <ng-container [ngSwitch]="field.type">
        <!-- ... -->
        <ng-container *ngSwitchCase="genericInputType.MULTIPLE_INPUT_AUTOCOMPLETE">
          <!-- ...other field types -->
        </ng-container>

        <ng-container *ngSwitchCase="genericInputType.USER_AUTOCOMPLETE">
          <!-- ...field content -->
        </ng-container>

        <ng-container *ngSwitchCase="genericInputType.PHONE">
          <!-- ...field content -->
        </ng-container>

        <ng-container *ngSwitchCase="genericInputType.CLIENT_AUTOCOMPLETE">
          <app-autocomplete-client></app-autocomplete-client>
        </ng-container>

        <ng-container *ngSwitchDefault>
        </ng-container>
      </ng-container>
    </ng-container>
  </div>
</form>

ClientAutocompleteModule

// ...
@NgModule({
  declarations: [
    ClientAutocompleteComponent,
  ],
  imports: [
    // ...
    ClientCreationFormModule,
  ],
  exports: [
    ClientAutocompleteComponent,
  ],
})
export class ClientAutocompleteModule { }

ClientAutocompleteComponent

<input-autocomplete
(onClickPrefix)="openDialog()"> <!-- This opens a dialog with the ClientCreationFormComponent -->
</input-autocomplete>

ClientCreationFormModule

// ...

@NgModule({
  declarations: [
    ClientCreationFormComponent,
  ],
  imports: [
    // ...
    GenericFormModule,
  ],
})
export class ClientCreationFormModule {
}

ClientCreationFormComponent

<!-- this instance of the GenericFormComponent is guaranteed to never use the ClientAutocomplete field -->
<app-generic-form [formGroup]="formGroup" [config]="formConfig"></app-generic-form>

The code snippets provided are not a complete MRE for brevity's sake, but I think it's enough to illustrate the situation


Solution

  • You need to declare the dependencies in the GenericFormModule and add them to the exports array also. Then the module will use what it needs from the exports array, since the contents of the exports array are visible to other modules.

    @NgModule({
      declarations: [
        GenericFormComponent,
        ClientAutocompleteComponent, // <- notice!
      ],
      imports: [], // <- notice!
      exports: [
        GenericFormComponent,
        ClientAutocompleteComponent, // <- notice!
      ],
    })
    export class GenericFormModule { }
    

    By doing this the circular dependency is broken and just import GenericFormModule in any module and it should work fine.

    ClientAutocompleteModule

    // ...
    @NgModule({
      declarations: [], // <- notice!
      imports: [
        GenericFormModule, // <- notice!
        ClientCreationFormModule,
      ],
      exports: [], // <- notice!
    })
    export class ClientAutocompleteModule { }
    

    ClientCreationFormModule

    // ...
    
    @NgModule({
      declarations: [
        ClientCreationFormComponent,
      ],
      imports: [
        GenericFormModule,
      ],
    })
    export class ClientCreationFormModule {}