Vue.js: النماذج و v-model

آخر تحديث: 2026-08-26

تُعد النماذج جوهر التفاعل في تطبيقات الويب — فعمليات تسجيل الدخول والتسجيل والبحث والإعدادات تعتمد جميعها على معالجة النماذج. ويُعد v-model في Vue بمثابة «تسهيل لغوي» لمعالجة النماذج، مما يجعل كود الربط ثنائي الاتجاه موجزًا للغاية (سطر واحد مقابل 5 أسطر).

يعمل defineModel في Vue 3.4+ على تبسيط تنفيذ v-model بشكل أكبر. كما تتطلب النماذج المعقدة ميزات متقدمة مثل التحقق من الصحة، والتسلسل، وتحميل الملفات. يتناول هذا الدرس خمسة سيناريوهات رئيسية للنماذج.

1. ما ستتعلمه



2. كابوس «الأقسام النمطية الخمسة» في نموذج تسجيل الدخول

(1) المشكلة: 5 حقول في النموذج، و10 أسطر من كود v-model

كان نموذج تسجيل الدخول الخاص بأليس يتألف من 5 حقول، كل منها مرتبط بنموذج v-model:

VUE
<!-- ❌ 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

VUE
<!-- ✅ 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:



3. كيف يعمل نموذج V

(1) جوهر نموذج V

VUE
<!-- v-model is props + emit syntax sugar -->
<input v-model="searchQuery">

<!-- equivalent to -->
<input 
  :value="searchQuery" 
  @input="searchQuery = $event.target.value"
>

(2) نموذج V في المكونات

VUE
<!-- Child component:CustomInput.vue -->
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
VUE
<!-- Using Parent Components v-model -->
<CustomInput v-model="searchQuery" />

<!-- equivalent to -->
<CustomInput 
  :modelValue="searchQuery" 
  @update:modelValue="searchQuery = $evento"
/>

(3) defineModel (مبسطة لـ Vue 3.4 وما بعدها)

VUE
<!-- 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) المدخلات الأساسية

VUE
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

(2) الرسائل النصية

VUE
<template>
  <textarea :value="modelValue" @input="$emit('update:modelValue', $evento.target.value)" />
</template>

(3) حدد

VUE
<template>
  <select 
    :value="modelValue" 
    @change="$emit('update:modelValue', $event.target.value)"
  >
    <option value="vue">Vue</option>
    <option value="react">React</option>
  </select>
</template>

(4) مربع الاختيار (اختيارات متعددة)

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

VUE
<!-- 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) التحقق من الصحة المخصص

JS
// 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
}
VUE
<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 (موصى بها)

BASH
npm install vee-validate @vee-validate/rules
VUE
<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) النماذج المتداخلة

JS
const form = reactive({
  user: {
    name: '',
    email: '',
    age: 0
  },
  address: {
    city: '',
    country: ''
  }
})
VUE
<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)

JS
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)
}
VUE
<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) المُعدِّلات الخمسة الرئيسية للشكل

VUE
<!-- 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) تحميل ملف واحد

VUE
<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) ملفات متعددة + السحب والإفلات

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

VUE
<!-- 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 المخصصة

VUE
<!-- 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 سيناريوهات التحقق من الصحة

JS
// 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 من أهم عوامل تعديل الصيغة

VUE
<!-- 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+


❓ أسئلة شائعة

س كيف تستخدم v-model في المكونات المخصصة؟
ج استخدم defineProps(['modelValue']) + defineEmits(['update:modelValue']) داخل المكون. واستخدم v-model في المكون الأصلي. في Vue 3.4 وما فوق، استخدم defineModel لاستبدال 10 أسطر بسطر واحد فقط.
س لماذا لا يعمل v-model.number؟
ج يجب استخدامه مع type="number". أو يمكنك استخدام parseFloat() يدويًّا.
س هل يجب عليّ استخدام التحقق المخصص أم VeeValidate للتحقق من صحة النماذج؟
ج استخدم التحقق المخصص للنماذج البسيطة (أقل من 5 حقول)؛ واستخدم VeeValidate للنماذج المعقدة (أكثر من 5 حقول + حقول متداخلة + التحقق غير المتزامن).
س كيف يمكنني تحميل ملف وعرض معاينة له؟
ج استخدم FileReader.readAsDataURL() لقراءة الملف كسلسلة Base64، ثم قم بتعيينه إلى &lt;img src="..."&gt;. Use FormData من أجل التحميل.
س ما الفرق بين المُعدِّلات .lazy و.debounce في v-model؟
ج يتم تشغيل المُعدِّل .lazy بواسطة الحدث change (بعد فقدان التركيز)، مما يقلل من العبء الناتج عن التحقق من الصحة في الوقت الفعلي. أما المُعدِّل .debounce فهو مُعدِّل مخصص يجب تنفيذه في المكون الفرعي؛ وهو يقوم بتشغيل آخر حدث في غضون 300 مللي ثانية.
س كيف يمكنني تحويل بيانات النموذج إلى صيغة JSON لإرسالها؟
ج ما عليك سوى استخدام JSON.stringify(form) (لا حاجة إلى FormData، ما لم تكن ترسل ملفات). ويمكن للخادم الخلفي استلام ملف JSON ببساطة.
س هل يحل defineModel محل الصيغة الكاملة لـ v-model؟
ج نعم. defineModel('modelValue') is equivalent to the 10 lines of code in props.modelValue + emit('update:modelValue'). وهو متوافق تمامًا مع الإصدارات السابقة.

📖 ملخص


📝 تمارين

  1. أسئلة أساسية (مستوى الصعوبة: ⭐)

    تنفيذ نموذج تسجيل دخول بسيط:

    • 5 حقول (اسم المستخدم / كلمة المرور / البريد الإلكتروني / رقم الهاتف / اختبار كابتشا)
    • نموذج V + التفاعلية
    • طباعة كائن النموذج بالكامل عند إرساله
  2. مسائل متقدمة (مستوى الصعوبة: ⭐⭐)

    تنفيذ التحقق من صحة النماذج:

    • 5 حقول + التحقق من الصحة المخصص (تنسيق البريد الإلكتروني، تنسيق رقم الهاتف، طول كلمة المرور)
    • @blur يُطلق عملية التحقق من الصحة
    • ظهور رسالة خطأ
    • يتم تمكين زر الإرسال أو تعطيله بناءً على الخاصية isValid
  3. مسألة التحدي (مستوى الصعوبة: ⭐⭐⭐)

    تنفيذ نظام نماذج «تسجيل المستخدم» متكامل:

    1. 10 حقول (بما في ذلك كائنات address المتداخلة وarray tags ديناميكية)
    2. VeeValidate + مخطط Yup
    3. تحميل الملف (صورة الملف الشخصي)
    4. تغطية كاملة لـ 5 عوامل معدلة
    5. يُستخدم defineModel للمكونات الفرعية
    6. 5 حالات الخطأ
    7. التعيين القوي للأنواع في TypeScript
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%