問題
angular バージョン
親子関係にあるchild-conponentとparent-componentがあり、さらにparent-componentと親子関係にあるgrand-parentがあるとします。
- grand-parent-component
- parent-component
- child-component
この3コンポーネント間でプリミティブ型のデータを双方向バインディングする場合、childコンポーネントのデータの変化がgrand-parentコンポーネントまで伝わらないという現象に遭遇しました。
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-child',
template: `<input type="checkbox" [(ngModel)]="flag"> <p>child: {{flag}}</p>`,
})
export class ChildComponent {
@Input() flag: boolean;
@Output() flagChange: EventEmitter<boolean> = new EventEmitter();
}
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-parent',
template: `<app-child [(flag)]="flag"></app-child> <p>parent: {{flag}}</p>`,
})
export class ParentComponent {
@Input() flag: boolean;
@Output() flagChange: EventEmitter<boolean> = new EventEmitter();
}
import { Component } from '@angular/core';
@Component({
selector: 'app-grand-parent',
template: `<app-parent [(flag)]="flag"></app-parent> <p>grandParent: {{flag}}</p>`,
})
export class GrandParentComponent {
flag: boolean = false;
}
次のようにflagChangeをemitする場合、parentコンポーネントまでは伝わります。
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-child',
template: `<input type="checkbox" [(ngModel)]="flag" (click)="onFlagChange()"> <p>child: {{flag}}</p>`,
})
export class ChildComponent {
@Input() flag: boolean;
@Output() flagChange: EventEmitter<boolean> = new EventEmitter();
onFlagChange() {
this.flag = !this.flag;
this.flagChange.emit(this.flag);
}
}
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-parent',
template: `<app-child [(flag)]="flag"></app-child> <p>parent: {{flag}}</p>`,
})
export class ParentComponent {
@Input() flag: boolean;
@Output() flagChange: EventEmitter<boolean> = new EventEmitter();
}
import { Component } from '@angular/core';
@Component({
selector: 'app-grand-parent',
template: `<app-parent [(flag)]="flag"></app-parent> <p>grandParent: {{flag}}</p>`,
})
export class GrandParentComponent {
flag: boolean = false;
}
しかしこの方法ではgrand-parent-componentまではデータが伝わりません。
解決策
プリミティブ型ではなくオブジェクトのプロパティとしてプリミティブ型を渡してあげる。
つまりこのようにする。
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Flag } from 'src/app/flag';
@Component({
selector: 'app-child',
template: `<input type="checkbox" [(ngModel)]="flag.isSelected"> <p>child: {{flag.isSelected}}</p>`,
})
export class ChildComponent {
@Input() flag: Flag;
@Output() flagChange: EventEmitter<boolean> = new EventEmitter();
}
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Flag } from 'src/app/flag';
@Component({
selector: 'app-parent',
template: `<app-child [(flag)]="flag"></app-child> <p>parent: {{flag.isSelected}}</p>`,
})
export class ParentComponent {
@Input() flag: Flag;
@Output() flagChange: EventEmitter<boolean> = new EventEmitter();
}
import { Component } from '@angular/core';
import { Flag } from 'src/app/flag';
@Component({
selector: 'app-grand-parent',
template: `<app-parent [(flag)]="flag"></app-parent> <p>grandParent: {{flag.isSelected}}</p>`,
})
export class GrandParentComponent {
flag: Flag = { isSelected: false };
}
なぜこのような現象が起こるのか
こちらの記事ではangularjsですが同じような問題を解説しており、javascriptの評価戦略(ある式をどのような手順で評価するかの規則)が関係していると述べております。
javascriptの評価戦略ではpremitive型の変数は参照元から渡されるのではなく、同じ変数を再び作ってしまいます。オブジェクトのみが参照元から渡されます。
こちらの記事でもngModelはprimitive型のデータではなくオブジェクトにバインドすることを推奨しております。