Riot.js Advent Calendar 2019 の17日目が空いていたので埋めます。
何とか全部埋まりそうです。
はじめに
Riot v4では子のメソッド、親のメソッドは簡単には呼べないようになっています。
前々回(Riot.js v4 子コンポーネントを操作する)と前回(Riot.js v4 内部プロパティまとめ)でだいぶ仕組みがわかってきました。
今回はこれを簡単に扱えるようにプラグインを作りました。
プラグインには、riot.installを使っています。
riot.install(plugin: function): Set
plugin - 生成された任意のコンポーネントの コンポーネントオブジェクト を受け取る関数
プラグイン
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
より後にマウント実行したコンポーネントが対象になります。
サンプル
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'));
<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>
<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>
<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>
Riotの設計思想から外れるかもしれませんが、よくあるパターンなので今後作るものはこれを使っていこうと思います。