Single Page Application の Form 画面における Best Practice を考える(中編)

というわけで前回 Form Validation について考えたので、それを踏まえて実際に Component を作成します。

## input text field

以下、私が作成した `components/forms/input-text-field.vue` です。`@change` ではなく、`@input` を使っているのは `@change` だと focus が外れるまで変更を検知できないからです。focus が外れてから form validation のトリガーが実行されると不便なので `@input` を使使います。なお、 CSS framework に Bulma を使用しています。

また 子となる component に `pristine` という state を持たせています。これはフォームに未入力の場合、`xx は入力必須です` といったメッセージはまだ表示させたくないので、あえて子となる component に state をもたせました。

私は原則 component は stateless にしたいと考えています。なぜなら密結合をさせない為にも、子となる component を外側の世界に関与させないためです。なのですが、逆に親 component に関与させたくない細かい事象については子となる component で完結させたいと考えました。

```vue:components/forms/input-text-field.vue
<template>
  <div class="field">
    <p class="control has-icons-right">
      <input @input="handleChange" :class="{'is-danger': hasError}" class="input" type="text" :placeholder="placeholder">
      <span v-if="isValid" class="icon is-small is-right">
        <fa icon="check" aria-hidden="true" />
      </span>
    </p>
    <p v-if="hasError" class="help is-danger">
       {{ errorMessages }}
    </p>
  </div>

</template>

<script>
export default {
  props: ['placeholder', 'errorMessages', 'onChange'],
  data () {
    return {
      pristine: true
    }
  },
  computed: {
    isValid () {
      return !this.pristine && !this.errorMessages
    },
    hasError () {
      return !this.pristine && this.errorMessages
    }
  },
  methods: {
    handleChange (e) {
      this.onChange(e.target.value)
      this.pristine = false
    }
  }
}
</script>
```

Nuxt.js でいうところの Page Component からは以下のように呼び出します。

```vue:pages/vue-examples/example.vue
<template>
  <div class="field-body">
    <InputTextField
      placeholder="姓"
      :errorMessages="errors.lastNameKanji"
      :onChange="(value) => handleChangeForms({lastNameKanji: value})"
    />
    <InputTextField
      placeholder="名"
      :errorMessages="errors.firstNameKanji"
      :onChange="(value) => handleChangeForms({firstNameKanji: value})"
    />
  </div>
</template>

<script>
import { validate } from '~/utils/validator'
import InputTextField from '~/components/forms/input-text-field'

export default {
  components: {
    InputTextField,
  },
  data () {
    return {
      fields: {},
      errors: {}
    }
  },
  methods: {
    async handleChangeForms (fields) {
      this.fields = { ...this.fields, ...fields }
      this.errors = Object.assign({}, validate(this.fields))
    }
  }
}
</script>
```

`<template>` の記述量はすこし多いですが、前回作成した validator 関数のおかげで`<script>` 内の記述量は抑える事ができました。

## 生年月日の Select Field を考える

よくある生年月日の入力フォームですが、今度は単一ではなく複数の select 要素から構成される場合を考えます。今回は `yyyy-mm-dd` 形式の文字列を `value` として受け取る component を考えます。

```vue:components/forms/birthday-field.vue
<template>
  <div class="field is-narrow">
    <div class="control">
      <div class="select">
        <select @change="handleChageYear" :value="year">
          <option :value="null" :key="`year:0`"></option>
          <option v-for="n in 50" :value="n + 1950" :key="`year:${n}`">{{ n + 1950 }}年</option>
        </select>
      </div>
      <div class="select">
        <select @change="handleChageMonth" :value="month">
          <option :value="null" :key="`month:0`"></option>
          <option v-for="n in 12" :value="zeroPadding(n)" :key="`month:${n}`">{{ n }}月</option>
        </select>
      </div>
      <div class="select">
        <select @change="handleChageDay" :value="day">
          <option :value="null" :key="`day:0`"></option>
          <option v-for="n in 31" :value="zeroPadding(n)" :key="`day:${n}`">{{ n }}日</option>
        </select>
      </div>
    </div>
    <p v-if="hasError" class="help is-danger">
       {{ errorMessages }}
    </p>
  </div>
</template>

<script>
export default {
  props: ['value', 'errorMessages', 'onChange'],
  data () {
    return {
      pristine: true
    }
  },
  computed: {
    year () {
      return this.value && this.value.substr(0, 4)
    },
    month () {
      return this.value && this.value.substr(5, 2)
    },
    day () {
      return this.value && this.value.substr(8, 2)
    },
    isValid () {
      return !this.pristine && !this.errorMessages
    },
    hasError () {
      return !this.pristine && this.errorMessages
    }
  },
  methods: {
    zeroPadding (value) {
      return String(value).padStart(2, '0')
    },
    handleChageYear (e) {
      this.onChange([e.target.value, this.month, this.day].join('-'))
      this.pristine = false
    },
    handleChageMonth (e) {
      this.onChange([this.year, e.target.value, this.day].join('-'))
      this.pristine = false
    },
    handleChageDay (e) {
      this.onChange([this.year, this.month, e.target.value].join('-'))
      this.pristine = false
    }
  }
}
</script>
```

Nuxt.js でいう Page Component では以下のように呼び出す事ができます。

```vue
<BirthdayField
  :value="fields.birthday"
  :errorMessages="errors.birthday"
  :onChange="(birthday) => handleChangeForms({birthday})"
/>
```

## まとめ

今回の例では Validation 部分は Page Component に記述して、子となる Component の処理は極力簡略化しています。Component の作成は CSS が得意な人に専念してもらいたいので責任をここで分離しています。

次回、Form Validation を Page Component から追い出せないかを考えます。