LoginSignup
0
0

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

Last updated at Posted at 2023-08-04

やりたいこと

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

手順

  1. composablesとしてフォームのfunction及びstoreをコンポーネントから独立させる。
  2. organismsでFormをインスタンス化してprovide。
  3. 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されたデータの展開及び検証処理を行う必要がある。
0
0
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
0
0