LoginSignup
1
0

More than 3 years have passed since last update.

【Riot.js】カスタムディレクティブ

Last updated at Posted at 2020-12-06

Riot.js Advent Calendar 2020 - Qiita の7日目の記事です。

最初に

riot.pureをつかってカスタムディレクティブっぽいものを実装します。
今回の完成物はこちらです
こんな感じのrefディレクティブを例としてつくります。

<app>
  <!-- クリックしたらまわる -->
  <div is="custom-directive" ref="message" onclick="{ onClick }">
    { state.message }
  </div>

  <script>
    export default {
      state: {
        message: 'Hello'
      },
      onClick() {
        this.$refs.message.animate([
          { transform: 'rotate(0)' },
          { transform: 'rotate(360deg)' }
        ], { duration: 100 })
      }
    }
  </script>
  <style>
    div {
      display: inline-block;
    }
  </style>
</app>

さっそくやっていきましょう

1.$refsに対象の要素をセットする

親スコープの$refsにref属性に基づいて自分の要素をセットしています。
子要素がないものならこれだけで十分動きそうですね。(例:<input is="custom-directive" ref="text" />)

<custom-directive>
  <script>
    import { pure } from 'riot'
    export default pure(({ slots, attributes, props }) => {
      return {
        el: null,
        mount(el, scope) {
          this.el = el
          this.refDirective(scope)
        },

        update(scope) {
        },

        unmount(...args) {
        },

        refDirective(scope) {
          const ref = this.el.getAttribute('ref')
          if (!ref) return
          scope.$refs = scope.$refs || {}
          scope.$refs[ref] = this.el
        }
      }
    })
  </script>
</custom-directive>

2. slotsをdomに反映させる

dom-bindingstemplateを使ってslotを反映させています。

<custom-directive>
  <script>
-    import { pure } from 'riot'
+    import { pure, __ } from 'riot'
+    const { template, bindingTypes } = __.DOMBindings
    export default pure(({ slots, attributes, props }) => {
      return {
        el: null,
+        slot: null

        mount(el, scope) {
          this.el = el
+          this.createSlot(scope)
          this.refDirective(scope)
        },

        update(scope) {
+          this.slot.update({}, scope)
        },

        unmount(...args) {
+          this.slot.unmount(...args)
        },

+        createSlot(scope) {
+          if (!slots && !slots.length) return
+
+          this.slot = template('<slot/>', [{
+            name: 'default',
+            type: bindingTypes.SLOT,
+            selector: 'slot'
+          }])
+          this.slot.mount(this.el, { slots }, scope)
+        },
...
      }
    })
  </script>
</custom-directive>

3. 属性式に対応する

属性式とは<p data-test={ 1 + 1 } onclick={ onClick }></p>の属性にでてくる{}に囲んであるところ
dom-bindingscreateExpressionを使って属性式を反映させます。
ついでにref属性が属性式になっているときの対応もします。


<custom-directive>
  <script>
    import { pure, __ } from 'riot'
-    const { template, bindingTypes } = __.DOMBindings
+    const { createExpression, template, bindingTypes } = __.DOMBindings
    export default pure(({ slots, attributes, props }) => {
      return {
        el: null,
        slot: null,
+        expressions: [],
+        prevRef: null,
        mount(el, scope) {
          this.el = el
+          this.expressions = (attributes || []).map(a => createExpression(el, a))
+          this.expressions.forEach(e => e.mount(scope))
          this.createSlot(scope)
          this.refDirective(scope)
        },

        update(scope) {
          this.slot.update({}, scope)
+          this.expressions.forEach(e => e.update(scope))
+          this.refDirective(scope)
        },

        unmount(...args) {
          this.slot.unmount(...args)
+          this.expressions.forEach(e => e.unmount(scope))
        },
...
        refDirective(scope) {
          const ref = this.el.getAttribute('ref')
-          if (!ref) return
+          if (!ref || this.prevRef === ref) return
+          this.prevRef = ref
          scope.$refs = scope.$refs || {}
          scope.$refs[ref] = this.el
        }
      }
    })
  </script>
</custom-directive>

これで完成です。
こんな感じにslotsattributesを反映させれば、大抵のケースは対応できるのではないかと思います。

【おまけ】custom-directiveにmodelディレクティブ(双方向バインディング)を追加しましょう。
<custom-directive>
  <script>
    import { pure, __ } from 'riot'
    const { createExpression, template, bindingTypes } = __.DOMBindings
    export default pure(({ slots, attributes, props }) => {
      return {
        el: null,
        slot: null,
        expressions: [],
        prevRef: null,
+        prevModel: null,
+        onChange: null,

        mount(el, scope) {
          this.el = el
          this.expressions = (attributes || []).map(a => createExpression(el, a))
          this.expressions.forEach(e => e.mount(scope))
          this.createSlot(scope)
          this.refDirective(scope)
+          this.modelDirective(scope)
        },

        update(scope) {
          this.slot.update({}, scope)
          this.expressions.forEach(e => e.update(scope))
          this.refDirective(scope)
+          this.modelDirective(scope)
        },

        unmount(...args) {
          this.slot.unmount(...args)
          this.expressions.forEach(e => e.unmount(scope))
+          this.el.removeEventListener('input', this.onChange)
        },

        createSlot(scope) {
          if (!slots && !slots.length) return
          this.slot = template('<slot/>', [{
            name: 'default',
            type: bindingTypes.SLOT,
            selector: 'slot'
          }])
          this.slot.mount(this.el, { slots }, scope)
        },

        refDirective(scope) {
          const ref = this.el.getAttribute('ref')
          if (!ref || this.prevRef === ref) return
          this.prevRef = ref
          scope.$refs = scope.$refs || {}
          scope.$refs[ref] = this.el
        },

+        modelDirective(scope) {
+          const model = this.el.getAttribute('model')
+          if (!model || this.prevModel === model) return
+          if (this.onChange) this.el.removeEventListener('input', this.onChange)
+          this.prevModel = model
+          this.onChange = e => scope.update({
+            ...scope.state,
+            [model]: e.target.value
+          })
+          this.el.addEventListener('input', this.onChange)
+          this.el.value = scope.state[model] || ''
+        }
      }
    })
  </script>
</custom-directive>

これで先程の例のメッセージをinput要素に双方向バインディングさせられます。

<app>
  <div is="custom-directive" ref="message" onclick="{ onClick }">
    { state.message }
  </div>
  <input is="custom-directive" type="text" model="message">
  <script>
    export default {
      state: {
        message: 'Hello'
      },
      onClick() {
        this.$refs.message.animate([
          { transform: 'rotate(0)' },
          { transform: 'rotate(360deg)' }
        ], { duration: 100 })
      }
    }
  </script>
  <style>
    div {
      display: inline-block;
    }
  </style>
</app>


最後に

完成物
riot.pureを使えばかなり自由に拡張できそうですね。@riotjs/loader, @riotjs/routerなどのライブラリにも使われています。

reduxをriotと連携させるサンプルを作ったのでよかったら見てください

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0