13
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ドメイン駆動設計 with Vue/Nuxt(Composition API)でリアルタイム・バリデーション

Last updated at Posted at 2020-05-16

追記

2020/05/31 別記事として発展編書きました

問題意識

ドメイン駆動設計に従うVue/Nuxtアプリケーションを作っていて、
名前フォームに文字列を打ち込んで8文字より多く書かれたとき、
そのフォームの一つ上に赤い文字で「名前は8文字以内です」というエラーをリアルタイムで表示してほしい、
……という要件があるとしましょう。

このときVue側のコードで、インプットフォームから受け取った変数に8文字という制限をすることも可能なのですが、
「それって 利口なUI という奴なのでは?」と考えていました。
一般に利口なUIはアンチパターンとして知られていて、(議論はありますが)たしかに、ドメイン知識がUI層に流出しています。
どのみちエンジニア的にも同じ内容を2度書くような気がして、利口なUIは利口じゃないコードになりかねません。

そういうわけで自分なりにその解決をしてみたいと思います。

完成品

devinoue/realtime-validation-ddd

環境

Nuxt2.12.2
Composition API
TypeScript

ドメインを書く

今回はNameクラスをTypeScriptで書きますが、必要そうな所だけです。
ひとまずdomainというディレクトリに以下のような値オブジェクトとしてName.tsを入れておきます。

domain/Name.ts
export default class Name {
  constructor(private _name: string) {
    Name.validation(this._name)
  }

  static validation(name: string): never | void {
    if (name === '') {
      throw new Error('名前を入力してください')
    }
    if (typeof name !== 'string') {
      throw new TypeError('名前は文字列にしてください')
    }
    if (name.length > 8) {
      throw new Error('名前は8文字以内にしてください')
    }
  }
}

ついでに、Eメールアドレス用値オブジェクトも作っておきます。
コード的にはほぼ同じになってしまうので、こちらから御覧ください。

ハンドラを書く

Composition APIで作るので、ハンドラも切り分けておきます。
別にそういうvue界の慣習があるわけではありませんし、切り分けなくてもいいのですが、
この方が見晴らしがいいという理由で分けています。
ちなみにDDDでいうプレゼンテーション層に属するものとして扱っています。
ここからドメイン知識にアクセスしていますが、生成メソッドではないため許容されるという認識です。

handler/InputHandler.ts
import { ref, watch } from '@vue/composition-api'
import Name from '~/domain/Name'
import EmailAddress from '~/domain/EmailAddress'

interface IForm {
  name: string
  email: string
}

export default function() {
  const defaultInput: IForm = { name: '', email: '' }

  const forms = reactive({ ...defaultInput })
  const errors = reactive({ ...defaultInput })

  watch(
    () => forms.name,
    () => {
      errors.name = ''
      try {
        Name.validation(forms.name)
      } catch (e) {
        errors.name = e.message
      }
    },
    { lazy: true }
  )
  watch(
    () => forms.email,
    () => {
      errors.email = ''
      try {
        EmailAddress.validation(forms.email)
      } catch (e) {
        errors.email = e.message
      }
    },
    { lazy: true }
  )

  return { forms, errors }
}


ここ、無駄が多い気がしますが、watch関数を使っている手前まとめにくい、、、、

vueファイルを完成させる

さて、残りはpages以下のindex.vueで、さきほど作ったファイルを読み込むだけです。

pages/index.vue
<template>
  <div class="container">
    <span v-show="errors.name" class="error">{{ errors.name }}</span>
    <span>名前 : <input v-model="name" type="text"/></span>
    <br />
    <span v-show="errors.email" class="error">{{ errors.email }}</span>
    <span>メールアドレス : <input v-model="email" type="text"/></span>
    <br />
  </div>
</template>

<script lang="ts">
import useInputHandler from '~/handler/InputHandler'
export default {
  setup() {
    return { ...useInputHandler() }
  }
}
</script>
// スタイル省略

御覧ください!! ほとんど空っぽ! なんとスッキリしているのでしょう!!!😭
今までのVueファイルがウソのようにキレイにまとめられました!

動作イメージ

何か書き込むたびにwatchメソッドが変数を監視して、エラーを報告してくれます。
これで「利口なUI」とならずに済みます😄
コメント 2020-05-16 231755.png

終わりに

Vue3素晴らしい……!

GitHub:
devinoue/realtime-validation-ddd

13
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?