Skip to content

Component v-model

استفاده پایه

با استفاده از v-model روی یک کامپوننت می‌توان یک اتصال دوطرفه ایجاد کرد.

از Vue 3.4 به بعد، رویکرد توصیه شده برای انجام این کار استفاده از ماکرو defineModel()‎ است:

vue
<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>parent bound v-model is: {{ model }}</div>
</template>

سپس والد می‌تواند مقدار را با v-model متصل کند:

template
<!-- Parent.vue -->
<Child v-model="countModel" />

مقدار برگردانده شده توسط defineModel()‎ یک ref است. می‌توان به آن دسترسی یافت و آن را تغییر داد مانند هر ref دیگری، به جز اینکه به عنوان یک اتصال دوطرفه بین داده والد و داده محلی عمل می‌کند:

  • ‎.value آن با مقدار متصل شده توسط v-model والد همگام‌سازی می‌شود؛
  • هنگامی که توسط فرزند تغییر داده شود، باعث به‌روزرسانی مقدار متصل شده والد نیز می‌شود.

این بدان معناست که می‌توانید این ref را نیز با v-model به یک عنصر input ساده متصل کنید، که پیاده‌سازی عناصر input را در حال استفاده از v-model را ساده می‌سازد:

vue
<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>

در Playground امتحان کنید

در زیر پوسته

defineModel یک ماکرو میانبر است. کامپایلر آن را برای موارد زیر توسعه می‌دهد:

  • یک پراپ به نام modelValue که مقدار ref محلی با آن همگام‌سازی می‌شود؛
  • یک رویداد به نام update:modelValue که هنگام تغییر مقدار ref محلی ارسال می‌شود.

روش پیاده‌سازی همان کامپوننت فرزند نشان داده شده در بالا قبل از نسخه 3.4 به این صورت است:

vue
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

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

همان‌طور که می‌بینید، کمی طولانی‌تر است. با این حال، درک آنچه در زیر پوسته اتفاق می‌افتد مفید است.

از آن‌جا که defineModel یک پراپ تعریف می‌کند، بنابراین می‌توانید آپشن‌های زیرین پراپ را با پاس دادن آن به defineModel اعلام کنید:

js
// v-model اجباری کردن
const model = defineModel({ required: true })

// ارائه یک مقدار پیش‌فرض
const model = defineModel({ default: 0 })

هشدار

اگر برای پراپ defineModel مقدار default تعریف کرده باشید و هیچ مقداری به آن از کامپوننت والد پاس ندهید، می‌تواند باعث ناهمگام‌سازی (de-synchronization) بین کامپوننت‌های والد و فرزند شود. در مثال زیر myRef والد برابر undefined است، اما model در فرزند برابر ۱ است:

js
// child component:
const model = defineModel({ default: 1 })

// parent component:
const myRef = ref()
html
<Child v-model="myRef"></Child>

در ابتدا بیایید نگاهی دوباره داشته باشیم که چطور v-model روی المان‌‌های بومی Html استفاده می‌شود:

template
<input v-model="searchText" />

در پشت پرده، کامپایلر تمپلیت، v-model را به معادل کامل‌تری برای ما تبدیل می‌کند. بنابراین کد بالا همان کار را انجام می‌دهد که کد زیر انجام می‌دهد:

template
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

وقتی v-model روی یک کامپوننت استفاده شود، به جای حالت بالا به صورت زیر تبدیل می‌شود:

template
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

اما برای اینکه این کار واقعا انجام شود، کامپوننت <CustomInput> باید دو کار انجام دهد:

  1. مقدار شاخصه value یک المان <input> بومی را به پراپ modelValue متصل کند
  2. وقتی یک رویداد input بومی رخ می‌دهد، یک رویداد سفارشی update:modelValue با مقدار جدید را منتشر کند

در اینجا این کار در عمل نمایش داده شده:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

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

حالا v-model باید به خوبی با این کامپوننت کار کند:

template
<CustomInput v-model="searchText" />

آزمایش این مورد در Playground

یک راه دیگر برای پیاده‌سازی v-model درون این کامپوننت استفاده از یک پراپرتی computed قابل نوشتن با یک getter و یک setter است. تابع get باید مقدار پراپ modelValue را برگرداند و تابع set باید رویداد مربوط را منتشر کند:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

آرگومان‌های v-model

همچنین v-model روی کامپوننت می‌تواند آرگومان بپذیرد:

template
<MyComponent v-model:title="bookTitle" />

در کامپوننت فرزند، می‌توانیم آرگومان مربوطه را با پاس دادن یک رشته به عنوان اولین آرگومان defineModel()‎ پشتیبانی کنیم:

vue
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>

<template>
  <input type="text" v-model="title" />
</template>

آزمایش این مورد در Playground

اگر آپشن‌های پراپ نیز مورد نیاز است، باید پس از نام مدل پاس داده شوند:

js
const title = defineModel('title', { required: true })
استفاده قبل از 3.4
vue
<!-- MyComponent.vue -->
<script setup>
defineProps({
  title: {
    required: true
  }
})
defineEmits(['update:title'])
</script>

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

آزمایش این مورد در Playground

در این مورد، به جای پراپ پیش‌فرض modelValue و رویداد update:modelValue، کامپوننت فرزند باید یک پراپ title و رویداد update:title برای به‌روزرسانی مقدار والد انتظار داشته باشد:

vue
<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

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

آزمایش این مورد در Playground

اتصال v-model چندگانه

با استفاده از توانایی هدف قراردادن یک پراپ و رویداد خاص که در آرگومان‌های v-model یاد گرفتیم، حالا می‌توانیم اتصال‌های v-model چندگانه، روی یک کامپوننت تکی ایجاد کنیم.

هر v-model بدون نیاز به گزینه‌های اضافه در کامپوننت با یک پراپ مختلف همگام‌سازی می‌شود:

template
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
vue
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

آزمایش این مورد در Playground

استفاده قبل از 3.4
vue
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

آزمایش این مورد در Playground

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

آزمایش این مورد در Playground

مدیریت modifierهای v-model

وقتی درحال یادگیری اتصال‌های input در form‌ها بودیم، مشاهده کردیم که v-model دارای modifierهای داخلی ‎.trim و ‎.number و ‎.lazy بود. گاهی اوقات همچنین ممکن است بخواهید که v-model‌ای که روی کامپوننت سفارشی‌شده input خود قرار دادید هم قابلیت پشتیبانی از modifierهای سفارشی را داشته باشد.

بیایید یک نمونه modifier سفارشی بسازیم، capitalize، که وظیفه داشته باشد حرف اول هر رشته‌ای که توسط اتصال v-model فراهم‌شده را به حالت بزرگ آن تبدیل کند:

template
<MyComponent v-model.capitalize="myText" />

می‌توان به modifierهای اضافه شده به یک v-model در کامپوننت فرزند با ساختارشکنی مقدار بازگشت داده شده از defineModel()‎ به این صورت دسترسی یافت:

vue
<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

برای تنظیم شرطی نحوه خواندن/نوشتن مقدار بر اساس modifierها، می‌توان آپشن‌های get و set را به defineModel()‎ پاس داد. این دو آپشن مقدار داده را در هنگام خواندن/نوشتن ref مدل دریافت می‌کنند و باید مقدار تبدیل شده را برگردانند. در ادامه نحوه استفاده از آپشن set برای پیاده‌سازی modifier مورد نظر یعنی capitalize را می‌بینیم:

vue
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

آزمایش این مورد در Playground

استفاده قبل از 3.4
vue
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

آزمایش این مورد در Playground

modifierهای اضافه شده به یک v-model کامپوننت از طریق پراپ modelModifiers به کامپوننت ارائه می‌شوند. در مثال زیر، ما یک کامپوننت ساخته‌ایم که شامل یک پراپ modelModifiers با مقدار پیش‌فرض آبجکت خالی است:

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
}
</script>

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

توجه کنید که پراپ modelModifiers شامل capitalize است و مقدارش true تنظیم شده‌ چرا که در v-model متصل‌شده، تنظیم شده است.

حالا که پراپ خود را تنظیم کرده‌ایم، می‌توانیم کلیدهای شی modelModifiers را بررسی کنیم و یک handler برای تغییر مقدار منتشرشده بنویسیم. در قطعه کد زیر، ما حروف رشته را هر زمان که المان ‎<input />‎ یک رویداد input انتشار دهد، به حالت بزرگ تبدیل می‌کنیم.

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

آزمایش این مورد در Playground

modifierها برای v-model‌های آرگومان‌دار

برای اتصال‌های v-model‌ای که هم پیراینده و هم آرگومان دارند، نام پراپ تولیدشده به شکل arg + "Modifiers"‎ خواهد بود. برای مثال:

template
<MyComponent v-model:title.capitalize="myText">

تعاریف متناظر باید به این شکل باشند:

js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}

اینجا مثال دیگری از استفاده از پیراینده‌ها با v-model چندگانه با آرگومان‌های مختلف را مشاهده می‌کنید:

template
<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
استفاده قبل از 3.4
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])

console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>
vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>
Component v-model has loaded