【vue3-jp-admin】: Vuetify3.0フォームコンポーネントのカプセル化


### 【Vue3 jp admin】にご注目いただきありがとうございます。今回のカプセル化は Vuetify3.0 の Formコンポーネント です。

## GitHub アドレス:
https://github.com/yangrongwe/vue3-jp-admin
## 連絡先メール:
yangrongwei1996@gmail.com

### 機能概要
複雑な HTML コードを避けるために、`formOptions` を使って動的にフォームを生成します。
フォームは以下の機能を持っています：
- フォームのすべてのイベントをリッスン
- フォームコンポーネントの値の変化をリッスン
- フォーム検証: 自動または手動でフォームの検証

デモリンク：[デモリンク](https://wxcorer.com/components/form)

### 実装手順

#### 1. `v-form` コンテナの作成

```vue
<template>
  <v-form ref="formRef" v-bind="$attrs" class="tw-px-8 tw-pt-1 tw-py-12">
    <template v-for="item in formOptions.formItems" :key="item.itemName">
      <component
        :is="getComponent(item.itemType)"
        :item="item"
        v-bind="item.props"
        v-on="item.eventHandlers"
        :rules="formOptions.rules[item.itemName] || []"
        class="tw-flex-grow"
        @update:modelValue="handleModelValueUpdate"
      />
    </template>
  </v-form>
</template>

<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue';
import type { JpFormOptions } from './type.ts';

// コンポーネントを動的にインポートする関数
const getComponent = (type: string) => {
  const components = {
    customEl: defineAsyncComponent(() => import('./JpCustomEl/index.vue')),
    autoComplete: defineAsyncComponent(
      () => import('./JpAutoComplete/index.vue')
    ),
    button: defineAsyncComponent(() => import('./JpButton/index.vue')),
    checkbox: defineAsyncComponent(() => import('./JpCheckbox/index.vue')),
    input: defineAsyncComponent(() => import('./JpInput/index.vue')),
    radioBtn: defineAsyncComponent(() => import('./JpRadioBtn/index.vue')),
    rangeSlider: defineAsyncComponent(
      () => import('./JpRangeSlider/index.vue')
    ),
    datePicker: defineAsyncComponent(() => import('./JpDatePicker/index.vue')),
    timePicker: defineAsyncComponent(() => import('./JpTimePicker/index.vue')),
    image: defineAsyncComponent(() => import('./JpImage/index.vue')),
    select: defineAsyncComponent(() => import('./JpSelect/index.vue')),
    switch: defineAsyncComponent(() => import('./JpSwitch/index.vue')),
    textarea: defineAsyncComponent(() => import('./JpTextarea/index.vue')),
  };
  return components[type] || null;
};

// プロパティの定義
const props = defineProps({
  formOptions: {
    type: Object as () => JpFormOptions,
    default: () => ({
      type: 'vuetify',
      formItems: [],
      rules: [],
    }),
  },
});

const formRef = ref(null); // フォームの参照
const formData = ref<{ [key: string]: any }>({}); // フォームデータの格納

// モデル値の更新を処理する関数
const handleModelValueUpdate = ({ itemName, value }) => {
  formData.value[itemName] = value;
};

// フォームのバリデーションを行う関数
const validateForm = async () => {
  if (formRef.value) {
    const validationResults = await formRef.value.validate();
    if (validationResults.valid) {
      console.log('フォームは有効です');
      return true;
    } else {
      console.log('フォームは無効です');
      return false;
    }
  }
};

// formRef のインスタンスメソッドを公開
defineExpose({
  validateForm,
  formData,
});
</script>

<style scoped></style>
```
#### 2. 子コンポーネントの作成 (例: Input)

```vue
<template>
  <!-- password -->
  <div v-if="item.props.type === 'password'" class="tw-flex">
    <div
      v-if="item.label"
      class="tw-mr-6 tw-mt-[12px]"
      :style="{ width: item.labelWidth }"
    >
      {{ item.label }}
    </div>
    <v-text-field
      :id="item.itemName"
      v-bind="item.props"
      v-model="inputValue"
      :rules="rules"
      @click:append-inner="passwordVisible = !passwordVisible"
      :type="passwordVisible ? 'text' : 'password'"
      :append-inner-icon="passwordVisible ? 'mdi-eye-off' : 'mdi-eye'"
      variant="outlined"
      density="comfortable"
    >
    </v-text-field>
  </div>
  <!-- input -->
  <div v-else class="tw-flex">
    <div
      v-if="item.label"
      class="tw-mr-6 tw-mt-[12px]"
      :style="{ width: item.labelWidth }"
    >
      {{ item.label }}
    </div>
    <v-text-field
      :id="item.itemName"
      v-bind="item.props"
      v-model="inputValue"
      :rules="rules"
      variant="outlined"
      density="comfortable"
    >
    </v-text-field>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';

const props = defineProps({
  item: {
    type: Object,
    required: true,
    default: () => ({}),
  },
  rules: {
    type: Array,
    default: () => [],
  },
});

const emit = defineEmits(['update:modelValue']);

const inputValue = ref(props.item.props.modelValue || '');

const passwordVisible = ref(false);

watch(inputValue, (newValue) => {
  emit('update:modelValue', { itemName: props.item.itemName, value: newValue });
});
</script>

```
#### 3. コンポーネントの使用（HTML）

```vue
<script setup lang="ts">
import JpMainArea from '@/components/JpLayout/layout/JpMainArea.vue';
import JpForm from '@/components/JpForm/index.vue';
import { mainTitle, formRef, formOptions } from './hook.tsx';
</script>

<template>
  <jp-main-area :main-title="mainTitle">
    <jp-form
      ref="formRef"
      :form-options="formOptions"
      class="tw-max-w-[700px]"
    ></jp-form>
  </jp-main-area>
</template>
```
#### 4. コンポーネントの使用と formOptions の設定（hook）

```tsx
// src/hooks/formHook.ts
import { ref, reactive } from 'vue';
import { MainTitle } from '@/types/index';
import { JpFormOptions } from '@/components/JpForm/type.ts';
import { VCard } from 'vuetify/components';
import { $t } from '@/plugins/i18n/i18nUtils';

const mainTitle: MainTitle = {
  title: $t('title.components.form'),
  linkText: 'src/views/componentsDemo/form/index.vue',
  path: 'https://github.com/yangrongwe/vue3-jp-admin/blob/main/src/views/componentsDemo/form/index.vue',
};

const formRef = ref<any>(null);

const formOptions = reactive<JpFormOptions>({
  formItems: [
    // メールアドレス入力項目
    {
      itemType: 'input',
      itemName: 'email',
      label: $t('views.form.email.label'),
      labelWidth: '150px',
      props: {
        type: 'email',
        placeholder: $t('views.form.email.placeholder'),
        prependInnerIcon: 'mdi-email-outline',
        validateOn: 'blur',
        class: 'tw-mb-1',
      },
      eventHandlers: {
        input: (event) => {
          // 入力イベントを処理
        },
        change: (el) => {
          // 変更イベントを処理
        },
      },
    },
    // パスワード入力項目
    {
      itemType: 'input',
      itemName: 'password',
      label: $t('views.form.password.label'),
      labelWidth: '150px',
      props: {
        type: 'password',
        placeholder: $t('views.form.password.placeholder'),
        prependInnerIcon: 'mdi-lock-outline',
        class: 'tw-mb-1',
      },
      eventHandlers: {
        change: (el) => {
          // 変更イベントを処理
        },
        click: () => {
          // クリックイベントを処理
        },
      },
    },
    // フロントエンドフレームワーク選択項目
    {
      itemType: 'select',
      itemName: 'frontEnd',
      label: $t('views.form.frontEnd.label'),
      labelWidth: '150px',
      props: {
        placeholder: $t('views.form.frontEnd.placeholder'),
        class: 'tw-mb-1',
        items: ['Vue', 'React', 'Angular', 'Jquery'],
        multiple: true,
        clearable: true,
      },
      eventHandlers: {
        'update:modelValue': (value) => {
          // update:modelValue イベントを処理
        },
        'update:menu': (value) => {
          // update:menu イベントを処理
        },
      },
    },
    // オートコンプリート項目
    {
      itemType: 'autoComplete',
      itemName: 'skills',
      label: $t('views.form.skills.label'),
      labelWidth: '150px',
      props: {
        items: ['JavaScript', 'TypeScript', 'Python', 'Java'],
        placeholder: $t('views.form.skills.placeholder'),
        class: 'tw-mb-1',
        clearable: true,
      },
      eventHandlers: {
        'update:modelValue': (value) => {
          // update:modelValue イベントを処理
        },
      },
    },
  ],
  rules: {
    email: [
      (v: string) => !!v || $t('validation.email.required'),
      (v: string) => /.+@.+\..+/.test(v) || $t('validation.email.invalid'),
    ],
    password: [
      (v: string) => !!v || $t('validation.password.required'),
      (v: string) => v.length >= 8 || $t('validation.password.minLength'),
    ],
  },
});


```


## まとめ: 
動的にフォームを生成することには大きな意義があります。それにより、複雑なフォームコンポーネントを迅速かつ明確に生成できるようになります。複数の場所で同じフォームを使用する場合、HTML テンプレートを繰り返し作成する必要がなくなります。