Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Summary

This proposal introduces a new SFC macro called defineModel that enhances the developer experience when declaring two-way binding props to be consumed by v-model.

With defineModel, v-model props can be declared and mutated like a ref.

  • The macro is a <script setup> only feature.
  • When compiled, it will declare a prop with the same name and a corresponding update:propName event.
  • Experimental feature. Introduced a new compiler script option defineModel, and it's disabled by default.

Basic example

Comp.vue

<script setup>
const modelValue = defineModel()

console.log(modelValue.value)
</script>

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

Parent.vue

<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue'

const msg = ref('')
</script>
<template>
  <Comp v-model="msg">
</template>

Motivation

Detailed design

If the first argument is a string, it will be used as the prop name; Otherwise the prop name will default to "modelValue". In both cases, you can also pass an additional object which will be used as the prop's options.

// default model (consumed via `v-model`)
const modelValue = defineModel()
   // ^? Ref<any>
modelValue.value = 10

const modelValue = defineModel<string>()
   // ^? Ref<string | undefined>
modelValue.value = "hello"

// default model with options, required removes possible undefined values
const modelValue = defineModel<string>({ required: true })
   // ^? Ref<string>

// with specified name (consumed via `v-model:count`)
const count = defineModel<number>('count')
count.value++

// with specified name and default value
const count = defineModel<number>('count', { default: 0 })
   // ^? Ref<number>

Local Mode

The options object can also specify an additional option, local. When set to true, the ref can be locally mutated even if the parent did not pass the matching v-model, essentially making the model optional.

// local mutable model, can be mutated locally
// even if the parent did not pass the matching `v-model`.
const count = defineModel<number>('count', { local: true, default: 0 })

Demo

Drawbacks

N/A

Alternatives

  • defineModels

    Declares multiple models in the same macro call. We opted for the single-model API for two reasons:

    1. It is more common for a component to declare only one model binding.

    2. The default usage defineModel(), which omits the prop name, aligns with the parent usage of v-model, which omits the argument. This completely removes the need for the modelValue prop name convention.

Adoption strategy

This is implemented and shipped as an experimental feature in 3.3 beta and requires explicit opt-in.

Vite

// vite.config.js
export default {
  plugins: [
    vue({
      script: {
        defineModel: true
      }
    })
  ]
}

vue-cli

Requires vue-loader@^17.1.1

// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => {
        return {
          ...options,
          defineModel: true
        }
      })
  }
}
You must be logged in to vote

Replies: 44 comments · 101 replies

Comment options

Could we also consider doing the same for the Options API (which is still a popular way of writing components)?

export default {
  models: [
    'foo', // name: 'foo', event: 'update:foo', required: false, default: undefined
    { name: 'value', event: 'updateValue', default: 0 },
    { name: 'bar', required: true }, // event: 'update:bar'
  ],
  props: {
    foo: String, // error: model is already declared
  },
}

We could then (finally) close #140.

You must be logged in to vote
3 replies
@yyx990803
Comment options

yyx990803 Apr 28, 2023
Maintainer Author

That would be a separate proposal - this RFC is strictly scoped to <script setup>.

@joezimjs
Comment options

That would be a separate proposal - this RFC is strictly scoped to SFCs.

Options API is available in SFCs. You mean it's only available for <script setup>.

@yyx990803
Comment options

yyx990803 May 2, 2023
Maintainer Author

Yes.

Comment options

What are the use cases for local: false? Would not be simplier to have local: true by default?

You must be logged in to vote
7 replies
@posva
Comment options

posva Apr 28, 2023
Collaborator

but if you provide a default value, you don't need required: true, do you? 🤔

@jods4
Comment options

@posva I recall for TS typing pre-setup and generics, you did, because required decides whether the type can be undefined.

// props.n: number | undefined = 0;
const props: { n: { type: Number, default: 0 } }

// props.n: number = 0;
const props: { n: { type: Number, required: true, default: 0 } }

As I said, maybe that's not super relevant for newer APIs, as in script setup I would do:

// This follows my TS generic type:
// n: number
// m: number | undefined
const { n = 0, m = 0 } = defineProps<{ 
  n: number, 
  m: number | undefined,
}>();
@joezimjs
Comment options

In what situation would a prop == undefined if a default is set?

@jods4
Comment options

@joezimjs nothing prevents you to explicitly bind an undefined value.

@posva
Comment options

posva May 2, 2023
Collaborator

@jods4 I'm glad the required is no longer necessary now though 😆

A prop that accepts undefined will still take the default value if explicitly set to undefined as it's the same as not being passed at all Example

Comment options

Very nice, it reminds me of how Aurelia just lets you write back into props to implement two-way bindings, great DX.

local: true is a really nice addition, because it's a fair amount of boilerplate to create a control that maintains state both when it's databound and when it's not.
Can you provide some details about the design decisions that went into it?

  • Is it implemented inside the core props code or as an extra layer (e.g. an additional ref that is sync'ed with the props?)
  • If it's an extra ref layer, does it exist always or is the component checking whether the component is two-way bound?
  • I assume that when model is assigned the same value (according to hasChanged) no event is raised (just like refs don't trigger)?

Speaking of hasChanged, this is outside the scope of this SFC but Vue is lacking a way to extend its behaviour (hard-coded to Object.is). For example, when using a date library such as Dayjs it is natural to treat same dates (but different objects) as equal. Many other frameworks have this, for example Lit has a hasChanged option when declaring reactive properties that you can define on a per-property basis.

You must be logged in to vote
1 reply
@yyx990803
Comment options

yyx990803 May 2, 2023
Maintainer Author

Is it implemented inside the core props code or as an extra layer

As an extra layer, with an additional ref and watchers. It works very similar to useVModel from VueUse.

If it's an extra ref layer, does it exist always or is the component checking whether the component is two-way bound?

It always exists if local: true is specified. Which is why it is false by default.

I assume that when model is assigned the same value (according to hasChanged) no event is raised (just like refs don't trigger)?

Correct.

Vue is lacking a way to extend hasChanged behaviour

An RFC is welcome. There are various aspects that need discussion. This can affect watchers, computed, and refs. Introducing extra options for each feels heavy handed, but a global option isn't flexible enough.

Comment options

Examples show only usage with primitive types, how will it work with object props?

Will it force (typewise) a factory for the default?

const count = defineModel<{ foo: string }>('count', { 
  default: () => ({ foo: '' })
})

Will it emit? I guess at this stage no, but would you consider adding a deep option (like in useVModel from vueuse)?

const ob = defineModel({ foo: string }>();

ob.value.foo = 'foo'; // or <input v-model="ob.foo">
You must be logged in to vote
4 replies
@yyx990803
Comment options

yyx990803 May 2, 2023
Maintainer Author

It does not emit, although if the object is deeply reactive then direct mutations should just trigger updates in parent without even emitting.

@jacekkarczmarczyk
Comment options

direct mutations should just trigger updates in parent without even emitting.

I'm fully aware of that, but I was always under the imporession that mutating prop's properties is not the best practice and you'd rather emit a new object with updated property. Not actually sure whether deep option is actually doing what I think it's doing

@setaman
Comment options

without even emitting

that's the point, this behavior breaks the consistency, some of the v-model's emit events, others not. I always end up defining a local state (based on the passed modelValue), mutating it and emitting to the parent as object. Although this generates boilerplate code, it ensures consistent behavior across all components. I think we still miss a better approach for this use case

@matthew-dean
Comment options

Wait, an object prop's changes don't emit? 🤔 Seems like sticking with VueUse is the better option then.

Comment options

I love this proposal for simple/standard cases because it can eliminate a bunch of boilerplate code, but I also am not a fan of the models now disappearing from the props definitions. I'm assuming (there's no example showing what it compiles down to, so I can't be certain) that I can still access the prop via prop.modelValue even if modelValue isn't defined in defineProps, right?

In complex cases, where I need to watch the prop and not update the local ref in certain conditions, there's no way to hook into this, so I'd have to skip using this, right?

You must be logged in to vote
2 replies
@yyx990803
Comment options

yyx990803 May 2, 2023
Maintainer Author

Correct. If you want this level of control, just roll it yourself.

@matthew-dean
Comment options

@joezimjs I'd recommend taking a look at: https://vueuse.org/core/useVModel/#usevmodel

Comment options

TS error

"vue": "^3.3.1",
"vite": "^4.3.5",
Type '{ defineModel: true; }' is not assignable to type 'Partial<Pick<SFCScriptCompileOptions, "babelParserPlugins">>'.
 
Object literal may only specify known properties, and 'defineModel' does not exist 
  in type 'Partial<Pick<SFCScriptCompileOptions, "babelParserPlugins">>'

image

You must be logged in to vote
1 reply
@aleksey-hoffman
Comment options

Fixed in vite-plugin-vue v4.2.3

Comment options

image

export default defineConfig(
{
//... rest of configurations
	plugins: [
		vue({
			script: {
				defineModel: true,
				propsDestructure: true,
			},
		}),
//... rest of plugins
]
}
You must be logged in to vote
7 replies
@Lin-JayLen
Comment options

I have the same problem

@angelhdzmultimedia
Comment options

Solved

On nuxt.config.ts:

 vite: {
    vue: {
      script: {
        defineModel: true,
      },
    },
  },

I've been searching for ages! Discord, YouTube, Google, even ChatGPT!!! Can't get this to work on Nuxt 3 or Vue.
Already installed all latest and beta versions of packages like vue, nuxt, @vue/compiler-sfc, vite. Attempted to override the packages in the package.json. defineModel is undefined when compiled to JS.

The one in the macros library works fine: https://vue-macros.sxzz.moe/macros/define-models.html

@darrylnoakes
Comment options

Just an aside, there is no way stock ChatGPT is going to be helpful about this: it's training data only goes up to 2021.

@leopiccionia
Comment options

In a plain Vite project, updating vue and @vitejs/plugin-vue solved my issue.

For Nuxt users, it seems like you have to wait the soon-to-be-released Nuxt 3.5.

@angelhdzmultimedia
Comment options

I got it working thanks to someone in Discord. Updated my reply.

Comment options

Why not expose this idea as writable props?

It's pretty simple to understand conceptually (defineModel defines a prop + an event, after all) and it works in classical <script> as well.

It would look something like:

export default {
  props: {
    // New prop attribute: "model: true" or maybe "writable: true"
    value: { type: Number, model: true },
  },
  setup(props) {
    props.value = 100; // emits "update:value"
  }
}
You must be logged in to vote
8 replies
@jods4
Comment options

From my personal experience with frameworks where it works like that, I never found it confusing nor have I seen someone say so.

In particular with defineProps destructuring experiment, I'd say this proposal is not any more hidden than defineModel.
At assignment site, both a destructured defineProps and defineModel would look identical, they're simple variable assignments. There's no telling sign that one raises an event and the other does not.
At declaration, I'd say that { model: true } is as explicit as definedModel, it is a simple marker with a clear meaning.

EDIT: I just realised the assignment site remark is is not 100% accurate. defineModel returns a ref so it'd be model.value = 1, whereas destructured props is just model = 1.
I'd see this as a benefit because just as we make one step towards plain variable for props, we're making one step back to ref and .value for models.

On the other hand, if the destructured props experiment is eventually dropped, assigning to props variable such as props.model = 1 is a very clear sign that you're sending an event, as props are otherwise be read-only.

@coolCucumber-cat
Comment options

Why not expose this idea as writable props?

I think it would make way more sense if it's all together instead of separated into two separate functions, and models are props anyway. though my idea was to have the options also be part of the type definition, if that's possible.

@coolCucumber-cat
Comment options

defineModel returns a ref so it'd be model.value = 1, whereas destructured props is just model = 1

But if it's model = 1 it can't be reactive? It can only be props.model = 1 or model.value = 1, there has to be some kind if property for it to be reactive/ref.

@jods4
Comment options

But if it's model = 1 it can't be reactive? It can only be props.model = 1 or model.value = 1, there has to be some kind if property for it to be reactive/ref.

There's an experimental feature in Vue 3.3 (has to be enabled with an option) that makes destructured props "magically" reactive.

// !! experimental, not yet for prod !!

// This code in your component:
let { model } = defineProps(...);
const x = model;
model = 42;

// Is transformed by compiler into
const x = props.model;
props.model = 42;
@ZackPlauche
Comment options

Personally I'm a fan of mutable props.

This comment has been hidden.

Comment options

defineModel default has a "wrong" typescript definition.
why is the default set to the type any in apiSetupHelpers.ts ?

export function defineModel<T>(
  options: { default: any } & PropOptions<T> & DefineModelOptions
): Ref<T>

wouldn't it be better to just leave this out, so that the correct definition of PropOptions can be used ?

You must be logged in to vote
1 reply
@yyx990803
Comment options

yyx990803 Dec 12, 2023
Maintainer Author

It's not "wrong" - it's just there to force the overload to be used when default is present. Because it's an intersection, the type constraints for default in PropOptions<T> still apply.

Comment options

Anyone get this working in Storybook 7? Overriding the @vitejs/plugin-vue in Storybook's config breaks everything

You must be logged in to vote
2 replies
@emosheeep
Comment options

Anyone get this working in Storybook 7? Overriding the @vitejs/plugin-vue in Storybook's config breaks everything

Did you mean that it breaks the app with a postcss unexpected token error?

@joneath
Comment options

When adding

vue({
  script: {
    defineModel: true,
    propsDestructure: true,
  },
})

to Storybook's viteFinal config I get the following on boot:
[vite] Internal server error: At least one <template> or <script> is required in a single file component.

Comment options

Nothing much to add to the discussion other than I'm finding defineModel to be very useful, and am hoping it becomes permanent, not experimental. :)

You must be logged in to vote
3 replies
@MajedMuhammad
Comment options

I went to use it at my own risk. I hope it becomes official too.
It is constructive when you define a custom component to be used multiple times. It saves much effort and time and prevents redundant code.

@Shinigami92
Comment options

I also opened a PR in my company Repo, but we want to merge it if it gets out of experimental
But the PR is already working find 👌
Around 50 files affected

@postmodernistx
Comment options

One more shoutout, found this incredibly convenient!

Comment options

should defineModel suppose to have a deep mode? It is convenient to bind vmodel with items of an array or object in v-for, for now, changes on item doesn't emit model event.

You must be logged in to vote
1 reply
@andredewaard
Comment options

This doesn't work. Vue wont allow you to bind a value in a v-for.

<SchemaCollectionProvider v-for="schema in schemas" v-model:schema="schema" :key="schema.id">

throws: Internal server error: v-model cannot be used on v-for or v-slot scope variables because they are not writable.

Comment options

Might've asked this sometime before, can't rememeber when or where though, but when will this become stable? I'm just hoping it works by the time my code goes into production sometime in the distant future.

You must be logged in to vote
0 replies

This comment was marked as disruptive content.

@angelhdzmultimedia
Comment options

Jesus Christ, and people complained about Options API having too much Magic. This is straight up confusing as hell. Had to look at the blog post for a better example and I'm still not 100% I understand. Like... creating any variable name with it will make it a prop automatically and also create an update:variableName under the hood that is emitted? Is that what's happening? That's so unintuitive. It's the exact opposite of obvious. It should be called vModelPropEmit or something to make it more obvious what it's doing.

It's marked as experimental. I hope it doesn't actually make it into Vue for real because that is going to make dealing with codebases fucking impossible to understand for people new to Vue. Feels very much like "the bad parts" of JavaScript that no one uses because we all know they are anti-patterns now. Like only people who think they are "clever" would use this, and they would over use and abuse it in awful ways. yikes

LOL emotional baby complaining. You use the words "unintuitive" as to make your point logical and smart and objective but all you're doing is complaining because you don't like something.

Hooks are completely intuitive. They are being used in every language now.
The OptionsAPI was hiding the magic. With Script Setup and its compiler macros, we are in control of that magic.
We use it or we don't. Simply as that. You can always emit all your props changes manually if you want to do more work LOL.

Having a hook that automatically emits props changes for the parent is unintuitive for you? WTF!?
Do you also have issues with .value mutation that automagically triggers a re-render in the view? 😂

I tried to take your comment seriously but you didn't give me anything useful to work with. Using curse words and all, you are just a troll.

Keep using the OptionsAPI there's a "Vue 2 Reviver" now. Haha.

@coolCucumber-cat
Comment options

So you understand this terrible garbage:

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
  emit('update:modelValue', e.target.value)
}
</script>
<template>
  <input :value="modelValue" @input="onInput" />
</template>

But not this?:

<script setup>
const modelValue = defineModel()
</script>
<template>
  <input v-model="modelValue" />
</template>

"opposite of obvious", "unintuitive", "the bad parts", "would get abused". Yeah it's very clear you don't actually understand it whatsoever. What is there not to get? It's so easy. It's a two way binding of a value. The model is a reference to the one in the v-model, as if they're the same value. Oh no, props and emits are registered without you knowing, well so do defineProps and defineEmits, better stop using those too.

This function has pretty much the same usage as defineModel, you just need to declare props and emits and pass them through:

const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
const modelValue = defineModel(props, emits)

function defineModel(props, emit, name = 'modelValue'){
  return computed({
    get: () => props[name],
    set: value => emits(`update:${name}`, value)
  })
}

Wow, Composition API so complicated and magic. Only reason it looks dumb is out of my control and now they're fixing it with defineModel. You probably write all those god awful comments saying composition api sucks because you don't understand .value and this. is way easier (despite the fact you can just have a reactive variable and call it that). You probably think mixins are good too (I never want to find out if this is a good insult or not, but I trust Vue in this when they said it was terrible). You could even put this thing in a composable (which are actually good mixins) and have Nuxt auto import it for you. So opaque and unextendable

Wanna know why you don't think it's obvious? Because defineModel is extremely obvious and intuitive and the previous way was extremely unobvious and intuitive, so when you explain how you would do the same previously, as I just did, it looks really stupid. Which is kinda why the new way exists.
New people will find this really easy, because it is, and you can find it easy too if you stop comparing it to the dumbass way of doing it which just makes it seem complicated.

Kinda reminds me of the time where someone said 24 hour time was more complicated than 12 hour time. This has to be satire.

TLDR: It's just a two way binding/reference to a value. It's really, really easy. Composition API based. (and people who use Options API but know it's not as good).

@angelhdzmultimedia
Comment options

Like only people who think they are "clever" would use this

😂 I use this and I don't find myself "clever". I just use the things I find useful. Like, ermmm, everything it's supposed to be in life?
Unless you are the overarchiver that craves for attention and do things in a "special way" than everyone else just to "prove something".

And I'm not of the crew that falls for evey new and shiny thing that comes. I tried the reactivity transform with the $ref and $$() and that was unintuitive, and I made a lot of mistakes because I was used to .value. Ultimately I was right, the team saw that people were not liking it and removed it.

defineModel was the missing piece of the puzzle. There's no utility in having a ref() that automatically forces a render in the view by mutating it, if we can't do the same thing with props that come from the parent. defineModel solves this.
It emits the model change event for us just by mutating the props, and the props keep being reactive.

It doesn't eliminate the need for emits. We still need to emit other events to the parent. It only encapsulates the events to change parent's model values.

I genuinely want to see if you aren't trolling and are just having a bad day. But if I'm wrong, enjoy the attention! :)

And to Vue team:

Great job like always! Wishing you all health, peace, and success.

Comment options

i recently come back learn coding and i pick vue, I'm quite headache with the how to create my custom input component. End up I use emit, then notice that definemodel is helpful.

as user who not familiar with vue my first issue is props is immutable, is that anyway can use syntax like below?

props = defineProps<{prop1:string, modelValue:{type:string, changable:true}}>

then you guys run 'defineModel' behind the scene?

Or there is another approach i feel workable:
use new method like defineAttribute(), which is sit infront of defineProps() and defineModel(), it will some property is immutable and v-model is mutable. it reduce complexity and in future new comer only need to know defineAttribute()

You must be logged in to vote
2 replies
@ThomasBerneHCSE
Comment options

The problem is that you are mixing typing and definition of props. It could be possible to implement something like this:

defineProps<{
  prop1?: string,
  modelValue: string
}>({
  models: [ 'modelValue' ]
});
@kstan79
Comment options

May use better keyword:

defineProps<{prop1:string,modelValue:string}>({mutable:['modelValue']})

Comment options

How does defineModel handle v-model modifiers? Are v-model modifiers never used with defineModel?

You must be logged in to vote
4 replies
@yyx990803
Comment options

yyx990803 Dec 12, 2023
Maintainer Author

You can still access them via the modelModifiers prop (currently needs to be declared separately):

const props = defineProps<{ modelModifiers: { trim?: true } }>()
const msg = defineModel<string>()

if (props.modelModifiers.trim) {
  // ...
}

We can potentially make this automatically handled:

const msg = defineModel<string, { trim?: true }>()
//    ^ Ref<string | undefined> & { modifiers: { trim?: true }}

if (msg.modifiers.trim) {
  // ...
}

I think we will probably ship current defineModel as stable without this though, because:

  1. This is currently not a blocking issue, and seems to be rarely used;
  2. It can be provided as a follow-up to the current design, and it doesn't seem worth it to cause breaking changes to the current design.
@yyx990803
Comment options

yyx990803 Dec 28, 2023
Maintainer Author

Follow up on this: we have implemented modifiers support in the latest 3.4.0-rc.3 that doesn't change anything already discussed in this RFC:

const [msg, modifiers] = defineModel<string, 'trim' | 'capitalize'>()

if (modifiers.trim) {
  // ...
}

if (modifiers.capitalize) {
  // ...
}

// type error!
if (modifiers.foo) {}

The returned ref implements Symbol.iterator to support the array destructure.

Also implemented get & set transformers so the value conversion can be done without the need for an extra computed wrapper:

const [msg, modifiers] = defineModel<string>({
  set(value) {
    if (modifiers.trim) {
      value = value.trim()
    }
    if (modifiers.capitalize) {
      value = capitalize(value)
    }
    return value
  }
})
@erropix
Comment options

@yyx990803 I think it's better to pass the modifiers to the get/set callbacks as second argument:

const msg = defineModel<string>({
  set(value, modifiers) {
    if (modifiers.trim) {
      value = value.trim()
    }
    if (modifiers.capitalize) {
      value = capitalize(value)
    }
    return value
  }
})
@jods4
Comment options

While passing modifiers to get and set looks more encapsulated, avoids captures, and pollutes the global scope less, there are advantages to having them available globally.

You can change the behavior of your component based on modifiers, or react to the modifiers changing.
For example you couldn't create a .debounced modifier inside set().

Maybe having both (globally available + passed to get/set as second parameter) would be a nice compromise.

Comment options

Is it possible to use defineModel inside a component with v-for?
im looping over an array of 'schemas' and these all have their own websocket that updates their own schema, so i would bind it like this

<SchemaCollectionProvider v-for="schema in schemas" v-model:schema="schema" :key="schema.id">
  <SchemaCard :schema="schema'/>
</SchemaCollectionProvider>

This is really simplified of course.

Would the only option be to use a prop with event and do this?

<script setup lang=ts>

const schemas = ref<SchemaCollectionModel[]>([])

function updateValue(index: number, schema: SchemaCollectionModel) {
  schemas.value.splice(index, 1, schema)
}
</script>

<template>
      <SchemaCollectionProvider v-for="(schema, i) in schemas" :schema="schema" :key="schema.id" @update:schema="(event) => updateValue(i, event)">
          <SchemaCard :schema="schema">
      </SchemaCollectionProvider>
</template>
You must be logged in to vote
1 reply
@coolCucumber-cat
Comment options

You could do schema, schemaIndex in schemas and then schemas[schemaIndex]

Comment options

@yyx990803 or somebody at Vue, could we please get an update on the status of this feature? It's been experimental for a long time now and we have not heard anything about it's future. There have been many patch updates since v3.3. I assume if it is kept we'll have to wait for v3.4?

You must be logged in to vote
1 reply
@skirtles-code
Comment options

See vuejs/core#9598.

Comment options

I just want to share my feedback about this feature. Our team has been using it for about two months and it does make using v-model much more enjoyable, looking back defining props/emits before feels like boilerplate.

I guess the only concern I have is that we always export our props interface, so other component consumers can extend it if needed. Since defineModel is separate thing it sometimes easy to miss, but you can still define modelValue prop and they do not clash, so that isn't major issue.

You must be logged in to vote
0 replies
Comment options

Overall, I think this is a nice addition.

There are a few minor inconsistencies that can occur in local mode. Whether these are worth fixing I'm not sure, but if not then they might impact how the feature is documented.

I think people will perceive local: true as just being a way to make v-model optional. If the parent is passing a v-model value, then it might be reasonable to expect the local setting to have no impact. In reality, using local mode makes other subtle changes that might be seen as unexpected inconsistencies.

The relevant Vue code that leads to these differences can be seen here:


Problem 1

Consider this example:

SFC Playground

This uses local: true, but the v-model is being passed from the parent anyway. When clicking the button, notice how the parent re-renders twice in the logs. The underlying cause of this double rendering is the internal use of a flush: 'pre' watcher to emit the event, which runs between the parent renderings.

If you change the example to local: false it will only render the parent once. The event is now emitted synchronously inside the setter, so the scheduler queue doesn't defer the event.

Perhaps the local watcher should use flush: 'sync' instead, for consistency with the get/set approach?

Problem 2

The same example shows the second problem. This time, take a look at the value for model that is logged with After update.

With local: true, the After update shows the updated value. With local: false the logged value is unchanged, as it needs to wait for a parent re-render to update the value.

It could be argued that this is not an inconsistency, it's just what the local setting does. local: false stays rigidly in sync with the current prop value, whereas local: true can get out of sync. However, I think there's potential for confusion if local is only documented as a way to make v-model optional, as that alone wouldn't explain the apparent inconsistency in my example. The example is passing a v-model in both cases, so whether v-model is optional doesn't really account for the difference.

Problem 3

This is the flip-side of the previous problem.

Consider this example:

SFC Playground

The parent attempts to apply upper and lower bounds to the value. With local: false this works fine, but the child drifts out of sync with local: true.


Just to reiterate, I'm not necessarily suggesting that these inconsistencies need fixing, but I do think they're worth pondering. It might impact the documentation if nothing else.

You must be logged in to vote
1 reply
@yyx990803
Comment options

yyx990803 Dec 12, 2023
Maintainer Author

When trying to address these issues, I realized the previous behavioral difference between local vs. non-local mode is kinda pointless (and in fact, brings about unnecessary confusion).

When local is false and the parent doesn't use v-model on the child, the child ref value will always be undefined, making it useless. If the user declared the model with required: true, there would already be a warning about missing modelValue.

In vuejs/core@f74785b I've removed the local option, making the model automatically adapt based on whether the parent provides v-model or not:

  • If the parent provides the necessary v-model prop, the child ref value will always be in sync with parent prop value.
  • Otherwise, the child ref will behave like a normal local ref.

After the commit, Problems (1) and (3) are solved. Behavior difference in (2) still exists, but is no longer determined by the local option - instead, it depends on whether the parent provided the v-model props, which I think is reasonable.

Comment options

To enable it in quasar, add the following line to quasar.config.js:

    build: {
...
      viteVuePluginOptions: { script: { defineModel: true } },
You must be logged in to vote
0 replies
Comment options

If it's not a ts project, how could I define the type of the props which is previously defiend by:

props:{
    foo: {tyep: Number}
}
You must be logged in to vote
1 reply
@yyx990803
Comment options

yyx990803 Dec 28, 2023
Maintainer Author

defineModel('foo', { type: Number })

Comment options

How do I approach this case where when I emit the update:modelValue, I pass multiple arguments?

For example, I have a component that I need both the current and previous value because I have to compare them before doing other things. So I have this kind of component:

<select :value="modelValue" @change="$emit('update:modelValue', $event.target.value, modelValue)">

I also have a case where it isn't necessarily a previous value, but some other data.

If I've approached this solution wrong even in the previous way, please correct me.

You must be logged in to vote
1 reply
@erropix
Comment options

When you use the component you can't do that with regular v-model directive, instead you need to handle the event and prop yourself:

<MyComponent :modelValue="modelValue" @update:modelValue="modelValueHandler">

The modelValueHandler function should look like:

function modelValueHandler(newValue, oldValue) {
    // handle modelValue change
}
Comment options

I'm having an issue with getting the same behaviour that I got with { local: true }. I assume that the intention is that using { required: false, default: ... } should yield the same behaviour, but when I try this with a literal object default (default : {}) or a factory default (default: () => ({})), I lose all reactivity with anything depending on the model. Inspecting with vue dev tools, I can see that the modelValue is being updated, but nothing depending on it is.

You must be logged in to vote
0 replies
Comment options

Congratulations on the release of Vue 3.4! I find defineModel to be a very useful feature. However, I've also realized that defineModel can be props with non-one-way data flow. Consequently, it's important to encourage developers not to use v-model too much for data exchange between parent and child components.

You must be logged in to vote
2 replies
@ZackPlauche
Comment options

Question as someone who likes the 2 way data flow and wants it to be this easy: What is the best practice for this?

I really like mutable props (and basically use defineModel exactly for this).

@ZackPlauche
Comment options

It makes a lot of sense on dynamic components that take in some sort of an object that you want to edit it with. Like forms and what not. Otherwise it's always felt over-complicated. Just pass in 1 variable that can be edited and be done with it. I don't want to manage all the events and what not.

Comment options

Using defineModel with an array does not seem to work when splicing.
const model = defineModel({ default: [], type: Array })

model.value.splice(idx, 1) does not update the model.
A workaround seems to be this: model.value = model.value

Is this intended??

You must be logged in to vote
9 replies
@nined9
Comment options

<script setup>
const model = defineModel({ type: Array, default: [] })
</script>

<template>
  <div>Not updating: {{ model.join(',') }}</div>
  <button @click="model.shift();">-1</button>
  <button @click="model.push(model.length);">+1</button>
</template>

it like this? SFC Playground

@jods4
Comment options

the same playground as @nined9 with a reactive array (default() { return reactive([]) }) works:
Modified SFC Playground

@HeuHax
Comment options

@jods4 default() { return reactive([]) } works amazing! Thank you so much!
I feel a bit dumb for not trying the default as a reactive([]).

@dobromir-hristov
Comment options

Unfortunately if you set the model.value in setup or just overwrite model in the template, it loses reactivity, if you return a reactive like that :/

SFC Playground

@bmarkovic
Comment options

@dobromir-hristov you are correct, the only workaround is passing e.g. a ref from an external consumer which defeats the purpose of a prop default.

Comment options

I have a small issue with the trim Modifier:
on a standard input v-model.trim trims only on blur, however, using defineModel on a child input it will trim on Backspace

Example

Parent

<script setup>
import { ref } from 'vue'
import Child from 'Child.vue'

const testModel = ref("foo        bar")
</script>

<template>
  <Child v-model.trim="testModel" />
</template>

Child

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

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

If I Backspace 'bar' the cursor will jump to the end of 'foo' -- I would expect the result to be 'foo ' until blur.
It does this with single spaces as well.

I've tried this in Playground with the same result.

You must be logged in to vote
1 reply
@Shinigami92
Comment options

This is the wrong place to discuss issues. Please open a separate issue.

Comment options

Just upgraded from 3.3, and apparently local is gone?

Screenshot 2025-04-01 at 11 56 23

Do I understand it right that all defined models are now local by default?

You must be logged in to vote
1 reply
@jh-leong
Comment options

jh-leong Apr 1, 2025
Collaborator

Yes, #503 (reply in thread)

Comment options

1.Why use watchSyncEffect to keep a local copy in sync instead of simply returning props[name] directly in the getter?
2.If it’s an object, the set won’t be triggered. Without cloning a local copy, you would be directly mutating the parent component’s data reference. Would that still align with the concept of one-way data flow?

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Morty Proxy This is a proxified and sanitized view of the page, visit original site.