Skip to content

Commit 45c9d54

Browse files
committed
feat(compiler): allow flexible spacing in v-for
1 parent 2c0414f commit 45c9d54

File tree

5 files changed

+154
-16
lines changed

5 files changed

+154
-16
lines changed

packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap

+14
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ return function render(_ctx, _cache) {
4545
}"
4646
`;
4747

48+
exports[`compiler: v-for > codegen > no whitespace around (in|of) with basic v-for 1`] = `
49+
"const _Vue = Vue
50+
51+
return function render(_ctx, _cache) {
52+
with (_ctx) {
53+
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
54+
55+
return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList([items], (item) => {
56+
return (_openBlock(), _createElementBlock(\\"span\\"))
57+
}), 256 /* UNKEYED_FRAGMENT */))
58+
}
59+
}"
60+
`;
61+
4862
exports[`compiler: v-for > codegen > skipped key 1`] = `
4963
"const _Vue = Vue
5064

packages/compiler-core/__tests__/transforms/vFor.spec.ts

+118
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,43 @@ describe('compiler: v-for', () => {
202202
expect(forNode.valueAlias).toBeUndefined()
203203
expect((forNode.source as SimpleExpressionNode).content).toBe('items')
204204
})
205+
206+
test('no whitespace around (in|of) with simple expression', () => {
207+
const { node: forNode } = parseWithForTransform(
208+
'<span v-for="(item)in[items]" />'
209+
)
210+
211+
expect(forNode.keyAlias).toBeUndefined()
212+
expect(forNode.objectIndexAlias).toBeUndefined()
213+
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe('item')
214+
expect((forNode.source as SimpleExpressionNode).content).toBe('[items]')
215+
})
216+
217+
test('no whitespace around (in|of) with object de-structured value', () => {
218+
const { node: forNode } = parseWithForTransform(
219+
'<span v-for="{ id, value }in[item]" />'
220+
)
221+
222+
expect(forNode.keyAlias).toBeUndefined()
223+
expect(forNode.objectIndexAlias).toBeUndefined()
224+
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
225+
'{ id, value }'
226+
)
227+
expect((forNode.source as SimpleExpressionNode).content).toBe('[item]')
228+
})
229+
230+
test('no whitespace around (in|of) with array de-structured value', () => {
231+
const { node: forNode } = parseWithForTransform(
232+
'<span v-for="[ id ]in[item]" />'
233+
)
234+
235+
expect(forNode.keyAlias).toBeUndefined()
236+
expect(forNode.objectIndexAlias).toBeUndefined()
237+
expect((forNode.valueAlias as SimpleExpressionNode).content).toBe(
238+
'[ id ]'
239+
)
240+
expect((forNode.source as SimpleExpressionNode).content).toBe('[item]')
241+
})
205242
})
206243

207244
describe('errors', () => {
@@ -241,6 +278,18 @@ describe('compiler: v-for', () => {
241278
)
242279
})
243280

281+
test('invalid expression containing (in|of)', () => {
282+
const onError = vi.fn()
283+
parseWithForTransform('<span v-for="fooinbar" />', { onError })
284+
285+
expect(onError).toHaveBeenCalledTimes(1)
286+
expect(onError).toHaveBeenCalledWith(
287+
expect.objectContaining({
288+
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION
289+
})
290+
)
291+
})
292+
244293
test('missing source', () => {
245294
const onError = vi.fn()
246295
parseWithForTransform('<span v-for="item in" />', { onError })
@@ -265,6 +314,18 @@ describe('compiler: v-for', () => {
265314
)
266315
})
267316

317+
test('missing source and value', () => {
318+
const onError = vi.fn()
319+
parseWithForTransform('<span v-for=" in " />', { onError })
320+
321+
expect(onError).toHaveBeenCalledTimes(1)
322+
expect(onError).toHaveBeenCalledWith(
323+
expect.objectContaining({
324+
code: ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION
325+
})
326+
)
327+
})
328+
268329
test('<template v-for> key placement', () => {
269330
const onError = vi.fn()
270331
parseWithForTransform(
@@ -409,6 +470,48 @@ describe('compiler: v-for', () => {
409470
)
410471
})
411472

473+
test('no whitespace around (in|of) with bracketed value, key, index', () => {
474+
const source = '<span v-for="( item, key, index )in[items]" />'
475+
const { node: forNode } = parseWithForTransform(source)
476+
477+
const itemOffset = source.indexOf('item')
478+
const value = forNode.valueAlias as SimpleExpressionNode
479+
expect(value.content).toBe('item')
480+
expect(value.loc.start.offset).toBe(itemOffset)
481+
expect(value.loc.start.line).toBe(1)
482+
expect(value.loc.start.column).toBe(itemOffset + 1)
483+
expect(value.loc.end.line).toBe(1)
484+
expect(value.loc.end.column).toBe(itemOffset + 1 + `item`.length)
485+
486+
const keyOffset = source.indexOf('key')
487+
const key = forNode.keyAlias as SimpleExpressionNode
488+
expect(key.content).toBe('key')
489+
expect(key.loc.start.offset).toBe(keyOffset)
490+
expect(key.loc.start.line).toBe(1)
491+
expect(key.loc.start.column).toBe(keyOffset + 1)
492+
expect(key.loc.end.line).toBe(1)
493+
expect(key.loc.end.column).toBe(keyOffset + 1 + `key`.length)
494+
495+
const indexOffset = source.indexOf('index')
496+
const index = forNode.objectIndexAlias as SimpleExpressionNode
497+
expect(index.content).toBe('index')
498+
expect(index.loc.start.offset).toBe(indexOffset)
499+
expect(index.loc.start.line).toBe(1)
500+
expect(index.loc.start.column).toBe(indexOffset + 1)
501+
expect(index.loc.end.line).toBe(1)
502+
expect(index.loc.end.column).toBe(indexOffset + 1 + `index`.length)
503+
504+
const itemsOffset = source.indexOf('[items]')
505+
expect((forNode.source as SimpleExpressionNode).content).toBe('[items]')
506+
expect(forNode.source.loc.start.offset).toBe(itemsOffset)
507+
expect(forNode.source.loc.start.line).toBe(1)
508+
expect(forNode.source.loc.start.column).toBe(itemsOffset + 1)
509+
expect(forNode.source.loc.end.line).toBe(1)
510+
expect(forNode.source.loc.end.column).toBe(
511+
itemsOffset + 1 + `[items]`.length
512+
)
513+
})
514+
412515
test('skipped key', () => {
413516
const source = '<span v-for="( item,, index ) in items" />'
414517
const { node: forNode } = parseWithForTransform(source)
@@ -717,6 +820,21 @@ describe('compiler: v-for', () => {
717820
expect(generate(root).code).toMatchSnapshot()
718821
})
719822

823+
test('no whitespace around (in|of) with basic v-for', () => {
824+
const {
825+
root,
826+
node: { codegenNode }
827+
} = parseWithForTransform('<span v-for="(item)in[items]" />')
828+
expect(assertSharedCodegen(codegenNode)).toMatchObject({
829+
source: { content: `[items]` },
830+
params: [{ content: `item` }],
831+
innerVNodeCall: {
832+
tag: `"span"`
833+
}
834+
})
835+
expect(generate(root).code).toMatchSnapshot()
836+
})
837+
720838
test('value + key + index', () => {
721839
const {
722840
root,

packages/compiler-core/src/transforms/vFor.ts

+7-11
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import {
3737
isTemplateNode,
3838
isSlotOutlet,
3939
injectProp,
40-
findDir
40+
findDir,
41+
matchForAlias
4142
} from '../utils'
4243
import {
4344
RENDER_LIST,
@@ -308,7 +309,6 @@ export function processFor(
308309
}
309310
}
310311

311-
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
312312
// This regex doesn't cover the case if key or index aliases have destructuring,
313313
// but those do not make sense in the first place, so this works in practice.
314314
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
@@ -327,17 +327,13 @@ export function parseForExpression(
327327
): ForParseResult | undefined {
328328
const loc = input.loc
329329
const exp = input.content
330-
const inMatch = exp.match(forAliasRE)
331-
if (!inMatch) return
332330

333-
const [, LHS, RHS] = inMatch
331+
const inMatch = matchForAlias(exp)
332+
if (!inMatch) return
333+
const { LHS, RHS } = inMatch
334334

335335
const result: ForParseResult = {
336-
source: createAliasExpression(
337-
loc,
338-
RHS.trim(),
339-
exp.indexOf(RHS, LHS.length)
340-
),
336+
source: createAliasExpression(loc, RHS, exp.indexOf(RHS, LHS.length)),
341337
value: undefined,
342338
key: undefined,
343339
index: undefined
@@ -352,7 +348,7 @@ export function parseForExpression(
352348
validateBrowserExpression(result.source as SimpleExpressionNode, context)
353349
}
354350

355-
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
351+
let valueContent = LHS.replace(stripParensRE, '').trim()
356352
const trimmedOffset = LHS.indexOf(valueContent)
357353

358354
const iteratorMatch = valueContent.match(forIteratorRE)

packages/compiler-core/src/utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -519,3 +519,14 @@ export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
519519
return node
520520
}
521521
}
522+
523+
const forAliasRE = /([\s\S]*?[\s\)\}\]]+)(?:in|of)([\s\[]+[\s\S]*)/
524+
export function matchForAlias(exp: string) {
525+
const inMatch = exp.match(forAliasRE)
526+
if (!inMatch) return
527+
528+
const LHS = inMatch[1].trim()
529+
const RHS = inMatch[2].trim()
530+
531+
if (LHS && RHS) return { LHS, RHS }
532+
}

packages/compiler-sfc/src/script/importUsageCheck.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
NodeTypes,
55
SimpleExpressionNode,
66
createRoot,
7+
matchForAlias,
78
parserOptions,
89
transform,
910
walkIdentifiers
@@ -87,20 +88,18 @@ function resolveTemplateUsageCheckString(sfc: SFCDescriptor) {
8788
return code
8889
}
8990

90-
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
91-
9291
function processExp(exp: string, dir?: string): string {
9392
if (/ as\s+\w|<.*>|:/.test(exp)) {
9493
if (dir === 'slot') {
9594
exp = `(${exp})=>{}`
9695
} else if (dir === 'on') {
9796
exp = `()=>{return ${exp}}`
9897
} else if (dir === 'for') {
99-
const inMatch = exp.match(forAliasRE)
98+
const inMatch = matchForAlias(exp)
10099
if (inMatch) {
101-
let [, LHS, RHS] = inMatch
100+
let { LHS, RHS } = inMatch
102101
// #6088
103-
LHS = LHS.trim().replace(/^\(|\)$/g, '')
102+
LHS = LHS.replace(/^\(|\)$/g, '')
104103
return processExp(`(${LHS})=>{}`) + processExp(RHS)
105104
}
106105
}

0 commit comments

Comments
 (0)