Files
agent-skills/skills/vue-best-practices/reference/render-function-v-model-implementation.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

4.8 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Implement v-model Correctly in Render Functions MEDIUM Incorrect v-model implementation breaks two-way binding with components best-practice
vue3
render-function
v-model
two-way-binding

Implement v-model Correctly in Render Functions

Impact: MEDIUM - When using v-model on components in render functions, you must manually handle both the modelValue prop and the update:modelValue event. Missing either part breaks two-way binding.

In templates, v-model is syntactic sugar that expands to a modelValue prop and an update:modelValue event listener. In render functions, you must implement this expansion manually.

Task Checklist

  • Pass modelValue as a prop for the bound value
  • Pass onUpdate:modelValue handler to receive updates
  • For named v-models, use the corresponding prop and event names
  • Use emit in child components to trigger updates

Incorrect:

import { h } from 'vue'
import CustomInput from './CustomInput.vue'

export default {
  setup() {
    const text = ref('')

    return () => h(CustomInput, {
      // WRONG: Missing the update handler
      modelValue: text.value
    })
  }
}
import { h } from 'vue'
import CustomInput from './CustomInput.vue'

export default {
  setup() {
    const text = ref('')

    return () => h(CustomInput, {
      // WRONG: Wrong event name format
      modelValue: text.value,
      onModelValueUpdate: (val) => text.value = val
    })
  }
}

Correct:

import { h, ref } from 'vue'
import CustomInput from './CustomInput.vue'

export default {
  setup() {
    const text = ref('')

    return () => h(CustomInput, {
      // CORRECT: modelValue prop + onUpdate:modelValue handler
      modelValue: text.value,
      'onUpdate:modelValue': (value) => text.value = value
    })
  }
}

Native Input Elements

For native inputs, use value and onInput:

import { h, ref } from 'vue'

export default {
  setup() {
    const text = ref('')

    return () => h('input', {
      value: text.value,
      onInput: (e) => text.value = e.target.value
    })
  }
}

Named v-models (Multiple v-models)

import { h, ref } from 'vue'
import UserForm from './UserForm.vue'

export default {
  setup() {
    const firstName = ref('')
    const lastName = ref('')

    return () => h(UserForm, {
      // v-model:firstName
      firstName: firstName.value,
      'onUpdate:firstName': (val) => firstName.value = val,

      // v-model:lastName
      lastName: lastName.value,
      'onUpdate:lastName': (val) => lastName.value = val
    })
  }
}

v-model with Modifiers

Handle modifiers in the child component:

import { h, ref } from 'vue'
import CustomInput from './CustomInput.vue'

// Parent - passing modifier
export default {
  setup() {
    const text = ref('')

    return () => h(CustomInput, {
      modelValue: text.value,
      'onUpdate:modelValue': (val) => text.value = val,
      modelModifiers: { trim: true, capitalize: true }
    })
  }
}

// Child - handling modifier
export default {
  props: ['modelValue', 'modelModifiers'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const handleInput = (e) => {
      let value = e.target.value

      if (props.modelModifiers?.trim) {
        value = value.trim()
      }
      if (props.modelModifiers?.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }

      emit('update:modelValue', value)
    }

    return () => h('input', {
      value: props.modelValue,
      onInput: handleInput
    })
  }
}

JSX Syntax

import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

export default {
  setup() {
    const text = ref('')
    const count = ref(0)

    return () => (
      <div>
        {/* v-model on custom component */}
        <CustomInput
          modelValue={text.value}
          onUpdate:modelValue={(val) => text.value = val}
        />

        {/* v-model on native input */}
        <input
          value={text.value}
          onInput={(e) => text.value = e.target.value}
        />

        {/* Named v-model */}
        <Counter
          count={count.value}
          onUpdate:count={(val) => count.value = val}
        />
      </div>
    )
  }
}

Creating v-model-compatible Components

import { h } from 'vue'

export default {
  props: {
    modelValue: String
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    return () => h('input', {
      value: props.modelValue,
      onInput: (e) => emit('update:modelValue', e.target.value)
    })
  }
}

Reference