From da6e6246d1710c8f06e88a9e417e7ac80d0964d6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 26 Dec 2023 22:42:08 +0800 Subject: [PATCH] feat: component v-model update to use defineModel + defineModel modifiers & transformers --- src/api/sfc-script-setup.md | 61 +++++-- src/guide/components/v-model.md | 314 ++++++++++++++++++++------------ 2 files changed, 243 insertions(+), 132 deletions(-) diff --git a/src/api/sfc-script-setup.md b/src/api/sfc-script-setup.md index 4b9003b3d1..34b924a768 100644 --- a/src/api/sfc-script-setup.md +++ b/src/api/sfc-script-setup.md @@ -229,40 +229,60 @@ This will be compiled to equivalent runtime props `default` options. In addition ## defineModel() {#definemodel} -This macro can be used to declare a two-way binding prop that can be consumed via `v-model` from the parent component, and it can be declared and mutated like a ref. This will declare a prop with the same name and a corresponding `update:propName` event. +This macro can be used to declare a two-way binding prop that can be consumed via `v-model` from the parent component. Example usage is also discussed in the [Component `v-model`](/guide/components/v-model) guide. -If the first argument is a literial 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. +Under the hood, this macro declares a model prop and a corresponding value update event. If the first argument is a literal 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 can include the prop's options and the model ref's value transform options. -```vue - - - ``` -### Local mode +### Modifiers and Transformers -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. +To access modifiers used with the `v-model` directive, we can destructure the return value of `defineModel()` like this: -```ts -// local mutable model, can be mutated locally -// even if the parent did not pass the matching `v-model`. -const count = defineModel('count', { local: true, default: 0 }) +```js +const [modelValue, modelModifiers] = defineModel() + +// corresponds to v-model.trim +if (modelModifiers.trim) { + // ... +} +``` + +Usually, we need to conditionally transform the value read from or synced back to the parent when a modifier is present. We can achieve this via the `get` and `set` transformer options: + +```js +const [modelValue, modelModifiers] = defineModel({ + // get() omitted as it is not needed here + set(value) { + if (modelModifiers.trim) { + return value.trim() + } + return value + } +}) ``` -### Provide value type {#provide-value-type} +### Usage with TypeScript {#usage-with-typescript} -Like `defineProps` and `defineEmits`, `defineModel` can also receive a type argument to specify the type of the model value: +Like `defineProps` and `defineEmits`, `defineModel` can also receive type arguments to specify the types of the model value and the modifiers: ```ts const modelValue = defineModel() @@ -271,6 +291,9 @@ const modelValue = defineModel() // default model with options, required removes possible undefined values const modelValue = defineModel({ required: true }) // ^? Ref + +const [modelValue, modifiers] = defineModel() +// ^? Record<'trim' | 'uppercase', true | undefined> ``` ## defineExpose() {#defineexpose} diff --git a/src/guide/components/v-model.md b/src/guide/components/v-model.md index 91a648c426..9a28773a7b 100644 --- a/src/guide/components/v-model.md +++ b/src/guide/components/v-model.md @@ -1,7 +1,93 @@ # Component v-model {#component-v-model} +## Basic Usage + `v-model` can be used on a component to implement a two-way binding. +
+ +Starting in Vue 3.4, the recommended approach to achieve this is using the [`defineModel()`](/api/sfc-script-setup#definemodel) macro: + +```vue + + + + +``` + +The parent can then bind a value with `v-model`: + +```vue-html + + +``` + +The value returned by `defineModel()` is a ref. It can be accessed and mutated like any other ref, except that it acts as a two-way binding between a parent value and a local one: + +- Its `.value` is synced with the value bound by the parent `v-model`; +- When it is mutated by the child, it causes the parent bound value to be updated as well. + +This means you can also bind this ref to a native input element with `v-model`, making it straightforward to wrap native input elements while providing the same `v-model` usage: + +```vue + + + +``` + +[Playground Example](https://play.vuejs.org/#eNqFUtFKwzAU/ZWYl06YLbK30Q10DFSYigq+5KW0t11mmoQknZPSf/cm3eqEsT0l555zuefmpKV3WsfbBuiUpjY3XDtiwTV6ziSvtTKOLNZcFKQ0qiZRnATkG6JB0BIDJen2kp5iMlfSOlLbisw8P4oeQAhFPpURxVV0zWSa9PNwEgIHtRaZA0SEpOvbeduG5q5LE0Sh2jvZ3tSqADFjFHlGSYJkmhz10zF1FseXvIo3VklcrfX9jOaq1lyAedGOoz1GpyQwnsvQ3fdTqDnTwPhQz9eQf52ob+zO1xh9NWDBbIHRgXOZqcD19PL9GXZ4H0h03whUnyHfwCrReI+97L6RBdo+0gW3j+H9uaw+7HLnQNrDUt6oV3ZBzyhmsjiz+p/dSTwJfUx2+IpD1ic+xz5enwQGXEDJJaw8Gl2I1upMzlc/hEvdOBR6SNKAjqP1J6P/o6XdL11L5h4=) + +### Under the Hood + +`defineModel` is a convenience macro. The compiler expands it to the following: + +- A prop named `modelValue`, which the local ref's value is synced with; +- An event named `update:modelValue`, which is emitted when the local ref's value is mutated. + +This is how you would implement the same child component shown above prior to 3.4: + +```vue + + + +``` + +As you can see, it is quite a bit more verbose. However, it is helpful to understand what is happening under the hood. + +Because `defineModel` declares a prop, you can therefore declare the underlying prop's options by passing it to `defineModel`: + +```js +// making the v-model required +const model = defineModel({ required: true }) + +// providing a default value +const model = defineModel({ default: 0 }) +``` + +
+ +
+ First let's revisit how `v-model` is used on a native element: ```vue-html @@ -33,8 +119,6 @@ For this to actually work though, the `` component must do two thin Here's that in action: -
- ```vue - - -``` - -
- Now `v-model` should work perfectly with this component: ```vue-html ``` -
- [Try it in the Playground](https://play.vuejs.org/#eNqFkctqwzAQRX9lEAEn4Np744aWrvoD3URdiHiSGvRCHpmC8b93JDfGKYGCkJjXvTrSJF69r8aIohHtcA69p6O0vfEuELzFgZx5tz4SXIIzUFT1JpfGCmmlxe/c3uFFRU0wSQtwdqxh0dLQwHSnNJep3ilS+8PSCxCQYrC3CMDgMKgrNlB8odaOXVJ2TgdvvNp6vSwHhMZrRcgRQLs1G5+M61A/S/ErKQXUR5immwXMWW1VEKX4g3j3Mo9QfXCeKU9FtvpQmp/lM0Oi6RP/qYieebHZNvyL0acLLODNmGYSxCogxVJ6yW1c2iWz/QOnEnY48kdUpMIVGSllD8t8zVZb+PkHqPG4iw==) -
-
- -[Try it in the Playground](https://play.vuejs.org/#eNp9j81qwzAQhF9lEQE7kNp344SW0kNvPfVS9WDidSrQH9LKF+N37yoOxoSQm7QzO9/sJN68r8aEohFtPAflCSJS8idplfEuEEwQcIAZhuAMFGwtVuk9RXLm0/pEN7mqN7Ocy2YAac/ORgKDMXYXhGOOLIs/1NoVe2nbekEzlD+ExuuOkH8A7ZYxvhjXoz5KcUuSAuoTTNOaPM85bU0QB3HX58GdPQ7K4ldwPpY/xZXw3Wmu/svVFvHDKMpi8j3HNneeZ/VVBucXQDPmjVx+XZdikV6vNpZ2yKTyAecAOxzRUkVduCCfkqf7Zb9m1Pbo+R9ZkqZn) - -
- Another way of implementing `v-model` within this component is to use a writable `computed` property with both a getter and a setter. The `get` method should return the `modelValue` property and the `set` method should emit the corresponding event: -
- ```vue ``` -
- -## `v-model` arguments {#v-model-arguments} +[Try it in the Playground](https://play.vuejs.org/#eNqFkl9PwjAUxb9K05dhglsMb2SQqOFBE9Soj31Zxh0Uu7bpHxxZ9t29LWOiQXzaes7p2a+9a+mt1unOA53S3JaGa0csOK/nTPJaK+NISwxUpCOVUTVJMJoM1nJ/r/BNgnS9nWYnWujFMCFMlkpaRxx3AsgsFI6S3XWtViBIYda+Dg3QFLUWkFwxmWcHFqTAhQPUCwe4IiTf3Mzbtq/qujzDddRPYfruaUzNGI1PRkmG0Twb+uiY/sI9cw0/0VdQcQnL0D5KovgfL5fa4/69jiDQOOTo+S6SOYtfrvg63VolkauNN0lLxOUCzLN2HMkYnZLoBK8QQn0+Rs0ZD+OjXm6g/Dijb20TNEZfDFgwOwQZPIdzAWQN9uLtKXIPJtL7gH3BfAWrhA+Mh9idlyvEPslF2of4J3G5freLxoG0x0MF0JDsYp5RHE6Y1F9H/8adpJO4j8mOdl/Hw/nf) -By default, `v-model` on a component uses `modelValue` as the prop and `update:modelValue` as the event. We can modify these names passing an argument to `v-model`: +If prop options are also needed, they should be passed after the model name: -```vue-html - +```js +const title = defineModel('title', { required: true }) ``` -In this case, the child component should expect a `title` prop and emit an `update:title` event to update the parent value: - -
+
+Pre 3.4 Usage ```vue @@ -175,9 +224,12 @@ defineEmits(['update:title']) [Try it in the Playground](https://play.vuejs.org/#eNp9kE1rwzAMhv+KMIW00DXsGtKyMXYc7D7vEBplM8QfOHJoCfnvk+1QsjJ2svVKevRKk3h27jAGFJWoh7NXjmBACu4kjdLOeoIJPHYwQ+ethoJLi1vq7fpi+WfQ0JI+lCstcrkYQJqzNQMBKeoRjhG4LcYHbVvsofFfQUcCXhrteix20tRl9sIuOCBkvSHkCKD+fjxN04Ka57rkOOlrMwu7SlVHKdIrBZRcWpc3ntiLO7t/nKHFThl899YN248ikYpP9pj1V60o6sG1TMwDU/q/FZRxgeIPgK4uGcQLSZGlamz6sHKd1afUxOoGeeT298A9bHCMKxBfE3mTSNjl1vud5x8qNa76) +
+In this case, instead of the default `modelValue` prop and `update:modelValue` event, the child component should expect a `title` prop and emit an `update:title` event to update the parent value: + ```vue + + +``` + +[Try it in the Playground](https://play.vuejs.org/#eNqFkstuwjAQRX/F8iZUAqKKHQpIfbAoUmnVx86bKEzANLEt26FUkf+9Y4MDSAg2UWbu9fjckVv6oNRw2wAd08wUmitLDNhGTZngtZLakpZoKIkjpZY1SdCadNK3Ab3IazhowzQ2/ES0MVFIYSwpucbvxA/qJXO5FsldlKr8qDxL8EKW7kEQAQsLtapyC1gRkq3vp217mOccwf8wwLksRSlYIoMvCNkOarmEahyODAT2J4yGgtFzhx8UDf5/r6c4NEs7CNqnpxkvbO0kcVjNhCyh5AJe/SW9pBPOV3DJGvu3dsKFaiyxf8qTW9gheQwVs4Z90BDm5oF47cF/Ht4aZC75argxUmD61g9ktJC14hXoN2U5ZmJ0TILitbyq5O889KxuoB/7xRqKnwv9jdn5HqPvGnDVWwTpNJvrFSCul2efi4DeiRigqdB9RfwAI6vGM+5tj41YIvaJL9C+hOfNxerLzHYWhImhPKh3uuBnFJ/A05XoR9zRcBTOMeGo+wcs+yse) + +
+Pre 3.4 Usage + ```vue ``` -
-
+To conditionally adjust how the value should be read / written based on modifiers, we can pass `get` and `set` options to `defineModel()`. These two options receive the value on get / set of the model ref and should return a transformed value. This is how we can use the `set` option to implement the `capitalize` modifier: -```vue{11} - ``` -
- -Notice the component's `modelModifiers` prop contains `capitalize` and its value is `true` - due to it being set on the `v-model` binding `v-model.capitalize="myText"`. - -Now that we have our prop set up, we can check the `modelModifiers` object keys and write a handler to change the emitted value. In the code below we will capitalize the string whenever the `` element fires an `input` event. +[Try it in the Playground](https://play.vuejs.org/#eNp9UsFu2zAM/RVClzhY5mzoLUgHdEUPG9Bt2LLTtIPh0Ik6WxIkyosb5N9LybFrFG1OkvgeyccnHsWNtXkbUKzE2pdOWQKPFOwnqVVjjSM4gsMKTlA508CMqbMRuu9uDd80ajrD+XISi3WZDCB1abQnaLoNHgiuY8VsNptLvV72TbkdPwgbWxeE/ALY7JUHpW0gKAurqKjVI3rAFl1He6V30JkA3AbdKvLXUzXt+8Zssc6fM6+l6NtLAUtusF6O3cRCvFB9yY2SiYFw+8KSYcY/qfEC+FCVQuf/8rxbrJTG+4hkxyiWq2ZtUQecQ3oDqAqyMWeieyQAu0bBaUh5ebkv3A1lH+Y5md/WorstPGZzeHfGfa1KzD6yxzH11B/TCjHC4dPlX1j3P0CdjQ5S79/Z3WhpPF91lDz7Uald/uCNZj/TFFJE91SN7rslxX5JsRrmk6Koa/P/a4qRC7gY4uUey3+vxB/8Icak+OHQo2tRihGjwu2QtUb47te3pHsEWXWomX0B/Ine1CFq7Gmfg96y7Akvqf2StoKXcePvDoTaD0NFocnhxJeClyRu2FujP8u9yq+GnxGnJxSEO+M=) -
+
+Pre 3.4 Usage ```vue{11-13} + + +``` + +Notice the component's `modelModifiers` prop contains `capitalize` and its value is `true` - due to it being set on the `v-model` binding `v-model.capitalize="myText"`. + +Now that we have our prop set up, we can check the `modelModifiers` object keys and write a handler to change the emitted value. In the code below we will capitalize the string whenever the `` element fires an `input` event. + ```vue{13-15} +``` + +
+Pre 3.4 Usage + ```vue{5,6,10,11} ``` +