プログラミング/riot.js
Riot 4 では大幅に内容が変わったそうです†
チートシート的な覚書。
コンポーネントの書き方の基本†
ユーザー定義のタグやプロパティにはハイフンを含めるのが本来の流儀。含めなくても動くけど。
LANG:html <some-tag> <!-- { } の中では this. が補完される --> <div foo={ state.brabra } onclick={ ()=> any_command() }> { any javascript code } <slot any-prop={ any } /> <!-- タグ内に記述された内容が入る --> { props.someProp } <input id="input1" onchange={ onChangeValue }></input> </div> <style type="scss"> :host { /* this.root に適用される */ } div { /* this.root div に適用される */ } </style> <script> import * as Validator from 'validatorjs'; export default { // システムコールバック onBeforeMount(props, state) { // props や state は this.props, this.state そのものなのでメンバーへの代入も可 // this.state = { a: b } のように書き換えると this.state !== state になってしまうので注意 Object.assign(state, { brabra: default_value }) // this.root で <some-tag> の DOM を参照できる // this.props.someProp で <some-tag some-prop="value"> の "value" を得られる // this.state にコンポーネントの状態変数を入れる // this.update({prop: value}) で state.prop = value をした上で DOM が書き換えられる // this.$("any selector") で this.root 以下に querySelector できる // this.$$("any selector") で this.root 以下に querySelectorAll できる }, onMounted(props, state) { }, onBeforeUpdate(props, state) { // this.state の内容を元に DOM が書き換えられる前に呼ばれる // バリデーションなどをするのに良い場所 const rule = { // rules for validatorjs }; state.validation = new Validator(state, rule); state.validation.passes(); // check! for(let id of Object.keys(rule)) { const errors = state.validation.errors.errors[id]; this.$('#'+id).classList.toggle('has-error', !!errors); if(errors) { this.$('#'+id+'-errors').innerText = errors.join('<br>'); if(this.lastState && this.lastState[id]) state[id] = this.lastState[id]; // 直前の値に戻す } } }, onUpdated(props, state) { this.lastState = [...state]; // 直前の値を取っておく }, onBeforeUnmount(props, state) { }, // イベントハンドラの例 onChangeValue(e) { // input 要素の id 名の要素に value を代入して update this.update({[e.target.id]: e.target.value}); }, // 汎用に使えるイベントディスパッチ用ヘルパー // dispatch("click", {any: data}) のように呼ぶことで、 // <some-tag on-click={ clickHandler }> として与えられたイベントリスナを呼び出す // onclick ではなく on-click であることに注意 dispatchEvent(event, detail) { const camelCase = (str) => { return str.split('-').map((w,i) => (i === 0) ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() ).join('') } const handler = this.props[camelCase("on-" + event)]; if(!handler) return; handler(new CustomEvent("name", {detail: data})); }, } </script> </some-tag>
タグの呼び出し側では以下のようにして使う
<tag any-prop="value" on-custom-event={customEventHandler}> ここに書かれた内容が slot に入る。 <span>{anyProp}</span> のようにして渡されたプロパティを使える。 </tag>
this.props.anyProp や this.props.onCustomEvent に値が入る。
コンポーネントからコールバックを受けるためのイベントハンドラ†
タグからコールバックを受けるには上記のように on-custom-event みたいなイベントリスナーを渡して、 タグ内で dispatchEvent("custom-event", anyData) とすれば、以下のようにデータを受け取れる。
customEventHandler(e) { // e.detail に anyData が入っている }
コンポーネント側のメソッドを呼びたい場合†
逆にタグの呼び出し側からコンポーネント側のメソッドを呼びたいときは、 props 経由でインターフェースを共有するのが良さそう?
呼び出し側から intf というプロパティを与えておいて、その中に function を返してもらう。
<parent-tag> <some-tag intf={someTagIntf}> </some-tag> <script> export default { onBeforeMount(props, state) { const someTagIntf = { someFunc: null // some-tag 側で値を設定する } }, anyMemberFunction() { someTagIntf.someFunc(); // some-tag 内のメソッドを呼び出せる }, } </script> </parent-tag>
タグ側では、
<some-tag> <script> export default { onBeforeUpdate(props, state) { props.intf.someFunc = this.someFunc; }, someFunc(arg) { // parent-tag から someTagIntf.someFunc として呼び出せる }, } </script> </some-tag>
みたいな。
プラグインのインストール†
上記の dispachEvent などは非常に汎用的に使えるので、すべてのコンポーネントに定義してしまいたくなる。
riot.install( (component) => { component.camelCase = (str) => { return str.split('-').map((w,i) => (i === 0) ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() ).join('') } component.kebabCase = (str) => { return str.split(/(?=[A-Z])/).join('-').toLowerCase() } component.dispatchEvent = (name, data) => { const handler = component.props[component.camelCase("on-" + name)]; if(!handler) return; handler(new CustomEvent("name", {detail: data})); } return component; })
のようにすると、すべてのコンポーネントで camelCase, kebabCase, dispatchEvent などを使えるようになる。
install 時には this にあたるところに component と書かなければならないことに注意が必要。
esbuild による riot プロジェクトのビルド†
esbuild のプラグインとして riot 処理を組み込むとスムーズに行った。
https://esbuild.github.io/plugins/#on-load を参考にして、 riot-build.mjs として以下の内容を入れておく。
LANG:js import * as esbuild from 'esbuild' import fs from 'node:fs' import * as riot from '@riotjs/compiler' import * as path from 'path' import sass from 'sass' // riot に scss プラグインを挿入 riot.registerPreprocessor('css', 'scss', (code, { options }) => { const { file } = options let result = sass.compileString( code, { loadPaths: [path.dirname(file)], sourceMap: true, color: true, verbose: true, } ) const map = {...result.sourceMap} map.sources = [path.relative(process.cwd(), file)] map.file = path.basename(file) return { code: result.css, map: map } }) // esbuild の riot 処理プラグイン const riotPlugin = { name: 'riot', setup(build) { // process ".riot" with the riot compiler build.onLoad({ filter: /\.riot$/ }, async (args) => { let src = await fs.promises.readFile(args.path, 'utf8') let {code, map} = riot.compile(src); map.sources = [path.relative(process.cwd(), args.path)]; map.file = path.basename(args.path); map = Buffer.from(JSON.stringify(map)).toString('base64'); // ソースマップをインラインに添付する code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${map}`; return { contents: code, loader: 'js', } }) }, } const options = {} process.argv.slice(2).forEach((arg)=>{ if(arg.slice(0,2) == '--') { let [k, v] = arg.split('=') let array = (v || '').split(',') if(!v) v = true if(v == 'true') v = true if(v == 'false') v = false options[k.slice(2)] = array.length == 1 ? v : array } else { options.entryPoints ??= [] options.entryPoints.push(arg) } }) options.plugins = [riotPlugin] await esbuild.build(options)
後はコンソールから、
LANG:console $ node riot-build.mjs src/index.js --outfile=dist/index.js --bundle --sourcemap=inline --minifyWhitespace
などとすれば .riot ファイルを import しているのをそのままバンドルできる。
import riot from 'riot' import tag1 from './app.riot' import tag2 from './component1.riot' import tag3 from './component2.riot' const tags = [tag1, tag2, tag3] for(let tag of tags) riot.register(tag.name, tag) riot.mount('app')
みたいな。
注意点†
<slot /> はトップレベルでは使えないみたい? @ 2023-03-29†
たぶんバグなのだけれど、<slot /> を含むタグをトップレベルで使おうとするとおかしなことになる。
親コンポーネントに含まれる子タグ側で <slot /> を使う分にはちゃんと動作する。
riot compiler で <template> に書いたタグ定義をコンパイルする際の問題†
<template> 内のプロパティに { } 記法を使うとバグる。
<template id="my-tag"> <my-tag any-prop={ this will not work }> <p>{ this will work }</p> </my-tag> </template>
<template> の innterHtml は、一旦 DOM になったものを再度テキスト化したものになるため、 そもそも html として成り立たない any-prop={ } の部分がおかしくなる。
<my-tag any-prop={ this will not work }>
ではなく、
<my-tag any-prop="{ this will not work }">
と書けば大丈夫なのだけれどちょっと面倒。
古い内容†
プログラミング/riot.js/古い内容 に移しました。