Vue3+composition-APIでevent upを避けてフォームを実装する

# やりたいこと

- AtomicDesignに準拠したフォームの切り出しを行いたい。
- moleculeやatomとしてパーツを切り出すと、親→子に限定しているデータフローが崩れてしまう。
- composition API + provide-injectパターンを使用して実装できないだろうか？

# 手順

1. composablesとしてフォームのfunction及びstoreをコンポーネントから独立させる。
1. organismsでFormをインスタンス化してprovide。
1. 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
```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

コンポーネントが再利用可能であるか確認するため、以下２種のフォームを用意して検証する。

- LoginForm.vue
- SignupForm.vue

### LoginForm.vue

```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

```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

```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されたデータの展開及び検証処理を行う必要がある。