【Vue3 jp admin】にご注目いただきありがとうございます。今回のカプセル化は Vuetify3.0 の Formコンポーネント です。
GitHub アドレス:
連絡先メール:
機能概要
複雑な 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 テンプレートを繰り返し作成する必要がなくなります。