テキスト入力のAtoms

Atomic Design の Atoms についてちょっと考えてみた。
要は Atoms って『分解できないもの』なので、『単体で機能として完結している』ものだ。という認識。

その前提で、普通のテキスト入力フォームはどう作るのが良いのだろうか。ということをコードを書きながら考えてみる。フレームワークはVue。

まず基本機能として、入力された値を上位コンポーネントが受け取れることと、上位コンポーネントから渡された値を反映できないと使いものにならない。
Vueで書くならこんな感じか。

<template>
  <input type="text" v-model="innerValue"/>
</template>
<script>
export default {
  props: {
    value: { type: [String, Number], default: '' }
  },
  computed: {
    innerValue: {
      get() {
        return this.value
      },
      set(val) {
        if (this.value != val) {
          this.$emit('input', val)
        }
      }
    }
  }
}
</script>

とりあえずコレをベースとして機能を追加していこうと思う。

input要素の属性

まずはinput要素の属性について。
nametypeplaceholderなど、コンポーネントの利用状況で設定を変えたい値はプロパティとして外部からもらうようにすることで再利用しやすくする。

<template>
  <input :type="type" :placeholder="placeholder" :name="name" v-model="innerValue"/>
</template>
<script>
export default {
  props: {
    name: { type: String, required: true },
    value: { type: [String, Number], default: '' },
    placeholder: { type: [String, Number], default: '' },
    type: { type: String, default: 'text',
            validator: (v) => {
              return ['text', 'password', 'number', 'tel', 'email', 'url', 'zip'].indexOf(v) !== -1
            }
          }
  },
  computed: {
    innerValue: {
      get() {
        return this.value
      },
      set(val) {
        if (this.value != val) {
          this.$emit('input', val)
        }
      }
    }
  }
}
</script>

でも、ここまで書いたとしても、本質的にはinput要素を直接利用した場合となんら変わらない。
わざわざコンポーネントとして作るのであれば、もうすこし実用的な機能を含めたいところ。

バリデーション

type属性が変更可能ということは、利用される場所によって入力内容が異なるということで、正しい値(欲しい値)が入力されているか確認が必要。
まあ、それでなくても入力値検証が不要なフォームなんて実際には存在しないので、バリデーション処理は必須機能として内包すべきでしょう。

基本的な書式検証はtype属性に応じて自動的に変えることができそう。
最大長、最低長等は外部から指定できるのが良さげ。
必須か任意かも外部から指定できた方が良いかな。

ここではVueのバリデーション実装としてVeeValidateを使用するけど、他のライブラリを使っても、全て自作で組んでも似たような処理になるんじゃないかと。

<template>
  <ValidationProvider :rules="validationRules" :vid="name" :name="title" slim>
    <input :type="type" v-model="innerValue"/>
  </ValidationProvider>
</template>
<script>
export default {
  props: {
    name: { type: String, required: true },
    value: { type: [String, Number], default: '' },
    placeholder: { type: [String, Number], default: '' },
    type: { type: String, default: 'text',
            validator: (v) => {
              return ['text', 'password', 'number', 'tel', 'email', 'url', 'zip'].indexOf(v) !== -1
            }
          },
    title: { type: String, required: true },
    required: { type: Boolean, default: false },
    min: { type: [String, Number], default: '' },
    max: { type: [String, Number], default: '' }
  },
  computed: {
    innerValue: {
      get() {
        return this.value
      },
      set(val) {
        if (this.value != val) {
          this.$emit('input', val)
        }
      }
    },
    validationRules() {
      const result = {}

      if (this.required) {
        result.required = true
      }

      if (['email', 'url'].indexOf(this.type) > -1) {
        result[this.type] = true
      } else if (this.type == 'number') {
        result['numeric'] = true
      }

      if (this.min) {
        const key = (this.type == 'number') ? 'min_value' : 'min'
        result[key] = this.min
      }
      if (this.max) {
        const key = (this.type == 'number') ? 'max_value' : 'max'
        result[key] = this.max
      }

      return result
    }
  }
}
</script>

分かりにくそうなのはif (this.min)if (this.max)あたりかな。
type="text"だと『n文字以上』と文字列長を判定するのに対し、type="number"の場合は『n以上』となり、入力値との比較をしないといけないので、type属性によって検証ルールを切り替える必要があるのです。


と、『テキスト入力の最小構成』ということで考えると、これぐらいは必要なんじゃないかと。
そもそもコンポーネントにまとめる理由は『再利用できるパーツを作る』ということのはず。
その前提で考えたとき、例えばinputtype属性を固定して、type毎にコンポーネントを作ったりしても再利用性は低いように思える。

バリデーションについては前述の通り、入力値の検証が不要なケースなんて Atomic Design を導入するような状況では事実上0%なはず。
であるなら、入力値検証は最小構成のひとつの機能となりうるのではないかと。

てことで、テキスト入力フォームの Atoms について考えてみた。
と言っても、Atomic Design って唯一無二の正解がある厳密なものではなく、指針として使うものなのだろう。
つまり何を Atoms と定義するかはローカルルールで良いように思える。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です