Skip to content

🎨 useCustomFieldValue - フォームカスタマイズの神器

🌟 紹介

フォームで独自のコンポーネントを使用したいですか?伝統的なフォームコントロールの制限を突破したいですか?🎨 useCustomFieldValue はあなたのフォームカスタマイズの神器です!

この強力な Hook は、あなたの創造的なコンポーネントと Vant フォームシステムを繋ぐ架け橋のような存在です:

  • 🔗 シームレスな統合 - どんなコンポーネントでもフォームアイテムにすることができます
  • 📊 データ同期 - フォームデータの収集と検証を自動的に処理します
  • 🎮 完全な制御 - コンポーネントの独立性と柔軟性を維持します
  • 🚀 ゼロ設定 - 一行のコードでフォーム統合を完成

🎯 コア機能:

  • 🎨 カスタムフォームアイテム - どんなコンポーネントでもフォームコントロールにできます
  • 📋 フォームデータ管理 - フォームデータの収集に自動的に参加します
  • 検証サポート - フォーム検証メカニズムを完璧にサポート
  • 🔄 リアクティブ更新 - データの変更が自動的にフォームに同期されます

🚀 コードデモ

🎨 基本的な使い方 - カスタム評価コンポーネント

最も一般的なシナリオ:星評価のフォームアイテムを作成する

html
<!-- 🌟 StarRating.vue - カスタム星評価コンポーネント -->
<template>
  <div class="star-rating">
    <div class="rating-display">
      <span 
        v-for="star in 5" 
        :key="star"
        class="star"
        :class="{ active: star <= currentRating, hover: star <= hoverRating }"
        @click="setRating(star)"
        @mouseenter="hoverRating = star"
        @mouseleave="hoverRating = 0"
      >
        {{ star <= (hoverRating || currentRating) ? '⭐' : '☆' }}
      </span>
    </div>
    
    <div class="rating-info">
        <span class="rating-text">{{ ratingText }}</span>
        <span class="rating-value">({{ currentRating }}/5)</span>
      </div>
      
      <div class="rating-description" v-if="currentRating > 0">
        {{ ratingDescriptions[currentRating - 1] }}
      </div>
  </div>
</template>
js
// 🌟 StarRating.vue
import { ref, computed } from 'vue';
import { useCustomFieldValue } from '@vant/use';

export default {
  name: 'StarRating',
  setup() {
    const currentRating = ref(0);
    const hoverRating = ref(0);
    
    // 🎯 コア:コンポーネントの値をフォームシステムに登録
    useCustomFieldValue(() => currentRating.value);
    
    // 🎨 評価の説明
    const ratingDescriptions = [
      '😞 とても不満',
      '😐 あまり満足していない', 
      '😊 普通',
      '😄 比較的満足',
      '🤩 非常に満足'
    ];
    
    // 🎯 評価テキストを計算
    const ratingText = computed(() => {
      if (currentRating.value === 0) return '星をクリックして評価してください';
      return ratingDescriptions[currentRating.value - 1];
    });
    
    // 🎮 評価を設定
    const setRating = (rating) => {
      currentRating.value = rating;
      console.log(`⭐ ユーザー評価:${rating}星`);
    };
    
    return {
      currentRating,
      hoverRating,
      ratingText,
      ratingDescriptions,
      setRating
    };
  }
};
html
<!-- 📋 カスタム評価コンポーネントを使用したフォーム -->
<template>
  <div class="rating-form-demo">
    <van-form @submit="handleSubmit" ref="formRef">
      <!-- 📝 基本情報 -->
      <van-field
        v-model="formData.productName"
        name="productName"
        label="📦 商品名"
        placeholder="商品名を入力してください"
        :rules="[{ required: true, message: '商品名を入力してください' }]"
      />
      
      <!-- 🌟 カスタム評価フォームアイテム -->
      <van-field 
        name="rating" 
        label="⭐ 商品評価"
        :rules="[
          { required: true, message: '商品を評価してください' },
          { validator: validateRating }
        ]"
      >
        <template #input>
          <star-rating />
        </template>
      </van-field>
      
      <!-- 💬 評価内容 -->
      <van-field
        v-model="formData.comment"
        name="comment"
        label="💬 評価内容"
        type="textarea"
        placeholder="ご使用体験を共有してください..."
        rows="3"
      />
      
      <!-- 📸 画像アップロード -->
      <van-field name="images" label="📸 写真共有">
        <template #input>
          <image-uploader />
        </template>
      </van-field>
      
      <!-- 🎯 送信ボタン -->
      <div class="form-actions">
        <van-button 
          type="primary" 
          native-type="submit" 
          block
          :loading="isSubmitting"
        >
          {{ isSubmitting ? '📤 送信中...' : '✅ 評価を送信' }}
        </van-button>
      </div>
    </van-form>
    
    <!-- 📊 フォームデータのプレビュー -->
    <div class="form-preview" v-if="showPreview">
      <h4>📊 フォームデータのプレビュー</h4>
      <pre>{{ JSON.stringify(lastSubmitData, null, 2) }}</pre>
    </div>
  </div>
</template>
js
// 📋 フォームページのロジック
import { ref, reactive } from 'vue';
import StarRating from './components/StarRating.vue';
import ImageUploader from './components/ImageUploader.vue';

export default {
  components: {
    StarRating,
    ImageUploader
  },
  setup() {
    const formRef = ref();
    const isSubmitting = ref(false);
    const showPreview = ref(false);
    const lastSubmitData = ref(null);
    
    // 📝 フォームデータ
    const formData = reactive({
      productName: '',
      comment: ''
    });
    
    // ✅ 評価検証器
    const validateRating = (value) => {
      if (!value || value === 0) {
        return '商品を評価してください';
      }
      if (value < 1 || value > 5) {
        return '評価は1-5星の間でなければなりません';
      }
      return true;
    };
    
    // 📤 フォームを送信
    const handleSubmit = async (values) => {
      console.log('📋 フォーム送信データ:', values);
      
      isSubmitting.value = true;
      
      try {
        // 🌐 API送信のシミュレーション
        await new Promise(resolve => setTimeout(resolve, 2000));
        
        // ✅ 送信成功
        lastSubmitData.value = values;
        showPreview.value = true;
        
        console.log('✅ 評価の送信が成功しました!', {
          商品名: values.productName,
          評価: `${values.rating}星`,
          評価内容: values.comment,
          画像数: values.images?.length || 0
        });
        
        // 🎉 成功通知
        await showSuccessToast('🎉 評価が送信されました!フィードバックありがとうございます!');
        
        // 🔄 フォームをリセット
        formRef.value?.resetValidation();
        Object.assign(formData, {
          productName: '',
          comment: ''
        });
        
      } catch (error) {
        console.error('❌ 送信に失敗しました:', error);
        showFailToast('❌ 送信に失敗しました。再試行してください');
      } finally {
        isSubmitting.value = false;
      }
    };
    
    return {
      formRef,
      formData,
      isSubmitting,
      showPreview,
      lastSubmitData,
      validateRating,
      handleSubmit
    };
  }
};

🎮 高度な使い方 - カスタムスライダーコンポーネント

アニメーション効果付きの価格範囲セレクターを作成:

html
<!-- 💰 PriceRangeSlider.vue - 価格範囲スライダー -->
<template>
  <div class="price-range-slider">
    <div class="price-display">
      <div class="price-item">
        <label>💰 最低価格</label>
        <div class="price-value">¥{{ range.min }}</div>
      </div>
      <div class="price-separator">-</div>
      <div class="price-item">
        <label>💎 最高価格</label>
        <div class="price-value">¥{{ range.max }}</div>
      </div>
    </div>
    
    <div class="slider-container">
      <!-- 🎚️ デュアルスライダー実装 -->
      <div class="slider-track" ref="trackRef">
        <div 
          class="slider-range" 
          :style="rangeStyle"
        ></div>
        
        <div 
          class="slider-thumb min-thumb"
          :style="{ left: minThumbPosition }"
          @mousedown="startDrag('min', $event)"
          @touchstart="startDrag('min', $event)"
        >
          <div class="thumb-tooltip">¥{{ range.min }}</div>
        </div>
        
        <div 
          class="slider-thumb max-thumb"
          :style="{ left: maxThumbPosition }"
          @mousedown="startDrag('max', $event)"
          @touchstart="startDrag('max', $event)"
        >
          <div class="thumb-tooltip">¥{{ range.max }}</div>
        </div>
      </div>
    </div>
    
    <div class="price-presets">
      <span class="preset-label">🎯 クイック選択:</span>
      <button 
        v-for="preset in pricePresets"
        :key="preset.label"
        class="preset-btn"
        :class="{ active: isPresetActive(preset) }"
        @click="applyPreset(preset)"
      >
        {{ preset.label }}
      </button>
    </div>
  </div>
</template>
js
// 💰 PriceRangeSlider.vue
import { ref, computed, reactive, onMounted, onUnmounted } from 'vue';
import { useCustomFieldValue } from '@vant/use';

export default {
  name: 'PriceRangeSlider',
  props: {
    min: { type: Number, default: 0 },
    max: { type: Number, default: 10000 },
    step: { type: Number, default: 100 }
  },
  setup(props) {
    const trackRef = ref();
    const isDragging = ref(false);
    const dragType = ref('');
    
    // 💰 価格範囲の状態
    const range = reactive({
      min: props.min,
      max: props.max
    });
    
    // 🎯 価格範囲をフォームに登録
    useCustomFieldValue(() => ({
      min: range.min,
      max: range.max,
      formatted: `¥${range.min} - ¥${range.max}`
    }));
    
    // 🎨 プリセット価格範囲
    const pricePresets = [
      { label: '💸 0-1000', min: 0, max: 1000 },
      { label: '💰 1000-3000', min: 1000, max: 3000 },
      { label: '💎 3000-5000', min: 3000, max: 5000 },
      { label: '👑 5000+', min: 5000, max: 10000 }
    ];
    
    // 📊 スライダー位置を計算
    const minThumbPosition = computed(() => {
      const percent = (range.min - props.min) / (props.max - props.min) * 100;
      return `${percent}%`;
    });
    
    const maxThumbPosition = computed(() => {
      const percent = (range.max - props.min) / (props.max - props.min) * 100;
      return `${percent}%`;
    });
    
    const rangeStyle = computed(() => {
      const minPercent = (range.min - props.min) / (props.max - props.min) * 100;
      const maxPercent = (range.max - props.min) / (props.max - props.min) * 100;
      return {
        left: `${minPercent}%`,
        width: `${maxPercent - minPercent}%`
      };
    });
    
    // 🎮 ドラッグ処理
    const startDrag = (type, event) => {
      isDragging.value = true;
      dragType.value = type;
      
      const handleMove = (e) => {
        if (!isDragging.value) return;
        
        const rect = trackRef.value.getBoundingClientRect();
        const clientX = e.clientX || e.touches[0].clientX;
        const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
        const value = Math.round((props.min + percent * (props.max - props.min)) / props.step) * props.step;
        
        if (type === 'min') {
          range.min = Math.min(value, range.max - props.step);
        } else {
          range.max = Math.max(value, range.min + props.step);
        }
        
        console.log(`💰 価格範囲更新: ¥${range.min} - ¥${range.max}`);
      };
      
      const handleEnd = () => {
        isDragging.value = false;
        dragType.value = '';
        document.removeEventListener('mousemove', handleMove);
        document.removeEventListener('mouseup', handleEnd);
        document.removeEventListener('touchmove', handleMove);
        document.removeEventListener('touchend', handleEnd);
      };
      
      document.addEventListener('mousemove', handleMove);
      document.addEventListener('mouseup', handleEnd);
      document.addEventListener('touchmove', handleMove);
      document.addEventListener('touchend', handleEnd);
      
      event.preventDefault();
    };
    
    // 🎯 プリセットを適用
    const applyPreset = (preset) => {
      range.min = preset.min;
      range.max = preset.max;
      console.log(`🎯 プリセットを適用: ${preset.label}`);
    };
    
    // ✅ プリセットがアクティブかどうかをチェック
    const isPresetActive = (preset) => {
      return range.min === preset.min && range.max === preset.max;
    };
    
    return {
      trackRef,
      range,
      pricePresets,
      minThumbPosition,
      maxThumbPosition,
      rangeStyle,
      startDrag,
      applyPreset,
      isPresetActive
    };
  }
};

📊 複雑なシナリオ - 複数選択タグコンポーネント

検索機能付きの複数選択タグフォームアイテムを作成:

html
<!-- 🏷️ TagSelector.vue - 複数選択タグコンポーネント -->
<template>
  <div class="tag-selector">
    <!-- 🔍 検索ボックス -->
    <div class="search-section">
      <van-field
        v-model="searchKeyword"
        placeholder="🔍 タグを検索..."
        clearable
        @input="handleSearch"
      >
        <template #left-icon>
          <van-icon name="search" />
        </template>
      </van-field>
    </div>
    
    <!-- 🏷️ 選択済みタグ -->
    <div class="selected-tags" v-if="selectedTags.length > 0">
      <div class="section-title">
        ✅ 選択済みタグ ({{ selectedTags.length }}/{{ maxSelection }})
      </div>
      <div class="tag-list">
        <van-tag
          v-for="tag in selectedTags"
          :key="tag.id"
          type="primary"
          closeable
          @close="removeTag(tag)"
        >
          {{ tag.emoji }} {{ tag.name }}
        </van-tag>
      </div>
    </div>
    
    <!-- 🎯 選択可能なタグ -->
    <div class="available-tags">
      <div class="section-title">
        🎯 選択可能なタグ ({{ filteredTags.length }})
      </div>
      
      <!-- 📂 カテゴリータグ -->
      <div class="category-tabs">
        <button
          v-for="category in categories"
          :key="category.id"
          class="category-tab"
          :class="{ active: activeCategory === category.id }"
          @click="setActiveCategory(category.id)"
        >
          {{ category.emoji }} {{ category.name }}
        </button>
      </div>
      
      <!-- 🏷️ タググリッド -->
      <div class="tag-grid">
        <div
          v-for="tag in filteredTags"
          :key="tag.id"
          class="tag-item"
          :class="{ 
            selected: isTagSelected(tag),
            disabled: !canSelectTag(tag)
          }"
          @click="toggleTag(tag)"
        >
          <span class="tag-emoji">{{ tag.emoji }}</span>
          <span class="tag-name">{{ tag.name }}</span>
          <span class="tag-count" v-if="tag.count">({{ tag.count }})</span>
        </div>
      </div>
      
      <!-- 📝 カスタムタグ -->
      <div class="custom-tag-section">
        <van-field
          v-model="customTagName"
          placeholder="💡 カスタムタグを作成..."
          @keyup.enter="addCustomTag"
        >
          <template #button>
            <van-button 
              size="small" 
              type="primary"
              :disabled="!customTagName.trim()"
              @click="addCustomTag"
            >
              ➕ 追加
            </van-button>
          </template>
        </van-field>
      </div>
    </div>
  </div>
</template>
js
// 🏷️ TagSelector.vue
import { ref, computed, reactive } from 'vue';
import { useCustomFieldValue } from '@vant/use';

export default {
  name: 'TagSelector',
  props: {
    maxSelection: { type: Number, default: 5 },
    allowCustom: { type: Boolean, default: true }
  },
  setup(props) {
    const searchKeyword = ref('');
    const activeCategory = ref('all');
    const customTagName = ref('');
    const selectedTags = ref([]);
    
    // 🎯 選択されたタグをフォームに登録
    useCustomFieldValue(() => ({
      tags: selectedTags.value,
      tagIds: selectedTags.value.map(tag => tag.id),
      tagNames: selectedTags.value.map(tag => tag.name),
      count: selectedTags.value.length
    }));
    
    // 📂 タグカテゴリー
    const categories = [
      { id: 'all', name: 'すべて', emoji: '🌟' },
      { id: 'tech', name: 'テクノロジー', emoji: '💻' },
      { id: 'design', name: 'デザイン', emoji: '🎨' },
      { id: 'business', name: 'ビジネス', emoji: '💼' },
      { id: 'life', name: 'ライフ', emoji: '🌱' }
    ];
    
    // 🏷️ プリセットタグ
    const allTags = ref([
      // テクノロジーカテゴリー
      { id: 1, name: 'Vue.js', emoji: '💚', category: 'tech', count: 1234 },
      { id: 2, name: 'React', emoji: '⚛️', category: 'tech', count: 2345 },
      { id: 3, name: 'TypeScript', emoji: '🔷', category: 'tech', count: 987 },
      { id: 4, name: 'Node.js', emoji: '🟢', category: 'tech', count: 1567 },
      
      // デザインカテゴリー
      { id: 5, name: 'UIデザイン', emoji: '🎨', category: 'design', count: 876 },
      { id: 6, name: 'UX体験', emoji: '✨', category: 'design', count: 654 },
      { id: 7, name: 'プロトタイプデザイン', emoji: '📐', category: 'design', count: 432 },
      
      // ビジネスカテゴリー
      { id: 8, name: '製品管理', emoji: '📊', category: 'business', count: 789 },
      { id: 9, name: 'マーケティング', emoji: '📈', category: 'business', count: 567 },
      { id: 10, name: 'データ分析', emoji: '📉', category: 'business', count: 345 },
      
      // ライフカテゴリー
      { id: 11, name: 'フィットネス', emoji: '💪', category: 'life', count: 234 },
      { id: 12, name: '料理', emoji: '🍳', category: 'life', count: 456 },
      { id: 13, name: '旅行写真', emoji: '📸', category: 'life', count: 678 }
    ]);
    
    // 🔍 タグをフィルタリング
    const filteredTags = computed(() => {
      let tags = allTags.value;
      
      // カテゴリーでフィルタリング
      if (activeCategory.value !== 'all') {
        tags = tags.filter(tag => tag.category === activeCategory.value);
      }
      
      // 検索キーワードでフィルタリング
      if (searchKeyword.value.trim()) {
        const keyword = searchKeyword.value.toLowerCase();
        tags = tags.filter(tag => 
          tag.name.toLowerCase().includes(keyword) ||
          tag.emoji.includes(keyword)
        );
      }
      
      return tags;
    });
    
    // 🎮 タグ操作
    const toggleTag = (tag) => {
      if (!canSelectTag(tag)) return;
      
      if (isTagSelected(tag)) {
        removeTag(tag);
      } else {
        addTag(tag);
      }
    };
    
    const addTag = (tag) => {
      if (selectedTags.value.length >= props.maxSelection) {
        showToast(`最大で ${props.maxSelection} 個まで選択可能です`);
        return;
      }
      
      selectedTags.value.push(tag);
      console.log(`✅ タグを追加: ${tag.emoji} ${tag.name}`);
    };
    
    const removeTag = (tag) => {
      const index = selectedTags.value.findIndex(t => t.id === tag.id);
      if (index > -1) {
        selectedTags.value.splice(index, 1);
        console.log(`❌ タグを削除: ${tag.emoji} ${tag.name}`);
      }
    };
    
    const isTagSelected = (tag) => {
      return selectedTags.value.some(t => t.id === tag.id);
    };
    
    const canSelectTag = (tag) => {
      return !isTagSelected(tag) && selectedTags.value.length < props.maxSelection;
    };
    
    // 📂 カテゴリー切り替え
    const setActiveCategory = (categoryId) => {
      activeCategory.value = categoryId;
      console.log(`📂 カテゴリーを切り替え: ${categoryId}`);
    };
    
    // 🔍 検索処理
    const handleSearch = (value) => {
      console.log(`🔍 タグを検索: ${value}`);
    };
    
    // 💡 カスタムタグの追加
    const addCustomTag = () => {
      const name = customTagName.value.trim();
      if (!name) return;
      
      // 既に存在するかチェック
      const exists = allTags.value.some(tag => 
        tag.name.toLowerCase() === name.toLowerCase()
      );
      
      if (exists) {
        showToast('このタグは既に存在します');
        return;
      }
      
      // 新しいタグを作成
      const newTag = {
        id: Date.now(),
        name,
        emoji: '💡',
        category: 'custom',
        count: 0,
        isCustom: true
      };
      
      allTags.value.push(newTag);
      addTag(newTag);
      customTagName.value = '';
      
      console.log(`💡 カスタムタグを作成: ${name}`);
    };
    
    return {
      searchKeyword,
      activeCategory,
      customTagName,
      selectedTags,
      categories,
      filteredTags,
      toggleTag,
      removeTag,
      isTagSelected,
      canSelectTag,
      setActiveCategory,
      handleSearch,
      addCustomTag
    };
  }
};

📚 API 参考

🔧 型定義

ts
// 🎯 カスタムフォーム値関数
function useCustomFieldValue(customValue: () => unknown): void;

// 💡 使用例の型
type CustomValue = 
  | string          // 🔤 シンプルな文字列
  | number          // 🔢 数値
  | boolean         // ✅ ブール値
  | object          // 📦 複雑なオブジェクト
  | Array<any>      // 📋 配列データ
  | null            // 🚫 空値
  | undefined;      // ❓ 未定義

// 🎨 一般的なカスタムコンポーネント値の型
interface RatingValue {
  rating: number;           // ⭐ 評価値
  description?: string;     // 📝 評価の説明
}

interface PriceRangeValue {
  min: number;             // 💰 最小価格
  max: number;             // 💎 最大価格
  formatted: string;       // 🎨 フォーマット済み表示
}

interface TagsValue {
  tags: Tag[];             // 🏷️ 标签数组
  tagIds: number[];        // 🆔 标签ID数组
  tagNames: string[];      // 📝 标签名称数组
  count: number;           // 📊 标签数量
}

📋 パラメータ説明

パラメータ説明デフォルト値
customValue🎯 フォームアイテムの値を取得する関数
💡 戻り値はフォームデータの収集と検証に使用されます
() => unknown-

🎮 使用ポイント

✅ 正しい使い方

  1. 🎯 コンポーネント内で呼び出す

    js
    // ✅ カスタムコンポーネントの setup 内で呼び出す
    export default {
      setup() {
        const value = ref('');
        useCustomFieldValue(() => value.value);
        return { value };
      }
    };
  2. 📊 リアクティブデータを返す

    js
    // ✅ リアクティブデータを返し、自動的に同期更新
    const data = reactive({ count: 0, name: '' });
    useCustomFieldValue(() => data);
  3. 🔄 動的に計算された値

    js
    // ✅ 計算プロパティまたは動的な値を返す
    const items = ref([]);
    useCustomFieldValue(() => ({
      items: items.value,
      count: items.value.length,
      isEmpty: items.value.length === 0
    }));

❌ 避けるべき使い方

  1. 🚫 コンポーネントの外部で呼び出す

    js
    // ❌ コンポーネントの外部で呼び出さない
    const value = ref('');
    useCustomFieldValue(() => value.value); // ✅ 正しい使い方
    
    export default {
      setup() {
        return { value };
      }
    };
  2. 🚫 非リアクティブデータを返す

    js
    // ❌ 静的な値を返すと、変化に反応できない
    useCustomFieldValue(() => 'static value');

🎯 実際の使用シナリオ

🛒 電子商取引シナリオ

js
// 🌟 商品評価コンポーネント
const ratingComponent = () => {
  const rating = ref(0);
  useCustomFieldValue(() => ({
    rating: rating.value,
    text: getRatingText(rating.value)
  }));
};

// 💰 価格範囲セレクター
const priceRangeComponent = () => {
  const range = reactive({ min: 0, max: 1000 });
  useCustomFieldValue(() => range);
};

📱 モバイルアプリ

js
// 📸 画像アップロードコンポーネント
const imageUploaderComponent = () => {
  const images = ref([]);
  useCustomFieldValue(() => ({
    images: images.value,
    count: images.value.length,
    urls: images.value.map(img => img.url)
  }));
};

// 📍 住所セレクター
const addressPickerComponent = () => {
  const address = reactive({
    province: '',
    city: '',
    district: '',
    detail: ''
  });
  useCustomFieldValue(() => address);
};

🏢 企業アプリ

js
// 👥 メンバーセレクター
const memberSelectorComponent = () => {
  const selectedMembers = ref([]);
  useCustomFieldValue(() => ({
    members: selectedMembers.value,
    memberIds: selectedMembers.value.map(m => m.id),
    count: selectedMembers.value.length
  }));
};

// 📊 データチャートコンポーネント
const chartComponent = () => {
  const chartData = ref(null);
  useCustomFieldValue(() => ({
    data: chartData.value,
    type: 'chart',
    timestamp: Date.now()
  }));
};

💡 ベストプラクティス

✅ 推奨される方法

  1. 🎯 明確なデータ構造

    js
    // ✅ フォーム処理に適した構造化データを返す
    useCustomFieldValue(() => ({
      value: currentValue.value,
      displayText: getDisplayText(),
      isValid: validateValue(),
      metadata: getMetadata()
    }));
  2. 🔄 リアクティブデータの同期

    js
    // ✅ リアクティブデータを使用して、リアルタイム同期を確保
    const formValue = computed(() => ({
      ...baseData.value,
      computed: calculateValue()
    }));
    useCustomFieldValue(() => formValue.value);
  3. ✅ データ検証のサポート

    js
    // ✅ フォーム検証と組み合わせて使用する
    const validateCustomValue = (value) => {
      if (!value || !value.required) {
        return '必須項目を入力してください';
      }
      return true;
    };

❌ 避けるべき方法

  1. 🚫 頻繁な複雑な計算

    js
    // ❌ 返却関数内での複雑な計算を避ける
    useCustomFieldValue(() => {
      // 複雑な計算ロジック...
      return heavyCalculation(); // ✅ 必要な場合のみ計算
    });
  2. 🚫 副作用の操作

    js
    // ❌ 返却関数内で副作用を実行しない
    useCustomFieldValue(() => {
      console.log('value changed'); // ✅ 必要な場合のみログ出力
      updateOtherState(); // ✅ 必要な場合のみ更新
      return value.value;
    });

🛠️ デバッグテクニック

🔍 データモニタリング

js
// 📊 フォーム値の変化を監視
const debugCustomValue = () => {
  const value = ref('');
  
  useCustomFieldValue(() => {
    const currentValue = value.value;
    console.log('🎯 カスタムフォーム値が更新されました:', {
      value: currentValue,
      type: typeof currentValue,
      timestamp: new Date().toISOString()
    });
    return currentValue;
  });
  
  return { value };
};

🧪 フォーム統合テスト

js
// 🧪 フォームデータ収集のテスト
const testFormIntegration = () => {
  const testValue = ref({ test: true });
  
  useCustomFieldValue(() => {
    console.log('📋 フォームがデータを収集:', testValue.value);
    return testValue.value;
  });
  
  // 🎮 データ変化のシミュレーション
  setTimeout(() => {
    testValue.value = { test: false, updated: true };
  }, 1000);
};

📚 関連ドキュメント

📋 フォーム関連

🎮 状態管理

🛠️ 開発ツール

💡 実践ケース

Vant に基づく企業向けモバイルソリューション