やりたいこと
- AtomicDesignに準拠したフォームの切り出しを行いたい。
- moleculeやatomとしてパーツを切り出すと、親→子に限定しているデータフローが崩れてしまう。
- composition API + provide-injectパターンを使用して実装できないだろうか?
手順
- composablesとしてフォームのfunction及びstoreをコンポーネントから独立させる。
- organismsでFormをインスタンス化してprovide。
- atomsで上記のインスタンスをinjectしてv-model等の処理を行う。
ディレクトリ構成
src/
├ components
│ ├ atoms
│ │ ├ FormInput.vue
│ │ └ SubmitButton.vue
│ ├ molecules
│ │ ├ InputMail.vue
│ │ ├ InputName.vue
│ │ └ InputPassword.vue
│ └ organisms
│ ├ LoginForm.vue
│ └ SignupForm.vue
│
├ views
│ └ Form.vue
│
├ composables
│ └ useForm.vue
.
.
.
Composables
useForm.ts
import { inject, reactive } from "vue";
// 汎用的なinterfaceを定義
export interface FormState {
email: string;
password: string;
[key: string]: string;
}
export class Form {
// フォームの値をオブジェクトとして保持
private formData: FormState;
constructor(data: FormState) {
this.formData = reactive(data);
}
// ゲッターを定義
get formValue(): FormState {
if (this.formData) {
return this.formData;
}
throw new Error("formDate is not found");
}
// formData内に特定のkeyが存在するか検証
public isExist(key: string): boolean {
return this.formData[key] !== undefined;
}
// formDataのプロパティに値を代入
public setFormValue(k: string, v: string): void {
this.formData[k] = v;
}
}
// useForm関数を呼び出すと同時にinject
const useForm = (): Form => {
const _form = inject<Form>("form");
// 親コンポーネントにて正しくprovideされていない場合の処理
if (!_form) {
throw new Error('formが正しくprovideされていません');
}
return _form;
};
export default useForm;
Organisms
コンポーネントが再利用可能であるか確認するため、以下2種のフォームを用意して検証する。
- LoginForm.vue
- SignupForm.vue
LoginForm.vue
<template>
<form>
<h2>ログイン</h2>
<div class="mb-3">
<input-mail :error="errorBag.email"></input-mail>
</div>
<div class="mb-3">
<input-password :error="errorBag.password"></input-password>
</div>
<div class="mb-3">
<submit-button label="ログイン" @click="submitForm"></submit-button>
</div>
<p><router-link to="/signup">新規会員登録</router-link>する</p>
</form>
</template>
<script lang="ts">
import { Form } from "../../composables/useForm";
import { defineComponent, provide, reactive } from "vue";
import SubmitButton from "../atoms/SubmitButton.vue";
import InputMail from "../molecules/InputMail.vue";
import InputPassword from "../molecules/InputPassword.vue";
import Validator from "validatorjs";
export default defineComponent({
name: "LoginForm",
components: { SubmitButton, InputPassword, InputMail },
setup() {
// Formクラスをインスタンス化
const form = new Form({
email: "",
password: "",
});
// バリデーションルールを定義
const rules = {
email: "required|email",
password: "required|min:8|max:20",
};
// エラーを格納するオブジェクトを定義
const errorBag: {
email: string | false;
password: string | false;
} = reactive({
email: "",
password: "",
});
// 生成したインスタンスをatomにProvide
provide("form", form);
// submitを処理(今回はalertを表示)
const submitForm = () => {
// validatorjsをインスタンス化
const validation = new Validator(form.formValue, rules);
// バリデーション処理
if (validation.fails()) {
errorBag.email = validation.errors.first("email");
errorBag.password = validation.errors.first("password");
return false;
}
// alertを表示
alert("Successfuly submitted!");
};
return {
submitForm,
errorBag,
};
},
});
</script>
Molecules
InputName.vue
<template>
<label for="name" class="form-label">ユーザー名</label>
<form-input type="text" val="name"></form-input>
<small v-if="error" class="text-danger">{{ error }}</small>
</template>
<script lang="type">
import { defineComponent } from "vue";
import FormInput from "../atoms/FormInput.vue";
export default defineComponent({
name: "InputName",
components: { FormInput },
props: {
error: {
type: [String, Boolean],
required: true
}
},
});
</script>
Atoms
FormInput.vue
<template>
<input :type="type" v-model="value" class="form-control" />
</template>
<script lang="ts">
import useForm from "../../composables/useForm";
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
name: "SubmitButton",
props: {
type: {
type: String,
default: "text",
},
val: {
type: String,
required: true,
},
},
setup(props) {
// organismsでprovideされたcomposableを実行と同時にinject
const form = useForm();
const value = ref("");
// formDataに該当するkeyが存在しなかった場合の処理
if (!form.isExist(props.val)) {
throw new Error("provideされたインスタンスに正しいプロパティが存在しません");
}
// inputのvalueが変更される度にsetFormValueを発火
watchEffect(() => {
form.setFormValue(props.val, value.value);
});
return {
value,
};
},
});
</script>
まとめ
- event upさせないで実装することは可能(だと思う)。
- atomにてinjectされたデータの展開及び検証処理を行う必要がある。