Vue.js: النماذج و v-model
آخر تحديث: 2026-08-26
تُعد النماذج جوهر التفاعل في تطبيقات الويب — فعمليات تسجيل الدخول والتسجيل والبحث والإعدادات تعتمد جميعها على معالجة النماذج. ويُعد v-model في Vue بمثابة «تسهيل لغوي» لمعالجة النماذج، مما يجعل كود الربط ثنائي الاتجاه موجزًا للغاية (سطر واحد مقابل 5 أسطر).
يعمل defineModel في Vue 3.4+ على تبسيط تنفيذ v-model بشكل أكبر. كما تتطلب النماذج المعقدة ميزات متقدمة مثل التحقق من الصحة، والتسلسل، وتحميل الملفات. يتناول هذا الدرس خمسة سيناريوهات رئيسية للنماذج.
1. ما ستتعلمه
- كيف يعمل نموذج V (بشكل أساسي، هو عبارة عن «props + emit» في صيغة مبسطة)
- مكونات نموذج V المخصصة
- تبسيط defineModel (Vue 3.4+)
- التحقق من صحة النماذج (VeeValidate / مخصص)
- تصميم نماذج معقدة (حقول متداخلة / ديناميكية)
- تسلسل النماذج وإرسالها
- تحميل الملفات ومعاينتها
2. كابوس «الأقسام النمطية الخمسة» في نموذج تسجيل الدخول
(1) المشكلة: 5 حقول في النموذج، و10 أسطر من كود v-model
كان نموذج تسجيل الدخول الخاص بأليس يتألف من 5 حقول، كل منها مرتبط بنموذج v-model:
<!-- ❌ The "Broken" Version: 5 fields + 5 refs + 5 v-model -->
<template>
<form @submit.prevent="handleLogin">
<input v-model="username">
<input v-model="password" type="password">
<input v-model="email" type="email">
<input v-model="phone">
<input v-model="captcha">
<button>Login</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
const password = ref('')
const email = ref('')
const phone = ref('')
const captcha = ref('')
</script>
5 حقول = 10 أسطر من النص المعياري. 10 حقول تعادل 20 سطراً. وكلما زادت تعقيد الاستمارة، زادت المشكلة سوءًا.
(2) نهج متقدم لاستخدام v-model في Vue: الكائنات التفاعلية + defineModel
<!-- ✅ Correct Version: 1 reactive Object -->
<template>
<form @submit.prevent="handleLogin">
<input v-model="form.username">
<input v-model="form.password" type="password">
<input v-model="form.email" type="email">
<input v-model="form.phone">
<input v-model="form.captcha">
<button>Login</button>
</form>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
username: '',
password: '',
email: '',
phone: '',
captcha: ''
})
</script>
5 حقول = كائن واحد. تقلل البرمجة التفاعلية حجم الكود بنسبة 50%.
(3) الإيرادات
بعد تحسين نموذج V:
- حجم الكود: 10 أسطر → 5 أسطر (-50٪)
- الحقول الجديدة: صف واحد (تمت إضافة حقول تفاعلية)
- إرسال النموذج بالكامل: ما عليك سوى إرسال كائن النموذج
- التحقق من الصحة: المعالجة المركزية
3. كيف يعمل نموذج V
(1) جوهر نموذج V
<!-- v-model is props + emit syntax sugar -->
<input v-model="searchQuery">
<!-- equivalent to -->
<input
:value="searchQuery"
@input="searchQuery = $event.target.value"
>
(2) نموذج V في المكونات
<!-- Child component:CustomInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- Using Parent Components v-model -->
<CustomInput v-model="searchQuery" />
<!-- equivalent to -->
<CustomInput
:modelValue="searchQuery"
@update:modelValue="searchQuery = $evento"
/>
(3) defineModel (مبسطة لـ Vue 3.4 وما بعدها)
<!-- Vue 3.4+: 1 line replaces the 10 lines above -->
<template>
<input v-model="value">
</template>
<script setup>
const value = defineModel('modelValue')
// Now value is ref, Sure:
// - Read: value.value
// - Write: value.value = 'new' (Automatic emit)
// - watch:watch(value, ...)
// - Parent Component v-model Two-way binding works as usual
</script>
4. 5 أنواع من نموذج V المخصص
(1) المدخلات الأساسية
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
(2) الرسائل النصية
<template>
<textarea :value="modelValue" @input="$emit('update:modelValue', $evento.target.value)" />
</template>
(3) حدد
<template>
<select
:value="modelValue"
@change="$emit('update:modelValue', $event.target.value)"
>
<option value="vue">Vue</option>
<option value="react">React</option>
</select>
</template>
(4) مربع الاختيار (اختيارات متعددة)
<template>
<div>
<label v-for="option in options" :key="option">
<input
type="checkbox"
:value="option"
:checked="modelValue.includes(option)"
@change="onChange($event, option)"
>
{{ option }}
</label>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: Array,
options: Array
})
const emit = defineEmits(['update:modelValue'])
function onChange(e, option) {
const newValue = e.target.checked
? [...props.modelValue, option]
: props.modelValue.filter(v => v !== option)
emit('update:modelValue', newValue)
}
</script>
(5) مُعدِّل مخصص (trim)
<!-- Parent Component:v-model.trim -->
<CustomInput v-model.trim="username" />
<!-- Child component:Processing trim Modifiers -->
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
let value = e.target.value
if (props.modelModifiers.trim) {
value = value.trim()
}
emit('update:modelValue', value)
}
</script>
5. التحقق من صحة البيانات في النموذج
(1) التحقق من الصحة المخصص
// utils/validators.js
export function isEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
export function isPhone(value) {
return /^1[3-9]\d{9}$/.test(value)
}
export function minLength(value, min) {
return value.length >= min
}
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.email" @blur="validateField('email')">
<p v-if="errors.email" class="error">{{ errors.email }}</p>
<input v-model="form.password" type="password">
<p v-if="errors.password">{{ errors.password }}</p>
<button :disabled="!isValid">Submit</button>
</form>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { isEmail, minLength } from '@/utils/validators'
const form = reactive({
email: '',
password: ''
})
const errors = reactive({})
const isValid = ref(false)
function validateField(field) {
if (field === 'email') {
if (!form.email) {
errors.email = 'Email is required'
} else if (!isEmail(form.email)) {
errors.email = 'Invalid email format'
} else {
delete errors.email
}
}
if (field === 'password') {
if (!form.password) {
errors.password = 'Password is required'
} else if (!minLength(form.password, 8)) {
errors.password = 'Password must be 8+ characters'
} else {
delete errors.password
}
}
isValid.value = Object.keys(errors).length === 0
}
function handleSubmit() {
Object.keys(form).forEach(validateField)
if (isValid.value) {
console.log('Submit:', form)
}
}
</script>
(2) مكتبة VeeValidate (موصى بها)
npm install vee-validate @vee-validate/rules
<template>
<Form @submit="handleSubmit" :validation-schema="schema">
<Field name="email" type="email" v-model="form.email" />
<ErrorMessage name="email" />
<Field name="password" type="password" v-model="form.password" />
<ErrorMessage name="password" />
<button>Submit</button>
</Form>
</template>
<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate'
import * as yup from 'yup'
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required()
})
const form = reactive({ email: '', password: '' })
function handleSubmit(values) {
console.log('Valid:', values)
}
</script>
6. تصميم النماذج المعقدة
(1) النماذج المتداخلة
const form = reactive({
user: {
name: '',
email: '',
age: 0
},
address: {
city: '',
country: ''
}
})
<template>
<input v-model="form.user.name" placeholder="Name">
<input v-model="form.user.email" placeholder="Email">
<input v-model="form.address.city" placeholder="City">
</template>
(2) الحقول الديناميكية (v-for)
const form = reactive({
items: [
{ name: '', quantity: 1, price: 0 }
]
})
function addItem() {
form.items.push({ name: '', quantity: 1, price: 0 })
}
function removeItem(index) {
form.items.splice(index, 1)
}
<template>
<div v-for="(item, index) in form.item" :key="index">
<input v-model="item.name" placeholder="Item name">
<input v-model.numbervalue="item.quantity" type="numbervalue">
<input v-model.numbervalue="item.price" type="numbervalue">
<button @click="removeItem(index)">Remove</button>
</div>
<button @click="addItem">Add Item</button>
</template>
(3) المُعدِّلات الخمسة الرئيسية للشكل
<!-- 1. .lazy:change Event Trigger(No input) -->
<input v-model.lazy="email">
<!-- 2. .number:Automatically Convert to Numbers -->
<input v-model.number="age" type="number">
<!-- 3. .trim:Automatically Remove Spaces -->
<input v-model.trim="username">
<!-- 4. Custom Modifiers(CustomInput) -->
<CustomInput v-model.trim="text" />
<!-- 5. .debounce(Custom):300ms Image Stabilization -->
<DebouncedInput v-model:debounce="500" />
7. تحميل الملفات
(1) تحميل ملف واحد
<template>
<input type="file" @change="handleFile" accept="image/*">
<img v-if="preview" :src="preview" alt="Preview">
</template>
<script setup>
import { ref } from 'vue'
const preview = ref(null)
function handleFile(event) {
const file = event.target.files[0]
if (file) {
// Local Preview
const reader = new FileReader()
reader.onload = (e) => {
preview.value = e.target.result
}
reader.readAsDataURL(file)
// Upload to the server
const formData = new FormData()
formData.append('file', file)
fetch('/api/upload', { method: 'POST', body: formData })
}
}
</script>
(2) ملفات متعددة + السحب والإفلات
<template>
<div
@dragover.prevent
@drop.prevent="handleDrop"
:classe="{ dragging: isDragging }"
>
<input type="file" multiple @change="handleFiles" accept="image/*">
<p>Drag files here or click to upload</p>
<div v-for="file in files" :key="file.name">
<img :src="file.preview" :alt="file.name">
<p>{{ file.name }} ({{ file.size }} bytes)</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const files = reactive([])
const isDragging = ref(false)
function addFile(file) {
if (!file.type.startsWith('image/')) return
const reader = new FileReader()
reader.onload = (e) => {
files.push({
name: file.name,
size: file.size,
preview: e.target.result
})
}
reader.readAsDataURL(file)
}
function handleFiles(e) {
Array.from(e.target.files).forEach(addFile)
}
function handleDrop(e) {
isDragging.value = false
Array.from(e.dataTransfer.files).forEach(addFile)
}
</script>
8. أمثلة كاملة: 5 سيناريوهات رئيسية للنماذج
▶ مثال: 1. 5 أساسيات نموذج V
<!-- 1. text input -->
<input v-model="text">
<!-- 2. textarea -->
<textarea v-model="description"></textarea>
<!-- 3. checkbox -->
<input v-model="agreed" type="checkbox">
<!-- 4. radio -->
<input v-model="gender" type="radio" value="male">
<input v-model="gender" type="radio" value="female">
<!-- 5. select -->
<select v-model="selected">
<option value="vue">Vue</option>
<option value="react">React</option>
</select>
▶ مثال: 2. 5 أنواع من نماذج V المخصصة
<!-- Child component:CustomInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- Parent Component -->
<CustomInput v-model="search" />
<!-- equivalent to -->
<CustomInput :modelValue="search" @update:modelValue="search = $event" />
▶ مثال: 3. 5 سيناريوهات التحقق من الصحة
// 1. Required
if (!form.email) errors.email = 'Required'
// 2. Email
if (!isEmail(form.email)) errors.email = 'Invalid email'
// 3. Cell Phone
if (!isPhone(form.phone)) errors.phone = 'Invalid phone'
// 4. Length
if (form.password.length < 8) errors.password = 'Too short'
// 5. Confirm Password
if (form.password !== form.confirm) errors.confirm = 'Mismatch'
▶ مثال: 4. 5 من أهم عوامل تعديل الصيغة
<!-- 1. .lazy:change Event -->
<input v-model.lazy="email">
<!-- 2. .numbervalue:Automatically Convert to Numbers -->
<input v-model.numbervalue="age" type="numbervalue">
<!-- 3. .trim:Automatic trim -->
<input v-model.trim="username">
<!-- 4. Combinations of Multiple Modifiers -->
<input v-model.lazy.trim="email">
<!-- 5. Custom Modifiers(Child component)-->
<CustomInput v-model.trim="text" />
▶ مثال: 5. مرجع سريع لـ 5 أخطاء شائعة
| الخطأ | الأعراض | الحل |
|---|---|---|
| v-model.number لا يزال سلسلة نصية | خطأ في الجمع | type="number" + .number |
| لا يتم تحديث نموذج v | تصبح المرجعية غير نشطة بعد التغيير إلى النمط التفاعلي | قم بالتغيير إلى .value أو toRefs |
| فشل تحميل الملف باستخدام FormData | خطأ متعلق بالنطاقات المتعددة أو بالطريقة | fetch + FormData |
| توقيت التحقق غير صحيح | العرض فقط عند فقدان التركيز | استخدام @blur |
| الكثير من النصوص النمطية الميدانية | كود مطول | باستخدام كائنات تفاعلية |
▶ مثال: 6. 5 مقارنات رئيسية في الأداء
| النمط | البساطة | الأداء | قابلية التطبيق |
|---|---|---|---|
| مراجع متعددة | ❌ مكرر | ⭐⭐⭐⭐ | حقلان أو حقل واحد |
| كائن تفاعلي | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 5 حقول أو أكثر |
| FormData (دليل) | ❌ | ⭐⭐⭐ | تحميل الملفات |
| VeeValidate | ⭐⭐⭐⭐ | ⭐⭐⭐ | التحقق المعقد |
| defineModel | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | مكون Vue 3.4+ |
❓ أسئلة شائعة
defineProps(['modelValue']) + defineEmits(['update:modelValue']) داخل المكون. واستخدم v-model في المكون الأصلي. في Vue 3.4 وما فوق، استخدم defineModel لاستبدال 10 أسطر بسطر واحد فقط.type="number". أو يمكنك استخدام parseFloat() يدويًّا.FileReader.readAsDataURL() لقراءة الملف كسلسلة Base64، ثم قم بتعيينه إلى <img src="...">. Use FormData من أجل التحميل.change (بعد فقدان التركيز)، مما يقلل من العبء الناتج عن التحقق من الصحة في الوقت الفعلي. أما المُعدِّل .debounce فهو مُعدِّل مخصص يجب تنفيذه في المكون الفرعي؛ وهو يقوم بتشغيل آخر حدث في غضون 300 مللي ثانية.JSON.stringify(form) (لا حاجة إلى FormData، ما لم تكن ترسل ملفات). ويمكن للخادم الخلفي استلام ملف JSON ببساطة.defineModel محل الصيغة الكاملة لـ v-model؟defineModel('modelValue') is equivalent to the 10 lines of code in props.modelValue + emit('update:modelValue'). وهو متوافق تمامًا مع الإصدارات السابقة.📖 ملخص
v-modelهو تعبير مبسط لـpropsوemit؛ وفي الإصدار Vue 3.4 والإصدارات الأحدث، تم استبداله بسطر واحد باستخدامdefineModel.- 5 أنواع من نماذج V المخصصة: حقل الإدخال / حقل النص الموسع / قائمة الاختيار / مربع الاختيار / المُعدِّلات
- 5 مُعدِّلات: .lazy / .number / .trim / مخصصة / مُعدِّلات متعددة
- التحقق من صحة النماذج: مخصص (بسيط) / VeeValidate (معقد)
- 5 أنواع من النماذج المعقدة: المتداخلة / الديناميكية / التسلسلية / التحقق من الصحة / الملفات
- الكائنات التفاعلية + نموذج V: تقليل النصوص النمطية بنسبة 50%
- تحميل الملفات: FileReader + FormData
📝 تمارين
-
أسئلة أساسية (مستوى الصعوبة: ⭐)
تنفيذ نموذج تسجيل دخول بسيط:
- 5 حقول (اسم المستخدم / كلمة المرور / البريد الإلكتروني / رقم الهاتف / اختبار كابتشا)
- نموذج V + التفاعلية
- طباعة كائن النموذج بالكامل عند إرساله
-
مسائل متقدمة (مستوى الصعوبة: ⭐⭐)
تنفيذ التحقق من صحة النماذج:
- 5 حقول + التحقق من الصحة المخصص (تنسيق البريد الإلكتروني، تنسيق رقم الهاتف، طول كلمة المرور)
- @blur يُطلق عملية التحقق من الصحة
- ظهور رسالة خطأ
- يتم تمكين زر الإرسال أو تعطيله بناءً على الخاصية
isValid
-
مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)
تنفيذ نظام نماذج «تسجيل المستخدم» متكامل:
- 10 حقول (بما في ذلك كائنات
addressالمتداخلة وarraytagsديناميكية) - VeeValidate + مخطط Yup
- تحميل الملف (صورة الملف الشخصي)
- تغطية كاملة لـ 5 عوامل معدلة
- يُستخدم
defineModelللمكونات الفرعية - 5 حالات الخطأ
- التعيين القوي للأنواع في TypeScript
- 10 حقول (بما في ذلك كائنات