Vue.js 用バリデーション付きテキスト入力コンポーネントサンプル


Bower 等を利用せず Vue.js を部分適用していて、プラグイン等を極力利用したくない方向けです。

必須入力、文字列長、数値（正の整数）、数値範囲に対応しています。
数値チェックについては正規表現を調整することで他の数値にも対応できると思います。

実際にフォームで利用するサンプルを CodePen においています。

<p data-height="265" data-theme-id="0" data-slug-hash="vQLERY" data-default-tab="js,result" data-user="R-STYLE" data-pen-title="vQLERY" data-preview="true" class="codepen">See the Pen <a href="https://codepen.io/R-STYLE/pen/vQLERY/">vQLERY</a> by R-STYLE (<a href="https://codepen.io/R-STYLE">@R-STYLE</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


## validatable-text component

新しい構文を避けているので、大概のブラウザで動作すると思われます。
IE10 辺りで使えない構文がいくつかあって、この形になりました。
※ デフォルト引数の構文が混ざっていたので削除しました。

```JavaScript
Vue.component('validatable-text', {
  props: {
    value: String,
    attr: {
      id: String,
      label: String,
      placeholder: String,
      required: {
        type: Boolean,
        default: false,
      },
      numeric: {
        type: Boolean,
        default: false,
      },
      maxlength: Number,
      min: Number,
      max: Number,
    },
  },
  data: function () {
    return {
      helper: '',
      integerPattern: new RegExp(/^(0|[1-9][0-9]*)$/),
    };
  },
  computed: {
    needsValidate: function () {
      return this.attr.required
        || this.attr.numeric
        || typeof this.attr.maxlength !== 'undefined'
        || typeof this.attr.min !== 'undefined'
        || typeof this.attr.max !== 'undefined';
    },
    invalid: function () {
      return this.helper.length > 0;
    },
    message: function () {
      return '[' + this.attr.label + '] ' + this.helper;
    },
  },
  methods: {
    validateOnSubmit: function (callback) {
      if (typeof callback !== 'function') {
        throw new TypeError('callback is not a function.');
      }
      this.validateRequired(this.value);
      if (this.invalid === false) {
        this.validate(this.value);
      }
      if (this.invalid) {
        callback(this.message);
      }
    },
    validateRequired: function (value) {
      if (this.needsValidate === false) {
        return;
      }
      this.helper = '';
      if (this.attr.required && this.isEmpty(value)) {
        this.helper = 'please input required';
      }
    },
    validate: function (value) {
      if (this.needsValidate === false) {
        return;
      }
      this.helper = '';
      if (this.isInvalidLength(value)) {
        this.helper = 'please input less than ' + this.maxlength;
      }
      else if (this.isInvalidInteger(value)) {
        this.helper = 'please input an integer';
      }
      else if (this.isInvalidRange(value)) {
        this.helper = 'please input between ' + this.attr.min + ' and ' + this.attr.max;
      }
    },
    isEmpty: function (value) {
      if (typeof value === 'undefined' || value === null) {
        return true;
      }
      if (typeof value === 'object') {
        return Object.keys(value).length === 0;
      }
      if (typeof value.length === 'number') {
        return value.length === 0;
      }
      if (typeof value.size === 'number') {
        return value.size === 0;
      }
      return false;
    },
    isInvalidLength: function (value) {
      return this.attr.maxlength && value && (value + '').length > this.attr.maxlength;
    },
    isInvalidInteger: function (value) {
      return this.attr.numeric && value && !(this.integerPattern.test(this.toHalfNum(value)));
    },
    isInvalidRange: function (value) {
      if (!(this.attr.min && this.attr.max)) {
        return false;
      }
      var num = this.toHalfNum(value);
      return value && this.integerPattern.test(num)
      && ((num - 0) < this.attr.min || (num - 0) > this.attr.max);
    },
    toHalfNum: function (value) {
      if (typeof value !== 'number' && typeof value !== 'string') {
        throw new TypeError('the argument value only supports the types String and Number.');
      }
      if (typeof value === 'number') {
        return value + '';
      }
      return value.replace(/[０-９]/g, function(src) {
        return String.fromCharCode(src.charCodeAt(0) - 65248);
      });
    },
  },
  template: `
    <div v-bind="$attrs">
    <label v-bind:class="[attr.needsValidate ? 'NeedsValidate' : '', attr.required ? 'Required' : '', invalid ? 'Invalid' : '']">{{attr.label}} <em v-cloak v-if="attr.required">*</em>
    <input type="text"
      v-on:input="$emit('input', $event.target.value); validate($event.target.value);"
      v-bind:value="value"
      v-bind:id="attr.id"
      v-bind:name="attr.id"
      v-bind:placeholder="attr.placeholder"
      v-bind:maxlength="attr.maxlength"
      v-bind:min="attr.min"
      v-bind:max="attr.max"
    />
    <i v-cloak v-show="invalid"></i>
    </label>
    <small v-cloak v-show="invalid">{{helper}}</small>
    </div>
  `,
});
```