6
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-14

どんな時に起こるか

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

@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から分離することで循環参照を防ぐ。

図解すると
スクリーンショット 2019-12-13 14.47.43.png

DialogNameEnum

export enum DialogName {
  A,
  B
}

ParentComponent

<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>
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

@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

@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の検知

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などを駆使してスマートにやるための案待ってます。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2