プログラミング/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: 2506 (from 2010/06/03),
today: 1,
yesterday: 0