プログラミング/riot.js のバックアップの現在との差分(No.3)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
[[公開メモ]]

* 覚えにくい点をメモ [#ndc106d3]
#contents

- カスタムタグのスクリプト内に於いて、this 変数は常に「現在のタグ(=カスタムタグ)のインスタンス」を指します。→ これどういうことだ?
- <style type="riot"></style> を置くことで、riot が css を挿入する位置を指定できる。~
(type って text/css じゃないものが書かれていても大丈夫なんだっけ???)
- カスタムタグを利用する際は<todo></todo>のように閉じられる必要があります。自己終了タグ<todo/>はサポートしません。
- this.refsオブジェクトに続くref属性を持つ要素へのアクセス って何だっけ?
- タグのライフサイクル
++ タグが構成される
++ タグのJavaScriptロジックが実行される
++ テンプレート変数が計算され、"before-mount" イベントが発火
++ ページ上でタグがマウントされ、"mount" イベントが発火
++ 次のいずれかで update される。~
その際、"update" イベントで値を計算し、html 更新後に "updated" が発火
--- Observable な変数が変更された際自動的に (e.preventUpdateをtrueにセットしない場合)
--- this.update()が現在のタグインスタンス上で呼ばれたとき
--- this.update()が親タグあるいは、さらに上流のタグで呼ばれたとき。更新は親から子への一方通行で流れる。
--- riot.update()が呼ばれたとき。ページ上のすべてのテンプレート変数を更新。
++ unmount() が呼ばれれば、'before-unmount' の後に 'unmount' が発火
- イベント名に一貫性がないことに注意が必要~
"mounting", "mounted", "updating", "updated", "unmounting", "unmounted" などだったら覚えやすいのに
- コンパイラ入り riot を使う場合は、<script src="todo.js" type="riot/tag"></script> の形で tag を呼んでおけばよしなにしてくれる
- 
* Riot 4 では大幅に内容が変わったそうです [#j06809f4]

* ドキュメントのバグ [#cfe335ea]
チートシート的な覚書。

** イベントハンドラでの e.target [#c1036157]
** コンポーネントの書き方の基本 [#g34b71e7]

http://riotjs.com/ja/guide/#イベントハンドラ
ユーザー定義のタグやプロパティにはハイフンを含めるのが本来の流儀。含めなくても動くけど。

では、
 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>

>- e.currentTarget は、イベントハンドラが指定された要素を指します
>- e.targetはイベントの送信元エレメントです。これは必ずしも必要ではなく、currentTarget と同じです。
タグの呼び出し側では以下のようにして使う

と書かれているのだけれど、
 <tag any-prop="value" on-custom-event={customEventHandler}>
   ここに書かれた内容が slot に入る。
   <span>{anyProp}</span> のようにして渡されたプロパティを使える。
 </tag>

e.currentTarget がイベントハンドラが指定された要素であるのに対して、~
e.target はイベントが発生した要素になることがあるようでした。
this.props.anyProp や this.props.onCustomEvent に値が入る。

http://jsbin.com/tiqoyehaco/edit?html,output
** コンポーネントからコールバックを受けるためのイベントハンドラ [#wc372bec]

こちらの黄色い枠の上へマウスカーソルを持って行くと~
e.target が A 要素なのに対して~
e.currentTarget が DIV 要素になることが分かります。
タグからコールバックを受けるには上記のように on-custom-event みたいなイベントリスナーを渡して、
タグ内で dispatchEvent("custom-event", anyData) とすれば、以下のようにデータを受け取れる。

で、確かめてみると英語の説明は
 customEventHandler(e) {
   // e.detail に anyData が入っている
 }

>- e.currentTarget points to the element where the event handler is specified.
>- e.target is the originating element. This is not necessarily the same as currentTarget.
** コンポーネント側のメソッドを呼びたい場合 [#k14225e2]

で、e.target の説明を日本語にする際に誤訳していたことが分かりました。
逆にタグの呼び出し側からコンポーネント側のメソッドを呼びたいときは、
props 経由でインターフェースを共有するのが良さそう?

修正を PR しました。~
https://github.com/riot/riot.github.io/pull/191
呼び出し側から intf というプロパティを与えておいて、その中に function を返してもらう。

** プリプロセッサの説明へのリンクが壊れている [#sd142b7d]
 <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>

http://riotjs.com/ja/guide/#%E3%83%97%E3%83%AA%E3%83%97%E3%83%AD%E3%82%BB%E3%83%83%E3%82%B5
タグ側では、

「プリプロセッサ」からの飛び先は、
 <some-tag>
  <script>
    export default {
      onBeforeUpdate(props, state) {
        props.intf.someFunc = this.someFunc;
      },
      someFunc(arg) {
        // parent-tag から someTagIntf.someFunc として呼び出せる
      },
    }
   </script>
 </some-tag>

- http://riotjs.com/ja/guide/#%E3%83%97%E3%83%AA%E3%83%97%E3%83%AD%E3%82%BB%E3%83%83%E3%82%B5 ではなく
- http://riotjs.com/ja/guide/compiler/#%E3%83%97%E3%83%AA%E3%83%97%E3%83%AD%E3%82%BB%E3%83%83%E3%82%B5 であるべき
みたいな。

PR するために準備中。~
https://github.com/osamutake/riot.github.io/commit/766b8e6124797bf6641300356eb5ed1b7135c389
** プラグインのインストール [#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 で &lt;template> に書いたタグ定義をコンパイルする際の問題 [#afee6ffb]

&lt;template> 内のプロパティに { } 記法を使うとバグる。

 <template id="my-tag">
   <my-tag any-prop={ this will not work }>
     <p>{ this will work }</p>
   </my-tag>
 </template>

&lt;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: 2175 (from 2010/06/03), today: 1, yesterday: 2