Componentの循環参照問題と現在の解決方法

### どんな時に起こるか

あるDialogを表示したいとする。DialogのComponentの中でAcomponent,Bcomponentの表示を動的に変化させたい。


```typescript
@Component({
})
export class  Acomponent implements OnInit {
  constructor(private dialog: Dialog) {

  }

  open() {
    /// AcomponentでDialogを経由してBcomponentを呼び出す
    this.dialog.open(Bcomponent)
  }
}


@Component({
})
export class  Bcomponent implements OnInit {
  constructor(private dialog: Dialog) {

  }

  open() {
    /// BcomponentでDialogを経由してAcomponentを呼び出す
    this.dialog.open(Acomponent)
  }
}
```

この時、compile時に以下のようなwarningが表示される。

```
WARNING in Circular dependency detected: 
a-component.ts -> dialog.ts -> b-component.ts

WARNING in Circular dependency detected: 
b-component.ts -> dialog.ts -> a-component.ts
```

### 解決方法
1. ParentDialogにAcomponentとBcomponentを仕込む。
2. 子Componentの切り替えは、Outputを通して行う。
3. Dialog自体のcloseはService経由で行う
4. enumでParentDialogComponentのtsFileから分離することで循環参照を防ぐ。

図解すると
<img width="1002" alt="スクリーンショット 2019-12-13 14.47.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/220683/45750d7c-8f2d-79b9-7ba3-f4916505772f.png">


### DialogNameEnum
```typescript
export enum DialogName {
  A,
  B
}
```

#### ParentComponent
```html
<app-a-component *ngIf="componentList[0].active" (toDialog)="openDialog($event)"></app-a-component>
<app-b-component *ngIf="componentList[1].active" (toDialog)="openDialog($event)"></app-b-component>
```

```typescript
interface DialogList {
  name: DialogName;
  component: any;
  active: boolean;
}

export interface ParentDialogData {
  dialogName: DialogName;
}

@Component({
  selector: 'app--parent-dialog',
  templateUrl: './parent-dialog.component.html',
  styleUrls: ['./parent-dialog.component.scss'],
})
export class ParentDialogComponent implements OnInit, OnDestroy {
  loading$: Observable<boolean>;
  private dialogCloseSubscription: Subscription;
  componentList: Array<DialogList> = [
    {
      active: true,
      name: DialogName.A,
      component: AComponent,
    },
    {
      active: false,
      name: DialogName.B,
      component: BComponent,
    },
  ];
  constructor(
    @Inject(MAT_DIALOG_DATA) public data: ParentDialogData,
    public readonly matDialogRef: MatDialogRef<ParentDialogComponent>,
    private dialogService: DialogCloseService,
  ) {
    this.query.selectLoading();
  }

  ngOnInit() {
    this.openDialog(this.data.dialogName);
    this.dialogCloseSubscription = this.dialogService.sharedDataSource$.subscribe((msg) => {
      if (msg === 'close') {
        this.matDialogRef.close();
      }
    });
  }

  openDialog(name: DialogName) {
    this.componentList.forEach((c) => (c.active = false));
    this.componentList.find((c) => c.name === name).active = true;
  }

  close() {
    this.matDialogRef.close();
  }

  ngOnDestroy(): void {
    this.dialogCloseSubscription.unsubscribe();
  }
}

```

### Acomponent

```typescript
@Component({
  selector: 'app-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss'],
  changeDetection: ChangeDetectionStrategy.Default,
})
export class AComponent implements OnInit {
  @Output() toDialog: EventEmitter<DialogName> = new EventEmitter();

  constructor(
    private readonly dialog: DialogCloseService,
  ) {}

  ngOnInit() {
  }

  toBDialog() {
    this.toDialog.emit(DialogName.B);
  }

  closeDialog() {
    this.dialog.close();
  }
```



### Bcomponent

```typescript
@Component({
  selector: 'app-b',
  templateUrl: './b.component.html',
  styleUrls: ['./b.component.scss'],
  changeDetection: ChangeDetectionStrategy.Default,
})
export class BComponent implements OnInit {
  @Output() toDialog: EventEmitter<DialogName> = new EventEmitter();

  constructor(
    private readonly dialog: DialogCloseService,
  ) {}

  ngOnInit() {
  }

  toADialog() {
    this.toDialog.emit(DialogName.A);
  }

  closeDialog() {
    this.dialog.close();
  }
```

### CloseService
Subject経由でcloseの検知

```typescript
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class DialogCloseService {
  constructor() {}
  private sharedDataSource = new Subject<string>();
  public sharedDataSource$ = this.sharedDataSource.asObservable();

  close() {
    this.sharedDataSource.next('close');
  }
}
```

### まとめ
デメリットとしては切り替えるcomponentが増えるたびにParentComponentを修正しなきゃいけないところ。
ngComponentOutletなどを駆使してスマートにやるための案待ってます。