0
0

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

Posted at

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

GitHub アドレス:

連絡先メール:

yangrongwei1996@gmail.com

機能概要

複雑な HTML コードを避けるために、formOptions を使って動的にフォームを生成します。
フォームは以下の機能を持っています:

  • フォームのすべてのイベントをリッスン
  • フォームコンポーネントの値の変化をリッスン
  • フォーム検証: 自動または手動でフォームの検証

デモリンク:デモリンク

実装手順

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

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

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

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

// 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 テンプレートを繰り返し作成する必要がなくなります。

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