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-bindings
のtemplate
を使って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-bindings
のcreateExpression
を使って属性式を反映させます。
ついでに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>
これで完成です。
こんな感じにslots
とattributes
を反映させれば、大抵のケースは対応できるのではないかと思います。
【おまけ】`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と連携させるサンプルを作ったのでよかったら見てください