Skip to main content

useProxyModel

Proxy composable for bidirectional synchronization between a selection registry and Vue’s v-model.

Overview

Bridges the gap between selection composables and Vue’s v-model system, enabling seamless two-way data binding for form controls backed by selection registries. Key Features:
  • Bidirectional synchronization between registry and v-model
  • Array and single-value modes
  • Automatic cleanup on scope disposal
  • Transform functions for value conversion
  • Lazy registration support (syncs when items register)
  • Perfect for form controls with selection backing

Signature

function useProxyModel<Z extends SelectionTicket = SelectionTicket>(
  registry: SelectionContext<Z>,
  model: Ref<unknown>,
  options?: ProxyModelOptions,
): () => void

Parameters

registry
SelectionContext<Z>
required
The selection registry to bind to. Created via createSelection().
model
Ref<unknown>
required
The ref to sync. Used as v-model binding.
options
ProxyModelOptions
Configuration options for the proxy model.
multiple
MaybeRefOrGetter<boolean>
default:"false"
Enable array mode for multi-selection. When true, model syncs as array.
transformIn
(val: unknown) => unknown
Transform function applied to model value before looking up in registry.
transformOut
(val: unknown) => unknown
Transform function applied to registry values before updating model.

Return Value

stop
() => void
Function to stop synchronization and clean up watchers.

Basic Usage

Single Selection

<script setup lang="ts">
import { ref } from 'vue'
import { createSelection, useProxyModel } from '@vuetify/v0'

const model = ref()
const registry = createSelection({ events: true })

registry.onboard([
  { id: 'item-1', value: 'Item 1' },
  { id: 'item-2', value: 'Item 2' },
  { id: 'item-3', value: 'Item 3' },
])

const stop = useProxyModel(registry, model)

// Now model and registry stay in sync
model.value = 'Item 2' // Selects item-2 in registry
registry.select('item-3') // Updates model to 'Item 3'
</script>

<template>
  <div>
    <p>Selected: {{ model }}</p>
    <button @click="model = 'Item 1'">Select Item 1</button>
    <button @click="model = 'Item 2'">Select Item 2</button>
  </div>
</template>

Multiple Selection

<script setup lang="ts">
import { ref } from 'vue'
import { createSelection, useProxyModel } from '@vuetify/v0'

const model = ref<string[]>([])
const registry = createSelection({ events: true })

registry.onboard([
  { id: 'red', value: 'Red' },
  { id: 'green', value: 'Green' },
  { id: 'blue', value: 'Blue' },
])

useProxyModel(registry, model, { multiple: true })

model.value = ['Red', 'Blue'] // Selects red and blue in registry
</script>

<template>
  <div>
    <p>Selected colors: {{ model.join(', ') }}</p>
    <label v-for="color in ['Red', 'Green', 'Blue']" :key="color">
      <input
        type="checkbox"
        :value="color"
        :checked="model.includes(color)"
        @change="(e) => {
          if (e.target.checked) model = [...model, color]
          else model = model.filter(c => c !== color)
        }"
      >
      {{ color }}
    </label>
  </div>
</template>

Advanced Usage

Transform Functions

Use transforms to normalize values between model and registry:
import { ref } from 'vue'
import { createSelection, useProxyModel } from '@vuetify/v0'

const model = ref()
const registry = createSelection({ events: true })

registry.onboard([
  { id: 'item-1', value: 'VALUE-1' },
  { id: 'item-2', value: 'VALUE-2' },
])

// Model uses lowercase, registry uses uppercase
useProxyModel(registry, model, {
  transformIn: (val) => String(val).toUpperCase(),
  transformOut: (val) => String(val).toLowerCase(),
})

model.value = 'value-1' // Matches VALUE-1 in registry
console.log(model.value) // 'value-1' (lowercase)

Lazy Registration

Proxy handles items that register after model is set:
import { ref } from 'vue'
import { createSelection, useProxyModel } from '@vuetify/v0'

const model = ref('Item 2')
const registry = createSelection({ events: true })

// Set up sync before items exist
useProxyModel(registry, model)

// Items register later - 'Item 2' will be automatically selected
setTimeout(() => {
  registry.onboard([
    { id: 'item-1', value: 'Item 1' },
    { id: 'item-2', value: 'Item 2' }, // Auto-selected
    { id: 'item-3', value: 'Item 3' },
  ])
}, 1000)

Manual Cleanup

import { ref, onUnmounted } from 'vue'
import { createSelection, useProxyModel } from '@vuetify/v0'

const model = ref()
const registry = createSelection({ events: true })
const stop = useProxyModel(registry, model)

// Stop synchronization manually
stop()

// Or automatically on component unmount
onUnmounted(stop)

Type Safety

import type { SelectionTicket } from '@vuetify/v0'

interface User {
  id: string
  name: string
  email: string
}

interface UserTicket extends SelectionTicket {
  id: string
  value: User
}

const model = ref<User>()
const registry = createSelection<UserTicket>({ events: true })

registry.onboard([
  { id: '1', value: { id: '1', name: 'Alice', email: '[email protected]' } },
  { id: '2', value: { id: '2', name: 'Bob', email: '[email protected]' } },
])

useProxyModel<UserTicket>(registry, model)

model.value = { id: '1', name: 'Alice', email: '[email protected]' }

Notes

  • Requires events: true on the selection registry
  • Automatic cleanup via onScopeDispose (no manual cleanup needed in most cases)
  • Uses flush: 'sync' watchers to prevent infinite loops
  • Pending values are tracked until matching items register
  • In single mode, setting model to new value unselects previous selection
  • In multiple mode, model array changes trigger symmetric difference updates

Build docs developers (and LLMs) love