Vueで文字数制限するdirectiveを実装する方法と注意点
vueでinputの文字数を制限する機能を実装する機会がありました。
directiveにして共通化しようと実装してみると、Vueの仕組みをちゃんと考慮しておかないとハマってしまう現象に出会ってしまいました。
ちゃんと実装できてからは考えてみれば当然と思えても、実装している最中は時間をとられてしまいました・・。
それではまず最初に実装したコードから紹介します。
最初書いていたコード
シンプルにdirectiveで設定された数値の文字数で切り取って、値をセットしています。
一見これでうまく動きそうですが、特定の条件化では想定しない挙動になってしまいます。
1 2 3 4 5 6 7 8 9 10
Vue.directive('sample', { bind: function(el: Element, binding: any, vnode: any) { el.addEventListener('input', (e: any) => { e.target.value = e.target.value.substr(0, binding.value); }); } }); // HTML <input type="text" v-sample="10" />
v-modelが設定済みかつコピー&ペーストした時
上記のコードはv-modelが設定されていなかったり、コピー&ペーストに対応する必要がなければ問題なく動作します。
ですがv-modelを設定してコピー&ペーストに対応した時に限り上記のコードではうまく動きません。
コピー&ペーストで上限以上の文字を入力した時に瞬間的には正常に動いているように見えますが、他の要素の値が変更されるなど再描画が起こったときに問題が生じます。
再描画で制限文字数を超えて値が入力される
再描画時に入力制限したはずのinputに、文字数を超えて最後にコピー&ペーストした内容が値に設定されてしまいます。
この現象が起こる原因は、v-modelには下記の加工した値が設定されていないからです。
1
e.target.value.substr(0, binding.value)
つまり加工した値をe.target.valueで設定するだけではうまくいきません。
完成形
v-modelが設定されているかどうかで挙動が変わってしまうため、実装したdirectiveと同時にv-modelが設定されているかどうかで処理を分ける必要があります。
完成したコードが下記です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Vue.directive('sample', { bind: function(el: Element, binding: any, vnode: any) { const model = _.find(vnode.data.directives, (directive: any) => { return directive.name === 'model'; }); const name = model ? model.expression : ''; const max = binding.value; el.addEventListener('input', (e: any) => { const value = e.target.value.substr(0, max); if (name) { Vue.set(vnode.context, name, value); } else { e.target.value = value; } }); } }); // HTML <input type="text" v-sample="10" />
vnode.data.directivesに同時に設定済みのdirectiveが配列で入っているので、v-modelが設定されている場合にはVue.setでmodelの方の値を更新することでうまくいきます。
分岐せずもうまくいくようですが、Vue.setとe.target.valueで両方設定するとiOS系のinputで値が2重に入力される現象が発生するので、分岐しておくほうがよいです。
VueはReactやAngularに比べてシンプルで分かりやすいと思っていることもあり、よく使うライブラリの1つです。
今回文字数制限のdirectiveを実装したのは何気に初めてでしたが、Vueを使っているからこそハマったようなポイントで勉強になりました。