Skip to content

Commit 0fbad1e

Browse files
committed
feat: warn non-existent v-model keys
1 parent 2431d3d commit 0fbad1e

File tree

6 files changed

+71
-20
lines changed

6 files changed

+71
-20
lines changed

Diff for: src/compiler/directives/model.js

+17-12
Original file line numberDiff line numberDiff line change
@@ -37,39 +37,44 @@ export function genAssignmentCode (
3737
value: string,
3838
assignment: string
3939
): string {
40-
const modelRs = parseModel(value)
41-
if (modelRs.idx === null) {
40+
const res = parseModel(value)
41+
if (res.key === null) {
4242
return `${value}=${assignment}`
4343
} else {
44-
return `$set(${modelRs.exp}, ${modelRs.idx}, ${assignment})`
44+
return `$set(${res.exp}, ${res.key}, ${assignment})`
4545
}
4646
}
4747

4848
/**
49-
* parse directive model to do the array update transform. a[idx] = val => $$a.splice($$idx, 1, val)
49+
* parse directive model to do the array update transform. a[key] = val => $$a.splice($$key, 1, val)
5050
*
5151
* for loop possible cases:
5252
*
5353
* - test
54-
* - test[idx]
55-
* - test[test1[idx]]
56-
* - test["a"][idx]
57-
* - xxx.test[a[a].test1[idx]]
58-
* - test.xxx.a["asa"][test1[idx]]
54+
* - test[key]
55+
* - test[test1[key]]
56+
* - test["a"][key]
57+
* - xxx.test[a[a].test1[key]]
58+
* - test.xxx.a["asa"][test1[key]]
5959
*
6060
*/
6161

6262
let len, str, chr, index, expressionPos, expressionEndPos
6363

64-
export function parseModel (val: string): Object {
64+
type ModelParseResult = {
65+
exp: string,
66+
key: string | null
67+
}
68+
69+
export function parseModel (val: string): ModelParseResult {
6570
str = val
6671
len = str.length
6772
index = expressionPos = expressionEndPos = 0
6873

6974
if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {
7075
return {
7176
exp: val,
72-
idx: null
77+
key: null
7378
}
7479
}
7580

@@ -85,7 +90,7 @@ export function parseModel (val: string): Object {
8590

8691
return {
8792
exp: val.substring(0, expressionPos),
88-
idx: val.substring(expressionPos + 1, expressionEndPos)
93+
key: val.substring(expressionPos + 1, expressionEndPos)
8994
}
9095
}
9196

Diff for: src/core/instance/init.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { initEvents } from './events'
88
import { mark, measure } from '../util/perf'
99
import { initLifecycle, callHook } from './lifecycle'
1010
import { initProvide, initInjections } from './inject'
11-
import { extend, mergeOptions, formatComponentName } from '../util/index'
11+
import { extend, mergeOptions, formatComponentName, checkKeyExistence } from '../util/index'
1212

1313
let uid = 0
1414

@@ -58,6 +58,13 @@ export function initMixin (Vue: Class<Component>) {
5858
initProvide(vm) // resolve provide after data/props
5959
callHook(vm, 'created')
6060

61+
// check v-model binding in parent context
62+
if (process.env.NODE_ENV !== 'production' &&
63+
this.$vnode &&
64+
this.$vnode.data.model) {
65+
checkKeyExistence(this.$parent, this.$vnode.data.model.expression)
66+
}
67+
6168
/* istanbul ignore if */
6269
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
6370
vm._name = formatComponentName(vm, false)

Diff for: src/core/util/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export * from './options'
77
export * from './debug'
88
export * from './props'
99
export * from './error'
10+
export * from './model'
1011
export { defineReactive } from '../observer/index'

Diff for: src/core/util/model.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* @flow */
2+
3+
import { warn } from './debug'
4+
import { parseModel } from 'compiler/directives/model'
5+
6+
export function checkKeyExistence (vm: Component, expression: string) {
7+
const res = parseModel(expression)
8+
// last key segment is dynamic and will be set with `$set`
9+
if (res.key) return
10+
const path = res.exp.split('.')
11+
// root path is already checked against
12+
if (path.length === 1) return
13+
let val = vm
14+
for (let i = 0; i < path.length; i++) {
15+
const key = path[i]
16+
if (!(key in val)) {
17+
warn(
18+
`v-model="${expression}" is bound to a property that does not exist ` +
19+
`and will not be reactive. Declare the property in data to ensure ` +
20+
`reactivity.`,
21+
vm
22+
)
23+
break
24+
} else {
25+
val = val[key]
26+
}
27+
}
28+
}

Diff for: src/platforms/web/runtime/directives/model.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55

66
import { isTextInputType } from 'web/util/element'
77
import { looseEqual, looseIndexOf } from 'shared/util'
8-
import { warn, isAndroid, isIE9, isIE, isEdge } from 'core/util/index'
8+
import {
9+
warn,
10+
isAndroid,
11+
isIE9,
12+
isIE,
13+
isEdge,
14+
checkKeyExistence
15+
} from 'core/util/index'
916

1017
/* istanbul ignore if */
1118
if (isIE9) {
@@ -20,6 +27,9 @@ if (isIE9) {
2027

2128
export default {
2229
inserted (el, binding, vnode) {
30+
if (process.env.NODE_ENV !== 'production') {
31+
checkKeyExistence(vnode.context, binding.expression)
32+
}
2333
if (vnode.tag === 'select') {
2434
setSelected(el, binding, vnode.context)
2535
el._vOptions = [].map.call(el.options, getValue)

Diff for: test/unit/features/directives/model-parse.spec.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,30 @@ describe('model expression parser', () => {
44
it('parse object dot notation', () => {
55
const res = parseModel('a.b.c')
66
expect(res.exp).toBe('a.b.c')
7-
expect(res.idx).toBe(null)
7+
expect(res.key).toBe(null)
88
})
99

1010
it('parse string in brackets', () => {
1111
const res = parseModel('a["b"][c]')
1212
expect(res.exp).toBe('a["b"]')
13-
expect(res.idx).toBe('c')
13+
expect(res.key).toBe('c')
1414
})
1515

1616
it('parse brackets with object dot notation', () => {
1717
const res = parseModel('a["b"][c].xxx')
1818
expect(res.exp).toBe('a["b"][c].xxx')
19-
expect(res.idx).toBe(null)
19+
expect(res.key).toBe(null)
2020
})
2121

2222
it('parse nested brackets', () => {
2323
const res = parseModel('a[i[c]]')
2424
expect(res.exp).toBe('a')
25-
expect(res.idx).toBe('i[c]')
25+
expect(res.key).toBe('i[c]')
2626
})
2727

2828
it('combined', () => {
29-
const res = parseModel('test.xxx.a["asa"][test1[idx]]')
29+
const res = parseModel('test.xxx.a["asa"][test1[key]]')
3030
expect(res.exp).toBe('test.xxx.a["asa"]')
31-
expect(res.idx).toBe('test1[idx]')
31+
expect(res.key).toBe('test1[key]')
3232
})
3333
})

0 commit comments

Comments
 (0)