Riot.js v4 親/子のメソッドを簡単に呼べるプラグインを作った

[Riot.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/riotjs) の17日目が空いていたので埋めます。
何とか全部埋まりそうです。

## はじめに
Riot v4では子のメソッド、親のメソッドは簡単には呼べないようになっています。
前々回（[Riot.js v4 子コンポーネントを操作する](https://qiita.com/nekijak/items/dad31aeb541a468634af)）と前回（[Riot.js v4 内部プロパティまとめ](https://qiita.com/nekijak/items/ca3a5bc131729ebbc141)）でだいぶ仕組みがわかってきました。

今回はこれを簡単に扱えるようにプラグインを作りました。
プラグインには、[riot.install](https://riot.js.org/ja/api/#riotinstall)を使っています。
> riot.install(plugin: function): Set
plugin - 生成された任意のコンポーネントの コンポーネントオブジェクト を受け取る関数

## プラグイン

```javascript
riot.install(component => {
  // この時点ではまだない。
  //let parent = component[Object.getOwnPropertySymbols(component).find(symbol => symbol.toString() === "Symbol(parent)")];
  if (component.hasOwnProperty("mount")) {
    let value = component["mount"];
    // コンポーネントが持っているmountを上書き可能に
    Object.defineProperty(component, "mount", {
      value,
      enumerable: false,
      writable: true,
      configurable: true
    });

    // mountにhook
    let org_mount = component.mount;
    component.mount = function (element, state, parentScope) {
      // parentがある場合
      if (parentScope) {
        component._parent = parentScope;
        element._component = component
      }
      // オリジナルのmountを実行
      return org_mount.call(this, element, state, parentScope);
    };
  }
  return component;
});
```

## 解説

`riot.install`で動く際に引数として与えられる`component`には、まだ`mount`,`update`,`unmount`しかありません。
`mount`を実行すると`Symbol`で`component`のプロパティに与えられる。

`mount`にhookすることで`Symbol`でセットしている元の変数`parentScope`が取れる。

`parentScope`は親の`this`と同義なので、自身のコンポーネントの`_parent`プロパティにセット。(子->親 用)
そして`element`はマウントされたタグ要素なので、ここの`_component`プロパティに自身のコンポーネントをセット。(親->子 用)

`riot.install`より後にマウント実行したコンポーネントが対象になります。

## サンプル

```javascript:index.js
import { component, register, install } from 'riot'
import App from './app.riot'
import Child from './child.riot'
import Grandchild from './grandchild.riot'

install(component => {
  // この時点ではまだない。
  //let parent = component[Object.getOwnPropertySymbols(component).find(symbol => symbol.toString() === "Symbol(parent)")];
  if (component.hasOwnProperty("mount")) {
    let value = component["mount"];
    // コンポーネントが持っているmountを上書き可能に
    Object.defineProperty(component, "mount", {
      value,
      enumerable: false,
      writable: true,
      configurable: true
    });

    // mountにhook
    let org_mount = component.mount;
    component.mount = function (element, state, parentScope) {
      // parentがある場合
      if (parentScope) {
        component._parent = parentScope;
        element._component = component
      }
      // オリジナルのmountを実行
      return org_mount.call(this, element, state, parentScope);
    };
  }
  return component;
});

register('my-child', Child);
register('my-grandchild', Grandchild);
component(App)(document.getElementById('root'));
```
```html:app.riot
<my-app>
  <input type="button" value="Hello" onclick="{ call_greeting }">
  <input type="button" value="Goodbye" onclick="{ call_greeting }">
  <input type="button" value="Hola" onclick="{ direct_greeting }">
  <input type="button" value="Adios" onclick="{ direct_greeting }">
  <input type="button" value="Grandchild Change" onclick="{ call_grandchild }">
  <hr>
  <my-child message="First"></my-child>
  <hr>
  <my-child message="Second"></my-child>

  <script>
    export default {
      // 子コンポーネントのメソッドを呼び出し:対象->1つ目のmy-child
      call_greeting(e) {
        this.$("my-child")._component.greeting(e.target.value);
      },

      // 子コンポーネントのstateを直接変更:対象->2つ目のmy-child
      direct_greeting(e) {
        const child = this.$$("my-child")[1]._component;
        child.state.message = e.target.value;
        child.update();
      },

      // 孫も自分にとっては子みたいなもの:対象->2つとも書き換え
      call_grandchild(e) {
        this.$$("my-grandchild").map(elm => elm._component.greeting("Call Grandchild"))
      }
    }
  </script>
</my-app>
```
```html:child.riot
<my-child>
  <p>{ state.message } World!!</p>
  <input type="button" value="First Child Change" onclick="{ call_parent }">
  <my-grandchild message="Grandchild"></my-grandchild>

  <script>
    export default {
      state: {
        message: ""
      },

      onMounted(props, state) {
        this.greeting(props.message);
      },

      greeting(msg) {
        this.state.message = msg;
        this.update();
      },

      call_parent() {
        // 親をコール
        this._parent.call_greeting({"target":{"value": "Call Parent"}});
      }
    }
  </script>
</my-child>
```
```html:grandchild.riot
<my-grandchild>
  <p>{ state.message } World!!</p>
  <input type="button" value="Second Child Change" onclick="{ call_grandparent }">

  <script>
    export default {
      state: {
        message: ""
      },

      onMounted(props, state) {
        this.greeting(props.message);
      },

      greeting(msg) {
        this.state.message = msg;
        this.update();
      },

      call_grandparent() {
        // 親の親をコール
        this._parent._parent.direct_greeting({"target":{"value": "Call Grandparent"}});
      }
    }
  </script>
</my-grandchild>
```
![t_2019_12_17_154348(slt)(raw).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/a003861c-cc67-a2ae-9627-4522cb56f680.gif)

Riotの設計思想から外れるかもしれませんが、よくあるパターンなので今後作るものはこれを使っていこうと思います。