🎨 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 | - |
🎮 使用ポイント
✅ 正しい使い方
🎯 コンポーネント内で呼び出す
js// ✅ カスタムコンポーネントの setup 内で呼び出す export default { setup() { const value = ref(''); useCustomFieldValue(() => value.value); return { value }; } };📊 リアクティブデータを返す
js// ✅ リアクティブデータを返し、自動的に同期更新 const data = reactive({ count: 0, name: '' }); useCustomFieldValue(() => data);🔄 動的に計算された値
js// ✅ 計算プロパティまたは動的な値を返す const items = ref([]); useCustomFieldValue(() => ({ items: items.value, count: items.value.length, isEmpty: items.value.length === 0 }));
❌ 避けるべき使い方
🚫 コンポーネントの外部で呼び出す
js// ❌ コンポーネントの外部で呼び出さない const value = ref(''); useCustomFieldValue(() => value.value); // ✅ 正しい使い方 export default { setup() { return { value }; } };🚫 非リアクティブデータを返す
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()
}));
};💡 ベストプラクティス
✅ 推奨される方法
🎯 明確なデータ構造
js// ✅ フォーム処理に適した構造化データを返す useCustomFieldValue(() => ({ value: currentValue.value, displayText: getDisplayText(), isValid: validateValue(), metadata: getMetadata() }));🔄 リアクティブデータの同期
js// ✅ リアクティブデータを使用して、リアルタイム同期を確保 const formValue = computed(() => ({ ...baseData.value, computed: calculateValue() })); useCustomFieldValue(() => formValue.value);✅ データ検証のサポート
js// ✅ フォーム検証と組み合わせて使用する const validateCustomValue = (value) => { if (!value || !value.required) { return '必須項目を入力してください'; } return true; };
❌ 避けるべき方法
🚫 頻繁な複雑な計算
js// ❌ 返却関数内での複雑な計算を避ける useCustomFieldValue(() => { // 複雑な計算ロジック... return heavyCalculation(); // ✅ 必要な場合のみ計算 });🚫 副作用の操作
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);
};📚 関連ドキュメント
📋 フォーム関連
- Form フォーム - フォームコンポーネントの基本的な使い方
- Field 入力フィールド - フォーム入力アイテムコンポーネント
- Uploader ファイルアップロード - ファイルアップロードコンポーネント
🎮 状態管理
- useToggle - ブール値の状態切り替え
- useEventListener - イベントリスナー管理
- useClickAway - 外側クリックのリスナー
🛠️ 開発ツール
- Composition API の紹介 - より多くの便利な Hook を知る
- フォーム検証ガイド - フォーム検証のベストプラクティス
- カスタムコンポーネントの開発 - コンポーネント開発ガイド
💡 実践ケース
- Rate 評価 - 評価コンポーネントの応用
- Slider スライダー - スライダーコンポーネントの応用
- Tag タグ - タグコンポーネントの応用
- Picker ピッカー - ピッカーコンポーネントの応用