プログラミング/riot.js の変更点
更新- 追加された行はこの色です。
- 削除された行はこの色です。
- プログラミング/riot.js へ行く。
- プログラミング/riot.js の差分を削除
[[公開メモ]] #contents * Riot 4 では大幅に内容が変わったそうです [#j06809f4] チートシート的な覚書。 ** コンポーネントの書き方の基本 [#g34b71e7] ユーザー定義のタグやプロパティにはハイフンを含めるのが本来の流儀。含めなくても動くけど。 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 に値が入る。 ** コンポーネントからコールバックを受けるためのイベントハンドラ [#wc372bec] タグからコールバックを受けるには上記のように on-custom-event みたいなイベントリスナーを渡して、 タグ内で dispatchEvent("custom-event", anyData) とすれば、以下のようにデータを受け取れる。 customEventHandler(e) { // e.detail に anyData が入っている } ** コンポーネント側のメソッドを呼びたい場合 [#k14225e2] 逆にタグの呼び出し側からコンポーネント側のメソッドを呼びたいときは、 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> みたいな。 ** プラグインのインストール [#o4f6abaf] 上記の 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 プロジェクトのビルド [#wb4d7e8b] 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') みたいな。 * 注意点 [#t5b9a7f0] ** <slot /> はトップレベルでは使えないみたい? @ 2023-03-29 [#a13e5c23] たぶんバグなのだけれど、<slot /> を含むタグをトップレベルで使おうとするとおかしなことになる。 親コンポーネントに含まれる子タグ側で <slot /> を使う分にはちゃんと動作する。 ** riot compiler で <template> に書いたタグ定義をコンパイルする際の問題 [#afee6ffb] <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 }"> と書けば大丈夫なのだけれどちょっと面倒。 * 古い内容 [#kc3b56c3] [[プログラミング/riot.js/古い内容]] に移しました。
Counter: 2338 (from 2010/06/03),
today: 1,
yesterday: 1