Skip to content

Binding přes v-model

Základní použití

Direkrivu v-model lze použít pro implementaci obousměrného (two-way) bindingu.

Od verze Vue 3.4 je doporučený postup použití makra defineModel():

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

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

<template>
  <div>hodnota v-model z rodiče je: {{ model }}</div>
</template>

Komponenta rodiče pak může provést binding hodnoty pomocí v-model:

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

Hodnota vrácená z defineModel() je ref. Může být přistupována a měněna jako jakýkoli jiný ref, kromě toho, že slouží jako obousměrný binding mezi hodnotou z rodiče a tou lokální:

  • Jeho .value je synchronizována s hodnotou navázanou pomocí v-model v komponentě rodiče;
  • Když je změněna komponentou potomka, způsobí automaticky aktualizaci hodnoty i v komponentě rodiče.

To znamená, že můžete také provést binding tohoto refu na nativní input element pomocí v-model, čímž se dá vstupní pole jednoduše a přímočaře obalit:

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

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

Vyzkoušejte si to

Pod pokličkou

defineModel je makro pro usnadnění zápisu. Překladač jej rozšíří na:

  • vlastnost (prop) modelValue, se kterou je synchronizována lokální hodnota ref;
  • událost (event) update:modelValue, která je emitována při změně lokální hodnoty ref.

Takto se implementovala komponenta potomka se stejnou funkcionalitou před verzí 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>

Jak můžete vidět, je to trochu zbytečně složité. Nicméně je dobré vědět, jak to ve skutečnosti funguje.

Protože defineModel deklaruje vlastnost (prop), mužete následně upravovat její možnosti (options) předáním do defineModel:

js
// v-model bude povinná hodnota
const model = defineModel({ required: true })

// poskytnutí výchozí hodnoty
const model = defineModel({ default: 0 })

VAROVÁNÍ

Pokud pro vlastnost defineModel použijete default a neposkytnete v komponentě rodiče žádnou hodnotu, může to způsobit de-synchronizaci mezi rodičem a potomkem. V příkladu níže je myRef v rodiči undefined, zatímco model v potomkovi je 1:

js
// komponenta potomka:
const model = defineModel({ default: 1 })

// komponenta rodiče:
const myRef = ref()
html
<Child v-model="myRef"></Child>

Napřed si připomeňme, jak se v-model používá v nativních elementech:

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

Kompilátor šablon zápis v-model na pozadí expanduje. Výše uvedený kód se chová stejně jako následující:

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

Při použití na komponentě, je v-model místo toho expandován na toto:

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

Aby to však mohlo fungovat, musí komponenta <CustomInput> udělat dvě věci:

  1. Provést binding atributu value nativního <input> elementu na vlastnost (prop) modelValue
  2. Když je vyvolána nativní událost (event) input, vyvolat (emit) vlastní událost update:modelValue s novou hodnotou

Zde to vidíte v praxi:

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

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

Na této komponentě by nyní měl v-model perfektně fungovat:

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

Vyzkoušejte si to

Dalším způsobem implementace v-model v rámci této komponenty je použití zapisovatelné computed proměnné s getterem a setterem. Metoda get by měla vracet vlastnost modelValue a metoda set by měla vyvolat odpovídající událost:

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 parametry

v-model na komponentě může také přijmout parametr:

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

V komponentě potomka můžeme takový parametr podporovat předáním řetězce do defineModel()

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

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

Vyzkoušejte si to

Pokud je potřeba předat i možnosti (options) vlastnosti (prop), je mpžné je předat jako druhý parametr po názvu:

js
const title = defineModel('title', { required: true })
Použití před verzí 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>

Vyzkoušejte si to

V tomto případě místo výchozí vlastnosti modelValue a události update:modelValue může komponenta potomka očekávat vlastnost title a emitovat událost update:title pro aktualizaci hodnoty v rodiči:

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>

Vyzkoušejte si to

Vícenásobný binding přes v-model

Využitím možnosti zaměřit se na konkrétní vlastnost a událost, jak jsme se to naučili dříve pomocí v-model parametrů, můžeme nyní na jedné instanci komponenty vytvořit více v-model vazeb.

Každý v-model se bude synchronizovat s jinou vlastností bez nutnosti speciální konfigurace uvnitř komponenty:

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>

Vyzkoušejte si to

Použití před verzí 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>

Vyzkoušejte si to

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>

Vyzkoušejte si to

Obsluha v-model modifikátorů

Když jsme se učili o bindingu dat z formuláře, viděli jsme, že v-modelvestavěné modifikátory - .trim, .number a .lazy. V některých případech můžete chtít, aby v-model na vaší input komponentě podporoval i vlastní (custom) modifikátory.

Pojďme vytvořit příklad vlastního modifikátoru capitalize, který bude psát první znak řetězce zadaného přes v-model binding velkými písmeny:

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

Modifikátory přidané na v-model komponenty mohou být získány v komponentně potomka pomocí destrukturování návratové hodnoty defineModel() takto:

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

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

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

Pro podmíněné nastavení jak by měla být hodnota na základě modifikátorů čtena / zapisována, můžeme do defineModel() předat možnosti get a set. Tyto dvě nastavení získají hodnotu při čtení / zápisu příslušné ref hodnoty a měly by vrátit transformovanou hodnotu. Takto můžeme použít set pro implementaci modifikátoru 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>

Vyzkoušejte si to

Použití před verzí 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>

Vyzkoušejte si to

Modifikátory přidané do v-model komponenty budou dostupné v komponentě potomka pomocí vlastnosti modelModifiers. V příkladu níže jsme vytvořili komponentu, která obsahuje vlastnost modelModifiers, jejíž výchozí hodnota je prázdný objekt:

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>

Všimněte si, že vlastnost modelModifiers komponenty obsahuje capitalize a jeho hodnota je true - protože tak byla nastavena pomocí v-model bindingu v-model.capitalize="myText".

Teď když máme naši vlastnost nastavenou, můžeme kontrolovat klíče modelModifiers a napsat handler, který změní emitovanou hodnotu. V kódu níže převedeme řetězec na velká písmena kdykoli, když <input /> element vyvolá událost 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>

Vyzkoušejte si to

Modifikátory pro v-model s parametry

Pro v-model binding, kde jsou jak parametry, tak modifikátory, bude vygenerované jméno vlastnosti arg + "Modifiers". Například:

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

Odpovídající deklarace by měla být:

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

Zde je další příklad použití modifikátorů na více v-model s různými parametry:

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>
Použití před verzí 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>
Binding přes v-model has loaded