プログラミング/riot.js

(383d) 更新


公開メモ

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/古い内容 に移しました。


Counter: 2191 (from 2010/06/03), today: 2, yesterday: 1