Forms and v-model
Vue Native provides comprehensive form handling with the v-model directive for two-way data binding.
Basic Usage
The v-model directive works on native input components:
<script setup>
import { ref } from '@thelacanians/vue-native-runtime'
const text = ref('')
const number = ref(0)
const enabled = ref(false)
</script>
<template>
<VView>
<VInput v-model="text" placeholder="Enter text" />
<VText>{{ text }}</VText>
<VInput
v-model.number="number"
keyboardType="numeric"
placeholder="Enter number"
/>
<VSwitch v-model="enabled" />
</VView>
</template>
Supported Components
v-model works with these native components:
VInput- Text inputVSwitch- Toggle switchVSlider- Range sliderVCheckbox- CheckboxVRadio- Radio buttonVDropdown- Dropdown pickerVSegmentedControl- Segmented controlVPicker- Date/time picker
Modifiers
.lazy
Sync after change event instead of input:
<VInput v-model.lazy="text" />
.number
Automatically cast input value to number:
<VInput v-model.number="age" keyboardType="numeric" />
.trim
Automatically trim whitespace from input:
<VInput v-model.trim="name" />
Custom Components
v-model works on custom components too:
<script setup>
import { ref } from '@thelacanians/vue-native-runtime'
const value = ref('')
</script>
<template>
<CustomInput v-model="value" />
</template>
Your custom component should accept modelValue prop and emit update:modelValue:
<script setup>
import { VInput } from '@thelacanians/vue-native-runtime'
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<VInput
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
/>
</template>
Multiple v-models
You can have multiple v-models on a single component:
<script setup>
import { ref } from '@thelacanians/vue-native-runtime'
const firstName = ref('')
const lastName = ref('')
</script>
<template>
<PersonForm
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
</template>
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<VView>
<VInput
:modelValue="firstName"
@update:modelValue="$emit('update:firstName', $event)"
/>
<VInput
:modelValue="lastName"
@update:modelValue="$emit('update:lastName', $event)"
/>
</VView>
</template>
Form Validation
Combine v-model with computed properties for validation:
<script setup>
import { ref, computed } from '@thelacanians/vue-native-runtime'
const email = ref('')
const password = ref('')
const emailError = computed(() => {
if (!email.value) return 'Email is required'
if (!/^\S+@\S+\.\S+$/.test(email.value)) return 'Invalid email format'
return ''
})
const isFormValid = computed(() => {
return !emailError.value && password.value.length >= 8
})
function submit() {
if (!isFormValid.value) return
// Submit form
}
</script>
<template>
<VView>
<VInput v-model="email" placeholder="Email" />
<VText v-if="emailError" :style="{ color: 'red' }">
{{ emailError }}
</VText>
<VInput
v-model="password"
secureTextEntry
placeholder="Password (min 8 chars)"
/>
<VButton
title="Submit"
@press="submit"
:disabled="!isFormValid"
/>
</VView>
</template>
Programmatic Control
For advanced scenarios, you can control inputs programmatically:
<script setup>
import { ref } from '@thelacanians/vue-native-runtime'
const text = ref('')
const inputRef = ref(null)
function focusInput() {
// Access the native node
const node = inputRef.value?.__nodeId
if (node) {
// Focus the input
NativeBridge.updateProp(node, 'focused', true)
}
}
function clearInput() {
text.value = ''
}
</script>
<template>
<VView>
<VInput
ref="inputRef"
v-model="text"
placeholder="Enter text"
/>
<VButton title="Focus" @press="focusInput" />
<VButton title="Clear" @press="clearInput" />
</VView>
</template>
Handling Submit
Handle form submission with the keyboard's return key:
<script setup>
import { ref } from '@thelacanians/vue-native-runtime'
const text = ref('')
function handleSubmit() {
console.log('Submitted:', text.value)
}
</script>
<template>
<VView>
<VInput
v-model="text"
placeholder="Enter text"
:returnKeyType="'done'"
@submit="handleSubmit"
/>
</VView>
</template>
Common Patterns
Search Input
<script setup>
import {
ref,
watch,
onUnmounted,
useHttp,
} from '@thelacanians/vue-native-runtime'
const search = ref('')
const results = ref([])
const { get } = useHttp({ baseURL: 'https://api.example.com' })
// Vue Native does not ship a `watchDebounced` (that is a VueUse API, and
// VueUse targets the DOM). Debounce with a plain timer — no extra dependency.
let debounceTimer = null
watch(search, (query) => {
if (debounceTimer !== null) clearTimeout(debounceTimer)
if (query.length < 3) {
results.value = []
return
}
debounceTimer = setTimeout(async () => {
debounceTimer = null
const response = await get('/search', { params: { q: query } })
results.value = response.data ?? []
}, 300)
})
// Don't leave a timer pending after the screen goes away.
onUnmounted(() => {
if (debounceTimer !== null) clearTimeout(debounceTimer)
})
</script>
<template>
<VView :style="{ flex: 1 }">
<VInput
v-model="search"
placeholder="Search..."
returnKeyType="search"
:autoCorrect="false"
/>
<VList
:data="results"
:keyExtractor="(item) => item.id"
:style="{ flex: 1 }"
>
<template #item="{ item }">
<VText :style="{ padding: 12 }">{{ item.name }}</VText>
</template>
</VList>
</VView>
</template>
Props VInput does not have
VInput accepts modelValue (v-model), placeholder, secureTextEntry, keyboardType, returnKeyType, autoCapitalize, autoCorrect, maxLength, multiline, style, and the four accessibility* props. There is no clearButtonMode — the native side never implemented it — and no renderItem on VList, which renders rows through its #item slot. See VInput and VList.
Login Form
<script setup>
import { ref, useHttp } from '@thelacanians/vue-native-runtime'
const email = ref('')
const password = ref('')
// `loading` and `error` are reactive refs owned by useHttp — there is no
// global `$fetch` in Vue Native (that is a Nuxt/ofetch API).
const { loading, error, post } = useHttp({ baseURL: 'https://api.example.com' })
async function login() {
try {
await post('/api/login', {
email: email.value,
password: password.value,
})
} catch {
// `error` is already populated by useHttp; surface it in the template.
}
}
</script>
<template>
<VView>
<VInput v-model="email" placeholder="Email" keyboardType="email-address" />
<VInput
v-model="password"
placeholder="Password"
secureTextEntry
@submit="login"
/>
<VText v-if="error" :style="{ color: '#FF3B30' }">{{ error }}</VText>
<VButton
:title="loading ? 'Logging in...' : 'Login'"
@press="login"
:disabled="loading"
/>
</VView>
</template>
Troubleshooting
v-model not updating
Problem: Input value doesn't update when the bound value changes.
Solution: Every form component (VInput, VSwitch, VSlider, VCheckbox, VRadio, VDropdown, VPicker) uses modelValue + update:modelValue, so bind with v-model. :value="..." does nothing — the only exception is VPicker, which still accepts value as a legacy alias for modelValue.
Number modifier not working
Problem: .number modifier doesn't convert to number.
Solution: Ensure the input's keyboardType is set to 'numeric' or 'number-pad' for better compatibility.
Custom component v-model
Problem: v-model doesn't work on custom component.
Solution: Ensure your component:
- Accepts
modelValueprop - Emits
update:modelValueevent - Passes the event correctly from the native input