Skip to content

Commit 0997235

Browse files
committed
chore: Merge branch 'master' into test-dom-class
2 parents e5eb83d + 159d58f commit 0997235

File tree

10 files changed

+229
-66
lines changed

10 files changed

+229
-66
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { parse } from '../src'
2+
import { mockWarn } from '@vue/runtime-test'
3+
4+
describe('compiler:sfc', () => {
5+
mockWarn()
6+
describe('error', () => {
7+
test('should only allow single template element', () => {
8+
parse(`<template><div/></template><template><div/></template>`)
9+
expect(
10+
`Single file component can contain only one template element`
11+
).toHaveBeenWarned()
12+
})
13+
14+
test('should only allow single script element', () => {
15+
parse(`<script>console.log(1)</script><script>console.log(1)</script>`)
16+
expect(
17+
`Single file component can contain only one script element`
18+
).toHaveBeenWarned()
19+
})
20+
})
21+
})

packages/compiler-sfc/src/parse.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SourceLocation
88
} from '@vue/compiler-core'
99
import { RawSourceMap } from 'source-map'
10+
import { generateCodeFrame } from '@vue/shared'
1011

1112
export interface SFCParseOptions {
1213
needMap?: boolean
@@ -78,14 +79,14 @@ export function parse(
7879
if (!sfc.template) {
7980
sfc.template = createBlock(node) as SFCTemplateBlock
8081
} else {
81-
// TODO warn duplicate template
82+
warnDuplicateBlock(source, filename, node)
8283
}
8384
break
8485
case 'script':
8586
if (!sfc.script) {
8687
sfc.script = createBlock(node) as SFCScriptBlock
8788
} else {
88-
// TODO warn duplicate script
89+
warnDuplicateBlock(source, filename, node)
8990
}
9091
break
9192
case 'style':
@@ -105,6 +106,24 @@ export function parse(
105106
return sfc
106107
}
107108

109+
function warnDuplicateBlock(
110+
source: string,
111+
filename: string,
112+
node: ElementNode
113+
) {
114+
const codeFrame = generateCodeFrame(
115+
source,
116+
node.loc.start.offset,
117+
node.loc.end.offset
118+
)
119+
const location = `${filename}:${node.loc.start.line}:${node.loc.start.column}`
120+
console.warn(
121+
`Single file component can contain only one ${
122+
node.tag
123+
} element (${location}):\n\n${codeFrame}`
124+
)
125+
}
126+
108127
function createBlock(node: ElementNode): SFCBlock {
109128
const type = node.tag
110129
const text = node.children[0] as TextNode

packages/runtime-core/src/component.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ export function setupStatefulComponent(
295295
`does not support it yet.`
296296
)
297297
}
298-
return
299298
} else {
300299
handleSetupResult(instance, setupResult, parentSuspense)
301300
}

packages/runtime-core/src/scheduler.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const p = Promise.resolve()
88
let isFlushing = false
99
let isFlushPending = false
1010

11+
const RECURSION_LIMIT = 100
12+
type CountMap = Map<Function, number>
13+
1114
export function nextTick(fn?: () => void): Promise<void> {
1215
return fn ? p.then(fn) : p
1316
}
@@ -37,51 +40,57 @@ function queueFlush() {
3740

3841
const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)]
3942

40-
export function flushPostFlushCbs() {
43+
export function flushPostFlushCbs(seen?: CountMap) {
4144
if (postFlushCbs.length) {
4245
const cbs = dedupe(postFlushCbs)
4346
postFlushCbs.length = 0
47+
if (__DEV__) {
48+
seen = seen || new Map()
49+
}
4450
for (let i = 0; i < cbs.length; i++) {
51+
if (__DEV__) {
52+
checkRecursiveUpdates(seen!, cbs[i])
53+
}
4554
cbs[i]()
4655
}
4756
}
4857
}
4958

50-
const RECURSION_LIMIT = 100
51-
type JobCountMap = Map<Function, number>
52-
53-
function flushJobs(seenJobs?: JobCountMap) {
59+
function flushJobs(seen?: CountMap) {
5460
isFlushPending = false
5561
isFlushing = true
5662
let job
5763
if (__DEV__) {
58-
seenJobs = seenJobs || new Map()
64+
seen = seen || new Map()
5965
}
6066
while ((job = queue.shift())) {
6167
if (__DEV__) {
62-
const seen = seenJobs!
63-
if (!seen.has(job)) {
64-
seen.set(job, 1)
65-
} else {
66-
const count = seen.get(job)!
67-
if (count > RECURSION_LIMIT) {
68-
throw new Error(
69-
'Maximum recursive updates exceeded. ' +
70-
"You may have code that is mutating state in your component's " +
71-
'render function or updated hook.'
72-
)
73-
} else {
74-
seen.set(job, count + 1)
75-
}
76-
}
68+
checkRecursiveUpdates(seen!, job)
7769
}
7870
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
7971
}
80-
flushPostFlushCbs()
72+
flushPostFlushCbs(seen)
8173
isFlushing = false
8274
// some postFlushCb queued jobs!
8375
// keep flushing until it drains.
8476
if (queue.length || postFlushCbs.length) {
85-
flushJobs(seenJobs)
77+
flushJobs(seen)
78+
}
79+
}
80+
81+
function checkRecursiveUpdates(seen: CountMap, fn: Function) {
82+
if (!seen.has(fn)) {
83+
seen.set(fn, 1)
84+
} else {
85+
const count = seen.get(fn)!
86+
if (count > RECURSION_LIMIT) {
87+
throw new Error(
88+
'Maximum recursive updates exceeded. ' +
89+
"You may have code that is mutating state in your component's " +
90+
'render function or updated hook or watcher source function.'
91+
)
92+
} else {
93+
seen.set(fn, count + 1)
94+
}
8695
}
8796
}

packages/runtime-dom/__tests__/directives/vModel.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,101 @@ describe('vModel', () => {
189189
data.value = false
190190
await nextTick()
191191
expect(input.checked).toEqual(false)
192+
193+
data.value = true
194+
await nextTick()
195+
expect(input.checked).toEqual(true)
196+
197+
input.checked = false
198+
triggerEvent('change', input)
199+
await nextTick()
200+
expect(data.value).toEqual(false)
201+
})
202+
203+
it('should work with checkbox and true-value/false-value', async () => {
204+
const component = createComponent({
205+
data() {
206+
return { value: null }
207+
},
208+
render() {
209+
return [
210+
withVModel(
211+
h('input', {
212+
type: 'checkbox',
213+
'true-value': 'yes',
214+
'false-value': 'no',
215+
'onUpdate:modelValue': setValue.bind(this)
216+
}),
217+
this.value
218+
)
219+
]
220+
}
221+
})
222+
app.mount(component, root)
223+
224+
const input = root.querySelector('input')
225+
const data = root._vnode.component.data
226+
227+
input.checked = true
228+
triggerEvent('change', input)
229+
await nextTick()
230+
expect(data.value).toEqual('yes')
231+
232+
data.value = 'no'
233+
await nextTick()
234+
expect(input.checked).toEqual(false)
235+
236+
data.value = 'yes'
237+
await nextTick()
238+
expect(input.checked).toEqual(true)
239+
240+
input.checked = false
241+
triggerEvent('change', input)
242+
await nextTick()
243+
expect(data.value).toEqual('no')
244+
})
245+
246+
it('should work with checkbox and true-value/false-value with object values', async () => {
247+
const component = createComponent({
248+
data() {
249+
return { value: null }
250+
},
251+
render() {
252+
return [
253+
withVModel(
254+
h('input', {
255+
type: 'checkbox',
256+
'true-value': { yes: 'yes' },
257+
'false-value': { no: 'no' },
258+
'onUpdate:modelValue': setValue.bind(this)
259+
}),
260+
this.value
261+
)
262+
]
263+
}
264+
})
265+
app.mount(component, root)
266+
267+
const input = root.querySelector('input')
268+
const data = root._vnode.component.data
269+
270+
input.checked = true
271+
triggerEvent('change', input)
272+
await nextTick()
273+
expect(data.value).toEqual({ yes: 'yes' })
274+
275+
data.value = { no: 'no' }
276+
await nextTick()
277+
expect(input.checked).toEqual(false)
278+
279+
data.value = { yes: 'yes' }
280+
await nextTick()
281+
expect(input.checked).toEqual(true)
282+
283+
input.checked = false
284+
triggerEvent('change', input)
285+
await nextTick()
286+
expect(data.value).toEqual({ no: 'no' })
192287
})
193288

194289
it(`should support array as a checkbox model`, async () => {

packages/runtime-dom/src/directives/vModel.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
101101
assign(filtered)
102102
}
103103
} else {
104-
assign(checked)
104+
assign(getCheckboxValue(el, checked))
105105
}
106106
})
107107
},
@@ -119,7 +119,7 @@ function setChecked(
119119
if (isArray(value)) {
120120
el.checked = looseIndexOf(value, vnode.props!.value) > -1
121121
} else if (value !== oldValue) {
122-
el.checked = !!value
122+
el.checked = looseEqual(value, getCheckboxValue(el, true))
123123
}
124124
}
125125

@@ -228,6 +228,15 @@ function getValue(el: HTMLOptionElement | HTMLInputElement) {
228228
return '_value' in el ? (el as any)._value : el.value
229229
}
230230

231+
// retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
232+
function getCheckboxValue(
233+
el: HTMLInputElement & { _trueValue?: any; _falseValue?: any },
234+
checked: boolean
235+
) {
236+
const key = checked ? '_trueValue' : '_falseValue'
237+
return key in el ? el[key] : checked
238+
}
239+
231240
export const vModelDynamic: ObjectDirective<
232241
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
233242
> = {

packages/runtime-dom/src/modules/props.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ export function patchDOMProp(
1212
) {
1313
if ((key === 'innerHTML' || key === 'textContent') && prevChildren != null) {
1414
unmountChildren(prevChildren, parentComponent, parentSuspense)
15+
el[key] = value == null ? '' : value
16+
return
1517
}
1618
if (key === 'value' && el.tagName !== 'PROGRESS') {
1719
// store value as _value as well since
1820
// non-string values will be stringified.
1921
el._value = value
22+
el.value = value == null ? '' : value
23+
return
2024
}
2125
if (value === '' && typeof el[key] === 'boolean') {
2226
// e.g. <select multiple> compiles to { multiple: '' }

packages/runtime-dom/src/patchProp.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { patchDOMProp } from './modules/props'
55
import { patchEvent } from './modules/events'
66
import { isOn } from '@vue/shared'
77
import {
8-
VNode,
98
ComponentInternalInstance,
10-
SuspenseBoundary
9+
SuspenseBoundary,
10+
VNode
1111
} from '@vue/runtime-core'
1212

1313
export function patchProp(
@@ -53,6 +53,15 @@ export function patchProp(
5353
unmountChildren
5454
)
5555
} else {
56+
// special case for <input v-model type="checkbox"> with
57+
// :true-value & :false-value
58+
// store value as dom properties since non-string values will be
59+
// stringified.
60+
if (key === 'true-value') {
61+
;(el as any)._trueValue = nextValue
62+
} else if (key === 'false-value') {
63+
;(el as any)._falseValue = nextValue
64+
}
5665
patchAttr(el, key, nextValue)
5766
}
5867
break

packages/shared/src/codeframe.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export function generateCodeFrame(
2020
if (j === i) {
2121
// push underline
2222
const pad = start - (count - lineLength) + 1
23-
const length = end > count ? lineLength - pad : end - start
23+
const length = Math.max(
24+
0,
25+
end > count ? lineLength - pad : end - start
26+
)
2427
res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length))
2528
} else if (j > i) {
2629
if (end > count) {

0 commit comments

Comments
 (0)