Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler-sfc): introduce defineModel macro and useModel helper #8018

Merged
merged 26 commits into from Apr 8, 2023

Conversation

sxzz
Copy link
Member

@sxzz sxzz commented Apr 3, 2023

⚠️ Status: Experimental

RFC: vuejs/rfcs#503

Summary

This PR introduces a new macro called defineModel that enhances the developer experience when using two-way binding props. 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.

The PR also includes a new runtime helper function called useModel. This function is used as the underlying output of defineModel, and can be used in plain setup() functions where SFC isn't used. It works similarly to useVModel from VueUse.

Basic Usage

defineModel

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.

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.

// 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<stirng>({ 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 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 })

See demo

useModel

import { useModel } from 'vue'

export default {
  props: {
    modelValue: { type: Number, default: 0 },
  },
  emits: ['update:modelValue'],
  setup(props) {
    const modelValue = useModel(props, 'modelValue')
       // ^? Ref<number>

    return { modelValue }
  }
}

See demo


Related:

@sxzz sxzz changed the title feat(compiler-sfc): introduce defineModel macro feat(compiler-sfc): introduce defineModel macro and useModel helper Apr 3, 2023
@sxzz sxzz force-pushed the feat/define-model branch 2 times, most recently from ed8c3fb to fdd91c6 Compare April 5, 2023 11:28
@sxzz sxzz marked this pull request as ready for review April 5, 2023 19:01
@sxzz sxzz requested a review from yyx990803 April 5, 2023 19:02
@sqal
Copy link
Contributor

sqal commented Apr 5, 2023

const modelValue = defineModel({ type: null, required: false, default: 0 })

// generated code
const __sfc__ = _defineComponent({
  props: {
    "modelValue": { required: true, ...{ type: null, required: false, default: 0 } },
  },
})

Can this output be optimized to modelValue: { type: null, required: false, default: 0 } ?

Second thing. Why is type any casted to Unknown?

const modelValue = defineModel<any>()

// output
"modelValue": { type: Unknown, required: true }

@sxzz
Copy link
Member Author

sxzz commented Apr 5, 2023

@sqal Thanks for reviewing. The first issue can be optimized but it only happened in development mode. So now it's not very worth improving it.

The second issue is fixed now.

@@ -1391,12 +1509,12 @@ const emit = defineEmits(['a', 'b'])
expect(content).toMatch(`const props = __props`)

// foo has no default value, the Function can be dropped
expect(content).toMatch(`foo: null`)
expect(content).toMatch(`bar: { type: Boolean }`)
expect(content).toMatch(`foo: { required: true }`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be avoided - in production mode required is useless since there will be no warnings / checks, so we should generate the minimal amount of options to save bundle size.

Copy link
Member Author

@sxzz sxzz Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's intentional. useModel requires required option to check if a prop is passive.

BTW required: false can still be omitted. (done)

runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}

let propsDecl = genRuntimeProps()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let propsDecl = genRuntimeProps()
const propsDecl = genRuntimeProps()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope ESLint prefer-const can be enabled.

packages/compiler-sfc/src/compileScript.ts Outdated Show resolved Hide resolved
packages/compiler-sfc/src/compileScript.ts Outdated Show resolved Hide resolved
packages/runtime-core/src/apiSetupHelpers.ts Outdated Show resolved Hide resolved
@vercel
Copy link

vercel bot commented Apr 6, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Ignored Deployment
Name Status Preview Updated (UTC)
sfc-playground ⬜️ Ignored (Inspect) Apr 8, 2023 4:00am

@sxzz sxzz requested a review from yyx990803 April 6, 2023 12:42
@yyx990803 yyx990803 merged commit 14f3d74 into main Apr 8, 2023
14 checks passed
@yyx990803 yyx990803 deleted the feat/define-model branch April 8, 2023 04:13
* modelValue.value = "hello"
*
* // default model with options
* const modelValue = defineModel<stirng>({ required: true })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 * const modelValue = defineModel<string>({ required: true })

IAmSSH pushed a commit to IAmSSH/core that referenced this pull request May 14, 2023
@Merkushin-AY
Copy link

@sxzz Why is component that has defineModel not working without v-model from outside?

Example from your demo:
App.vue:

<script setup>
import Comp from './Comp.vue'
import {ref} from 'vue'
  
const count = ref(0)
</script>

<template>
  <Comp /> <!-- NOT WORKING -->
  <hr />
  <Comp v-model="count" />
  <hr />
  <input v-model="count"/>
  {{ count }}
</template>

Comp.vue:

<script setup lang=ts>
import { ref } from 'vue'

const modelValue = defineModel<number>({ default: 0 })
</script>

<template>
  <button @click="modelValue++">+</button>
  {{modelValue}}
</template>

So you have to write a bunch of excess code to fix it:
Comp.vue:

<script setup lang=ts>
import { ref, watchEffect } from 'vue'

const modelValue = defineModel<number>({ default: 0 })

const counter = ref(modelValue.value)

watchEffect(() => {
  counter.value = modelValue.value
})
</script>

<template>
  <button @click="() => {counter++; modelValue++;}">+</button>
  {{counter}}
</template>

@loosheng
Copy link

@Merkushin-AY You can see vuejs/rfcs#503 Local Mode Chapter.

@coolCucumber-cat
Copy link

When will this no longer be experimental and opt-in? Should I avoid using it in my project until it's stable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants