Vue NativeVue Native
Guide
Components
Composables
Navigation
Architecture
  • iOS
  • Android
  • macOS
GitHub
Guide
Components
Composables
Navigation
Architecture
  • iOS
  • Android
  • macOS
GitHub
  • Getting Started

    • Introduction
    • Installation
    • Your First App
    • Project Structure
  • Core Concepts

    • Components
    • Styling
    • Theming
    • TypeScript
    • Navigation
    • Navigation Components
    • Native Modules
    • Native Code Blocks
    • Hot Reload
  • UI Patterns

    • Forms and v-model
    • Shared Element Transitions
    • Teleport
    • Custom Native Components (escape hatch)
  • Quality & Debugging

    • Testing
    • Debugging
    • Error Handling
  • Platform Hardening

    • Security
    • Accessibility
    • Performance
  • Integration Guides

    • State Management
    • Deep Linking & Universal Links
    • State Persistence
    • Push Notifications
    • Error Reporting & Monitoring
  • Tooling

    • Managed Workflow
    • VS Code Extension
    • Neovim Plugin
  • Building & Releasing

    • Building Native Apps
    • Deployment & App Store Submission
  • Reference

    • Upgrade Guide
    • Migrating from React Native
    • Known Limitations & Platform Differences
    • Troubleshooting

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 input
  • VSwitch - Toggle switch
  • VSlider - Range slider
  • VCheckbox - Checkbox
  • VRadio - Radio button
  • VDropdown - Dropdown picker
  • VSegmentedControl - Segmented control
  • VPicker - 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:

  1. Accepts modelValue prop
  2. Emits update:modelValue event
  3. Passes the event correctly from the native input

Related

  • VInput Component
  • VSwitch Component
  • VSlider Component
  • Form Validation
Edit this page
Last Updated: 10/14/26, 10:27 PM
Contributors: Abdul Hamid, Claude Opus 5.5
Next
Shared Element Transitions