【ionic】@InputでObjectをバインドするといろいろハマる【Angular】

**注意: 自分用メモです。内容が間違っている可能性があります**

Ionic(Angular)で＠Inputを使った値の受け渡しについていろいろハマったのでメモしておきます。
バージョンは以下です

* Ionic v4.2.1
* ionic-angular 3.9.2
* Angular 5.2.11

また今回試したコードは以下です
https://github.com/kuwabataK/ionic-input-output-test

## バインディングなし

まず以下のような、`@input`を含まない値の受け渡しを含まない単純なコンポーネントを作ります。
当然ですが値がバインディングされていないのでそれぞれのコンポーネントは独立して動きます。


``` home.html


  <h1>
    {{text}}
  </h1>
  <button ion-button (click)="changeText()">HomePageのTextを変更</button>

  <my-child></my-child>

```


``` home.ts
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  text = "Hello Parent!!"

  constructor(public navCtrl: NavController) {

  }

  changeText(){
    this.text = "Changed Parent string object !!"
  }

}

```

``` my-child.html
    <h1>
      {{childText}}
    </h1>
    
    <button ion-button (click)="changeText()">Child ComponentのTextを変更</button>
```

``` my-child.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-child',
  templateUrl: 'my-child.html'
})
export class MyChildComponent {

  childText = "Hello Child!!"

  constructor() {
  }

  changeText() {
    this.childText = "Changed Child string object!!"
  }

}

```

この場合、当たり前ですが各コンポーネントの値は独立に変わります。

![not_binding.gif](https://qiita-image-store.s3.amazonaws.com/0/93999/96ba2ee1-0915-0f6d-cdd9-6c0e22241686.gif)

## プリミティブ型の受け渡し

`@Input`を使ってHomePageの変数をChild Componentにバインドします。具体的には以下のような感じです。


``` home.html


  <h1>
    {{text}}
  </h1>
  <button ion-button (click)="changeText()">HomePageのTextを変更</button>

  <my-child [childText]="text"></my-child>

```

``` my-child.ts
省略
  @Input() childText = "Hello Child!!"
…省略
```

子コンポーネントのバインディングしたい変数の頭に`@Input`をつけて親コンポーネント側で`[]`を使ってバインディングします。

挙動は以下のような感じですね。注意なければいけない点は、

* 双方向バインディングではないため、子コンポーネントの値を変更しても親コンポーネントの値は変更されない
* 親コンポーネントの値が変化しない限りは`@Input`経由での子コンポーネントの書き換えは行われない。

といったところでしょうか

![input_binding.gif](https://qiita-image-store.s3.amazonaws.com/0/93999/0f6079a1-d600-652a-8d73-b6028b0c772f.gif)

### オブジェクトをInputで挿入した場合

ここからが本題ですが、Inputの挙動はオブジェクトを受け渡しした場合に挙動が変わります。以下のようなコードを書いてみます

``` home.html


  <h1>
    {{objectText}}
  </h1>
  <button ion-button (click)="changeObject()">HomePageのObjectを変更</button>

  <my-child [childObject]="object"></my-child>

```

``` home.ts
  object = {
    text: "Hello Parent!!"
  }

　// 親コンポーネントのテキスト更新ボタンの中身
  changeObject() {
    this.object.text = "Changed Parent Object !!"
  }

```

``` my-child.html
  <h1>
    {{childObject.text}}
  </h1>
      
  <button ion-button (click)="changeObject()">Child ComponentのObjectを変更</button>
```

``` my-child.ts
省略
  @Input() childObject = {
    text: "Hello Child Object!!"
  }

  // 子コンポーネントのテキスト更新ボタンの中身
  changeObject() {
     this.childObject.text = "Changed Child Object!!"  
  }
…省略
```

こうしてみると・・・

![input_binding_object1.gif](https://qiita-image-store.s3.amazonaws.com/0/93999/53d9122f-593d-3639-ceee-c79a433b8379.gif)

えっ、なんか双方向バインディングされてるように見えるんですけど・・・、`@Output`も `()` も使ってないのに明らかに子コンポーネントの値で親コンポーネントの値が上書きされてます。

さらに,`changeObject()`の中身を以下のように書き換えると・・・

```home.ts
  changeObject() {
    this.object = {
      text: "Changed Parent Object !!"
    }
  }

```

```my-child.ts
  changeObject() {
    this.childObject = {
      text: "Changed Child Object!!"
    }
  }
```

![input_binding_object2.gif](https://qiita-image-store.s3.amazonaws.com/0/93999/dafbc211-b488-c8aa-02f5-7225aef7f1b3.gif)

おお？　今度は子コンポーネントの値を変更しても親コンポの値は変更されなくなりました。
・・・でも、string型をバインドしたときと違って、`HOMEPAGEのOBJECTを変更` ボタンが常に効いているような・・・

一体どういうことなんだ？？

## 解説

解説・・・というよりも今の自分の理解ですが、おそらく`@Input`は子コンポーネントに親コンポーネントの参照の値を渡してるんじゃないかと思います。なので

* 子コンポーネントで受け取ったオブジェクトのメンバーが変わると、親コンポーネントのオブジェクトのメンバーも変わる
* 受け取ったオブジェクト自体が別のオブジェクトで置き換わると、親と子のオブジェクトの参照先が変わるので、子コンポーネントのオブジェクトの値が代わっても親コンポーネントが変更されることはなくなる
* `this.object = {text: "Changed Parent Object !!"}`のような代入はたとえ中身が同じでも常に新しいオブジェクトが生成されて代入されるので、代入されたタイミングで、オブジェクトの参照先が変わり、`＠Input`経由で子コンポーネントに値が送られる

ということ・・・なのかなあ。
まあこのあたりは公式ドキュメントをちゃんと調べたわけではないし、javascript/typescriptの言語仕様に詳しいわけでもないので、正しいかどうかわかりません。

というか誰かAngular/Ionicに強い人教えてください・・・

ちなみに最初Ionic触ったときは、「オブジェクトを`@input`した場合は双方向バインディングが効く」と勘違いして書いてたので、「`@Input`だけでオブジェクトの双方向バインディングできるじゃん超便利〜」とか思って、`@Output`使わずに書いてました。

もちろんあとでバグがいっぱい出てきて死にました。みんなも気をつけましょう