はじめに
以前の記事、「ドメイン駆動設計 with Vue/Nuxt(Composition API)でリアルタイム・バリデーション」の発展型を作ってみました。
当初VeeValidate3が発表され少し勉強しようと使っていたのですが、2系とは異なる破壊的変更に当初は戸惑いました。いざ自分で似たようなものを作ってみるとinvalidがv-slotで呼ばれている理由など発見も多かったです。
そこで前回の記事を元にして、もう少しVeeValidateに似せたドメイン駆動設計対応のリアルタイムバリデーションを作ってみます。
要件としては
- ドメインオブジェクトに対する入力情報の可否ロジックの責務はドメインオブジェクトが担う(ここを分離するのが不満だった)
- リアルタイムバリデーションする
- エラーは即座に、そのエラーが出ているフォーム付近に表示する
- 実装があんまり難しくならないようにする
- アトミックデザインにも対応できるようにする
- 今回は深く取り組んでいないですが修正すれば可能
コード
GitHubにあげています。
devinoue/realtime-validation-ddd-advanced
ドメイン層の実装
以下は一例で、しかも前回と同じものです。
validation
メソッドは必要です。
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文字以内にしてください')
}
}
}
子コンポーネントフォーム
使いまわしできるような子コンポーネントinput
フォームです。
このフォーム内で設定されるのは、id
、type
、placeholder
などHTML寄りの要素です。maxlength
を入れなかったのは、それはドメイン側で操作できたほうが良いかなという理由。
エラーの位置やラベルの位置などはここをいじる、またはそもそもコンポーネントごとに分離するというアトミックデザイン的なやり方もありかもしれません。
<template>
<div>
<label :for="labelId">{{ labelName }}</label>
<input
:id="labelId"
v-model="value"
type="inputType"
:placeholder="placeHolder"
/>
<br />
<span class="error">{{ errorMessage }}</span>
</div>
</template>
<script lang="ts">
import {
ref,
defineComponent,
watch,
SetupContext,
PropType
} from '@vue/composition-api'
import { Domain } from '~/types/index'
export default defineComponent({
name: 'FormInput',
props: {
domainName: {
type: Function as PropType<Domain>,
required: true
},
labelName: {
type: String,
required: true
},
labelId: {
type: String,
required: true
},
inputType: {
type: String,
required: true
},
placeHolder: {
type: String,
required: true
},
inputName: {
type: String,
required: true
}
},
setup(props, { emit }: SetupContext) {
const value = ref('')
const errorMessage = ref('')
const isValid = ref(false)
watch(
value,
() => {
isValid.value = false
errorMessage.value = ''
emit('input', value.value)
try {
props.domainName.validation(value.value)
isValid.value = true
} catch (e) {
errorMessage.value = e.message
}
},
{ lazy: true }
)
return {
errorMessage,
value,
isValid
}
}
})
</script>
<style scoped>
.error {
color: red;
}
</style>
親コンポーネントの記述
<template>
<div class="container">
<ValidationObserver
ref="observer"
v-slot="{ isValid }"
:observer="observer"
><!-- 必須定型文 -->
<FormInput
v-model="forms.name"
:domain-name="Name"
:label-id="'name'"
:label-name="'名前'"
:input-type="'text'"
:input-name="'name'"
:place-holder="'名前を入力してください'"
/>
<FormInput
v-model="forms.email"
:domain-name="EmailAddress"
:label-id="'email'"
:label-name="'メールアドレス'"
:input-type="'text'"
:input-name="'email'"
:place-holder="'メールアドレスを入力してください'"
/>
<button :disabled="!isValid">ボタン</button>
</ValidationObserver>
{{ forms }}
</div>
</template>
<script lang="ts">
import { ref, reactive } from '@vue/composition-api'
import Name from '~/domain/Name'
import EmailAddress from '~/domain/EmailAddress'
import FormInput from '~/components/ValidationForm/ValidationFormInput.vue'
import ValidationObserver from '~/components/ValidationForm/ValidationObserver.vue'
export default {
name: 'Index',
components: { FormInput, ValidationObserver },
setup() {
const observer = ref<any>(null) // 必須
const forms = reactive({
name: '',
email: ''
})
return { Name, EmailAddress, forms, observer }
}
}
</script>
上記の方法では、ドメインオブジェクトのうちName
とEmailAddress
だけ読み込んで、それをリアクティブにして、子コンポーネントの:domainName
ディレクティブで渡しています。子コンポーネントは受け取ったドメインオブジェクトを利用してvalidation
メソッドを実行しています。
これらの子コンポーネントフォームは、ValidationObserver
という名前のカスタムタグで囲まれていますが、これが子コンポーネントのすべてのvalidation
に問題がないかをチェックしています。
(このObserverの役割はVeeValidate3と同じですね)
今回はforms
で内容を受け取っています。
使用法
index.vue
がその例になっていますが改めてご説明させていただきます。
まずバリデーションしたいドメインオブジェクトを作ります。これは普通の.ts/.jsファイルです。バリデーションロジックの書き方はvalidation
メソッド内で例外を投げるように書きます。
ほぼ定型文になるのですが、ValidationFormInput
コンポーネントを呼び出し、ValidationObserver
とその定型文のプロパティでこれを囲みます。
これでValidationFormInput
コンポーネントの内容が$ref
経由で監視され、OKならisValid
がtrue
、ダメならfalse
になります。button
要素の:disabled
を利用して、ボタンコントロールができます。
また、ValidationFormInput
などはmixinなどで実装するといちいちimportで呼び出さずになります
(今後Nuxtでは自動importが標準で用意されるようなので、mixinも不要になるかもしれませんが)
議論
今回はクラス名をpropsとして渡していますが、これはドメイン駆動設計的にどうなのよ?という疑問がわきます。一つの代案としてはDTO的な何かにオプション情報をまとめて、propsするというものがあるのではないかと思います。それはそれでありなのではないかと思いますが、運用的にはかなり煩雑になるというのもあり、今回は避けました。
終わりに
もう少し改良の余地がありますが、DDDや他のアーキテクチャによるコーディングを実践しながらリアルタイムバリデーションしたいという方の参考になれば幸甚の至り。