Skip to content

Commit 710150e

Browse files
authored
+ Update v-model to support deep binding. (#8)
* + Add util function. * + With argument support. * + Update. * + Update unit testing. * + Update README.md * + Remove unnecessary code. * 0.4.0-0
1 parent 6682674 commit 710150e

9 files changed

+356
-212
lines changed

Diff for: README.md

+32-4
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ const Example = Vue.extend({
390390

391391
#### With modifiers
392392

393+
You can use modifiers to add some extra features:
394+
395+
- `lazy, number, trim`: These are the built-in modifiers from Vue.
396+
- `direct`: See **"About IME"** section below.
397+
393398
```tsx
394399
const Example = Vue.extend({
395400
data: () => ({
@@ -421,16 +426,39 @@ const Example = defineComponent({
421426

422427
#### With argument
423428

429+
Argument of v-model is designed for binding properties.
430+
431+
Due to limitation, binding properties in Vue 2 isn't that kinda convenient:
432+
433+
```tsx
434+
const userRef = ref({
435+
detail: {
436+
address: ''
437+
}
438+
})
439+
440+
// This works in Vue 3 but doesn't work in Vue 2.
441+
<input v-model={userRef.value.detail.address} />
442+
```
443+
444+
We have to use v-model like:
445+
424446
```tsx
425447
const Example = defineComponent({
426448
setup () {
427-
const nameRef = ref('')
428-
const ageRef = ref(0)
449+
const userRef = ref({
450+
username: '',
451+
age: 0,
452+
detail: {
453+
address: ''
454+
}
455+
})
429456

430457
return () => (
431458
<div>
432-
<input v-model={[nameRef, 'value', ['lazy']]}/>
433-
<input v-model={[agRef, 'value', ['number']]}/>
459+
<input v-model={[userRef, 'username', ['lazy']]}/>
460+
<input v-model={[userRef, 'age', ['number']]}/>
461+
<input v-model={[userRef, 'detail.address']}/>
434462
</div>
435463
)
436464
}

Diff for: lib/directives/v-model.ts

+64-70
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { checkIsRefObj, isArray, isString, isUndefined } from '../utils'
1+
import { checkIsRefObj, getValueFromObject, isArray, isString, isUndefined, setValueToObject } from '../utils'
22
import type { Ref } from '@vue/composition-api'
33
import type { VNodeData } from 'vue'
44
import { ConfigType, TagType } from '../type'
55
import { getCurrentInstance } from '../runtime'
66

77
const IME_START_KEY = '__ime_start__'
88

9-
type vModelBinding = string |
10-
Ref<unknown> |
9+
type vModelBinding =
10+
string | Ref<unknown> |
1111
[string | Ref<unknown>] |
1212
[string | Ref<unknown>, string[]] |
1313
[string | Ref<unknown>, string, string[]]
@@ -19,61 +19,83 @@ const dealWithVModel = (
1919
vNodeData: VNodeData,
2020
isHTMLElement: boolean
2121
) => {
22-
let bindingTarget: string | Ref<unknown>
23-
// let argument: string | undefined
22+
let bindingKeyPathOrRef: string | Ref<unknown>
23+
let argument: string | undefined
2424
let modifiers: string[] = []
2525

2626
if (isString(bindingExpression) || checkIsRefObj(bindingExpression)) {
27-
bindingTarget = bindingExpression
27+
bindingKeyPathOrRef = bindingExpression
2828
} else if (isArray(bindingExpression)) {
29-
bindingTarget = bindingExpression[0]
29+
bindingKeyPathOrRef = bindingExpression[0]
3030
if (bindingExpression.length === 2) {
31-
modifiers = bindingExpression[1]
31+
if (isArray(bindingExpression[1])) {
32+
modifiers = bindingExpression[1]
33+
} else if (isString(bindingExpression[1])) {
34+
argument = bindingExpression[1]
35+
}
3236
} else if (bindingExpression.length === 3) {
33-
// argument = bindingExpression[1]
37+
argument = bindingExpression[1]
3438
modifiers = bindingExpression[2]
3539
}
3640
}
3741

3842
const instance = getCurrentInstance()
43+
const getBindingValue = () => {
44+
// v-model='a.b.c'
45+
if (isString(bindingKeyPathOrRef)) {
46+
return getValueFromObject(instance, bindingKeyPathOrRef)
47+
}
48+
49+
// v-model={[xxRef, 'a.b.c']}
50+
if (argument) {
51+
return getValueFromObject(bindingKeyPathOrRef.value, argument)
52+
}
53+
54+
// v-model={xxRef}
55+
return bindingKeyPathOrRef.value
56+
}
3957

58+
const emitValue = (payload: unknown) => {
59+
if (isString(bindingKeyPathOrRef)) {
60+
setValueToObject(instance, bindingKeyPathOrRef, payload)
61+
// Vue.set(instance, bindingKeyPathOrRef, payload)
62+
} else if (argument) {
63+
setValueToObject(bindingKeyPathOrRef.value, argument, payload)
64+
// Vue.set(bindingKeyPathOrRef.value as any, argument, payload)
65+
} else {
66+
bindingKeyPathOrRef.value = payload
67+
}
68+
}
69+
70+
// <select />.
4071
if (tag === 'select') {
41-
vNodeData.domProps.value = isString(bindingTarget)
42-
? instance[bindingTarget]
43-
: bindingTarget.value
72+
vNodeData.domProps.value = getBindingValue()
4473
vNodeData.on.change = (event: Event) => {
4574
const target = event.target as HTMLSelectElement
46-
if (isString(bindingTarget)) {
47-
instance[bindingTarget] = target.value
48-
} else {
49-
bindingTarget.value = target.value
50-
}
75+
const payload = target.value
76+
emitValue(payload)
5177
}
5278
return
5379
}
5480

81+
// <input radio|checkbox />.
5582
if (tag === 'input') {
5683
const inputType = config.type // 'file', 'text', 'number', .ect.
5784

5885
// Skip unsupported input.
59-
const isSkippedInput = /button|file|submit|reset/.test(inputType)
60-
if (isSkippedInput) {
86+
const isSkippedType = /button|file|submit|reset/.test(inputType)
87+
if (isSkippedType) {
6188
return
6289
}
6390

6491
// Radio.
6592
const isRadioInput = inputType === 'radio'
6693
if (isRadioInput) {
67-
vNodeData.domProps.checked = isString(bindingTarget)
68-
? instance[bindingTarget] === config.value
69-
: bindingTarget.value === config.value
94+
vNodeData.domProps.checked = getBindingValue() === config.value
7095
vNodeData.on.change = (event: Event) => {
7196
const target = event.target as HTMLInputElement
72-
if (isString(bindingTarget)) {
73-
instance[bindingTarget] = target.value
74-
} else {
75-
bindingTarget.value = target.value
76-
}
97+
const payload = target.value
98+
emitValue(payload)
7799
}
78100
return
79101
}
@@ -100,70 +122,49 @@ const dealWithVModel = (
100122
const target = event.target as HTMLInputElement
101123
const isValueSpecified = !isUndefined(config.value)
102124
const newCheckStatus = target.checked
103-
if (isString(bindingTarget)) {
104-
instance[bindingTarget] = isValueSpecified
105-
? target.value
106-
: newCheckStatus ? checkboxDefaultValue : undefined
107-
} else {
108-
bindingTarget.value = isValueSpecified
109-
? target.value
110-
: newCheckStatus ? checkboxDefaultValue : undefined
111-
}
125+
const payload = isValueSpecified
126+
? target.value
127+
: newCheckStatus ? checkboxDefaultValue : undefined
128+
emitValue(payload)
112129
}
113130
}
114131

115-
let bindingValue: unknown
116-
if (isString(bindingTarget)) {
117-
bindingValue = instance[bindingTarget]
118-
} else {
119-
bindingValue = bindingTarget.value
120-
}
132+
const bindingValue: unknown = getBindingValue()
121133

122134
if (isArray(bindingValue)) {
123-
// Array binding.
124135
arrayBindingExec(bindingValue)
125136
} else {
126-
// Basic binding.
127137
basicBindingExec(bindingValue)
128138
}
129139
return
130140
}
131141
}
132142

133-
// Others are treated as text fields.
143+
// <input /> | <textarea />.
134144
if (tag === 'input' || tag === 'textarea') {
135145
const isDirectInput = modifiers.includes('direct')
136146
const isLazyInput = modifiers.includes('lazy')
137-
const emitValue = (value: string) => {
147+
const emit = (value: string) => {
138148
const hasNumberModifier = modifiers.includes('number')
139149
const hasTrimModifier = modifiers.includes('trim')
140-
const newValue = hasNumberModifier
150+
const payload = hasNumberModifier
141151
? parseFloat(value)
142152
: hasTrimModifier
143153
? value.trim()
144154
: value
145155

146-
if (isString(bindingTarget)) {
147-
instance[bindingTarget] = newValue
148-
} else {
149-
bindingTarget.value = newValue
150-
}
156+
emitValue(payload)
151157
}
152158

153159
vNodeData.attrs[IME_START_KEY] = false
154-
vNodeData.domProps.value = isString(bindingTarget)
155-
? instance[bindingTarget]
156-
: bindingTarget.value
160+
vNodeData.domProps.value = getBindingValue()
157161
vNodeData.on.input = (event: Event) => {
158-
if (
159-
isLazyInput ||
160-
(!isDirectInput && vNodeData.attrs[IME_START_KEY])
161-
) {
162+
if (isLazyInput || (!isDirectInput && vNodeData.attrs[IME_START_KEY])) {
162163
return
163164
}
164165

165166
const target = event.target as HTMLInputElement
166-
emitValue(target.value)
167+
emit(target.value)
167168
}
168169

169170
if (isLazyInput) {
@@ -190,16 +191,9 @@ const dealWithVModel = (
190191

191192
// v-model on component.
192193
if (!isHTMLElement) {
193-
const instance = getCurrentInstance()
194-
vNodeData.props.value = isString(bindingTarget)
195-
? instance[bindingTarget]
196-
: bindingTarget.value
197-
vNodeData.on.input = (value) => {
198-
if (isString(bindingTarget)) {
199-
instance[bindingTarget] = value
200-
} else {
201-
bindingTarget.value = value
202-
}
194+
vNodeData.props.value = getBindingValue()
195+
vNodeData.on.input = (payload) => {
196+
emitValue(payload)
203197
}
204198
}
205199
}

Diff for: lib/utils.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,39 @@ const removeNativeOn = (key: string) => camelCase(key
6161
.replace(ON_EVENT_REGEXP, '')
6262
)
6363

64+
const getValueFromObject = (target: any, keyPath: string): any => {
65+
const keys = keyPath.split('.')
66+
let value: any
67+
for (let i = 0, length = keys.length; i < length; i++) {
68+
const key = keys[i]
69+
if (i === 0) {
70+
value = target[key]
71+
continue
72+
}
73+
74+
if (typeof value !== 'undefined') {
75+
const currentValue = value[key]
76+
value = currentValue
77+
}
78+
}
79+
return value
80+
}
81+
82+
const setValueToObject = (target: any, keyPath: string, payload: any) => {
83+
const keys = keyPath.split('.')
84+
let lastTarget: any = target
85+
for (let i = 0, length = keys.length; i < length; i++) {
86+
const key = keys[i]
87+
88+
if (i === length - 1) {
89+
lastTarget[key] = payload
90+
return
91+
}
92+
93+
lastTarget = lastTarget[key]
94+
}
95+
}
96+
6497
export {
6598
isArray,
6699
isBoolean,
@@ -86,5 +119,8 @@ export {
86119
checkKeyIsVueDirective,
87120

88121
removeOn,
89-
removeNativeOn
122+
removeNativeOn,
123+
124+
getValueFromObject,
125+
setValueToObject
90126
}

0 commit comments

Comments
 (0)