feat(compiler): support v-for on named slots

This commit is contained in:
Evan You
2019-10-02 23:10:41 -04:00
parent f401ac6b88
commit fc47029ed3
18 changed files with 645 additions and 277 deletions

View File

@@ -14,9 +14,7 @@ return function render() {
_toString(world.burn()),
(_openBlock(), ok
? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: _createBlock(_Fragment, { key: 1 }, [
\\"no\\"
])),
: _createBlock(_Fragment, { key: 1 }, [\\"no\\"])),
(_openBlock(), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
@@ -39,9 +37,7 @@ return function render() {
toString(_ctx.world.burn()),
(openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [
\\"no\\"
])),
: createBlock(Fragment, { key: 1 }, [\\"no\\"])),
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */)
@@ -63,9 +59,7 @@ export default function render() {
_toString(_ctx.world.burn()),
(openBlock(), (_ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [
\\"no\\"
])),
: createBlock(Fragment, { key: 1 }, [\\"no\\"])),
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
return (openBlock(), createBlock(\\"div\\", null, [
createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)

View File

@@ -7,6 +7,7 @@ import {
ElementTypes
} from '../src'
import { CREATE_VNODE } from '../src/runtimeConstants'
import { isString } from '@vue/shared'
const leadingBracketRE = /^\[/
const bracketsRE = /^\[|\]$/g
@@ -26,11 +27,13 @@ export function createObjectMatcher(obj: any) {
content: key.replace(bracketsRE, ''),
isStatic: !leadingBracketRE.test(key)
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: obj[key].replace(bracketsRE, ''),
isStatic: !leadingBracketRE.test(obj[key])
}
value: isString(obj[key])
? {
type: NodeTypes.SIMPLE_EXPRESSION,
content: obj[key].replace(bracketsRE, ''),
isStatic: !leadingBracketRE.test(obj[key])
}
: obj[key]
}))
}
}

View File

@@ -71,9 +71,7 @@ return function render() {
? _createBlock(\\"div\\", { key: 0 })
: orNot
? _createBlock(\\"p\\", { key: 1 })
: _createBlock(_Fragment, { key: 2 }, [
\\"fine\\"
]))
: _createBlock(_Fragment, { key: 2 }, [\\"fine\\"]))
}
}"
`;

View File

@@ -8,14 +8,8 @@ return function render() {
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
[_ctx.one]: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
],
[_ctx.two]: ({ bar }) => [
toString(_ctx.foo),
toString(bar)
]
[_ctx.one]: ({ foo }) => [toString(foo), toString(_ctx.bar)],
[_ctx.two]: ({ bar }) => [toString(_ctx.foo), toString(bar)]
}, 256 /* DYNAMIC_SLOTS */))
}"
`;
@@ -28,10 +22,7 @@ return function render() {
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
]
default: ({ foo }) => [toString(foo), toString(_ctx.bar)]
}))
}"
`;
@@ -51,6 +42,92 @@ return function render() {
}"
`;
exports[`compiler: transform component slots named slot with v-for w/ prefixIdentifiers: true 1`] = `
"const { toString, resolveComponent, renderList, createSlots, createVNode, openBlock, createBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({}, [
renderList(_ctx.list, (name) => {
return {
name: name,
fn: () => [toString(name)]
}
})
]), 256 /* DYNAMIC_SLOTS */))
}"
`;
exports[`compiler: transform component slots named slot with v-if + prefixIdentifiers: true 1`] = `
"const { toString, resolveComponent, createSlots, createVNode, openBlock, createBlock } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({}, [
(_ctx.ok)
? {
name: \\"one\\",
fn: (props) => [toString(props)]
}
: undefined
]), 256 /* DYNAMIC_SLOTS */))
}"
`;
exports[`compiler: transform component slots named slot with v-if + v-else-if + v-else 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({}, [
ok
? {
name: \\"one\\",
fn: () => [\\"foo\\"]
}
: orNot
? {
name: \\"two\\",
fn: (props) => [\\"bar\\"]
}
: {
name: \\"one\\",
fn: () => [\\"baz\\"]
}
]), 256 /* DYNAMIC_SLOTS */))
}
}"
`;
exports[`compiler: transform component slots named slot with v-if 1`] = `
"const _Vue = Vue
return function render() {
with (this) {
const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({}, [
ok
? {
name: \\"one\\",
fn: () => [\\"hello\\"]
}
: undefined
]), 256 /* DYNAMIC_SLOTS */))
}
}"
`;
exports[`compiler: transform component slots named slots 1`] = `
"const { toString, resolveComponent, createVNode, openBlock, createBlock } = Vue
@@ -59,14 +136,8 @@ return function render() {
const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, {
one: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
],
two: ({ bar }) => [
toString(_ctx.foo),
toString(bar)
]
one: ({ foo }) => [toString(foo), toString(_ctx.bar)],
two: ({ bar }) => [toString(_ctx.foo), toString(bar)]
}))
}"
`;
@@ -82,11 +153,7 @@ return function render() {
return (openBlock(), createBlock(_component_Comp, null, {
default: ({ foo }) => [
createVNode(_component_Inner, null, {
default: ({ bar }) => [
toString(foo),
toString(bar),
toString(_ctx.baz)
]
default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)]
}),
toString(foo),
toString(_ctx.bar),

View File

@@ -11,14 +11,20 @@ import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind'
import { transformExpression } from '../../src/transforms/transformExpression'
import { trackSlotScopes } from '../../src/transforms/vSlot'
import {
trackSlotScopes,
trackVForSlotScopes
} from '../../src/transforms/vSlot'
import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeConstants'
import { createObjectMatcher } from '../testUtils'
import { PatchFlags } from '@vue/shared'
function parseWithSlots(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [
...(options.prefixIdentifiers
? [transformExpression, trackSlotScopes]
? [trackVForSlotScopes, transformExpression, trackSlotScopes]
: []),
transformElement
],
@@ -314,118 +320,311 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('error on extraneous children w/ named slots', () => {
const onError = jest.fn()
const source = `<Comp><template #default>foo</template>bar</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('bar')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
loc: {
source: `bar`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 3,
line: 1,
column: index + 4
test('named slot with v-if', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #one v-if="ok">hello</template>
</Comp>`
)
expect(slots).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${CREATE_SLOTS}`,
arguments: [
createObjectMatcher({}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: { content: `ok` },
consequent: createObjectMatcher({
name: `one`,
fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: [{ type: NodeTypes.TEXT, content: `hello` }]
}
}),
alternate: {
content: `undefined`,
isStatic: false
}
}
]
}
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root).code).toMatchSnapshot()
})
test('error on duplicated slot names', () => {
const onError = jest.fn()
const source = `<Comp><template #foo></template><template #foo></template></Comp>`
parseWithSlots(source, { onError })
const index = source.lastIndexOf('#foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 4,
line: 1,
column: index + 5
test('named slot with v-if + prefixIdentifiers: true', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #one="props" v-if="ok">{{ props }}</template>
</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_SLOTS,
arguments: [
createObjectMatcher({}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: { content: `_ctx.ok` },
consequent: createObjectMatcher({
name: `one`,
fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: { content: `props` },
returns: [
{
type: NodeTypes.INTERPOLATION,
content: { content: `props` }
}
]
}
}),
alternate: {
content: `undefined`,
isStatic: false
}
}
]
}
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('error on invalid mixed slot usage', () => {
const onError = jest.fn()
const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
parseWithSlots(source, { onError })
const index = source.lastIndexOf('#foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_MIXED_SLOT_USAGE,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 4,
line: 1,
column: index + 5
test('named slot with v-if + v-else-if + v-else', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #one v-if="ok">foo</template>
<template #two="props" v-else-if="orNot">bar</template>
<template #one v-else>baz</template>
</Comp>`
)
expect(slots).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: `_${CREATE_SLOTS}`,
arguments: [
createObjectMatcher({}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: { content: `ok` },
consequent: createObjectMatcher({
name: `one`,
fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [{ type: NodeTypes.TEXT, content: `foo` }]
}
}),
alternate: {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test: { content: `orNot` },
consequent: createObjectMatcher({
name: `two`,
fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: { content: `props` },
returns: [{ type: NodeTypes.TEXT, content: `bar` }]
}
}),
alternate: createObjectMatcher({
name: `one`,
fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: undefined,
returns: [{ type: NodeTypes.TEXT, content: `baz` }]
}
})
}
}
]
}
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root).code).toMatchSnapshot()
})
test('error on v-slot usage on plain elements', () => {
const onError = jest.fn()
const source = `<div v-slot/>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_MISPLACED_V_SLOT,
loc: {
source: `v-slot`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 6,
line: 1,
column: index + 7
test('named slot with v-for w/ prefixIdentifiers: true', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template v-for="name in list" #[name]>{{ name }}</template>
</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_SLOTS,
arguments: [
createObjectMatcher({}),
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
arguments: [
{ content: `_ctx.list` },
{
type: NodeTypes.JS_FUNCTION_EXPRESSION,
params: [{ content: `name` }],
returns: createObjectMatcher({
name: `[name]`,
fn: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: [
{
type: NodeTypes.INTERPOLATION,
content: { content: `name`, isStatic: false }
}
]
}
})
}
]
}
]
}
}
]
})
expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
PatchFlags.DYNAMIC_SLOTS + ''
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('error on named slot on component', () => {
const onError = jest.fn()
const source = `<Comp v-slot:foo>foo</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
loc: {
source: `v-slot:foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 10,
line: 1,
column: index + 11
describe('errors', () => {
test('error on extraneous children w/ named slots', () => {
const onError = jest.fn()
const source = `<Comp><template #default>foo</template>bar</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('bar')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
loc: {
source: `bar`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 3,
line: 1,
column: index + 4
}
}
}
})
})
test('error on duplicated slot names', () => {
const onError = jest.fn()
const source = `<Comp><template #foo></template><template #foo></template></Comp>`
parseWithSlots(source, { onError })
const index = source.lastIndexOf('#foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 4,
line: 1,
column: index + 5
}
}
})
})
test('error on invalid mixed slot usage', () => {
const onError = jest.fn()
const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
parseWithSlots(source, { onError })
const index = source.lastIndexOf('#foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_MIXED_SLOT_USAGE,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 4,
line: 1,
column: index + 5
}
}
})
})
test('error on v-slot usage on plain elements', () => {
const onError = jest.fn()
const source = `<div v-slot/>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_MISPLACED_V_SLOT,
loc: {
source: `v-slot`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 6,
line: 1,
column: index + 7
}
}
})
})
test('error on named slot on component', () => {
const onError = jest.fn()
const source = `<Comp v-slot:foo>foo</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
loc: {
source: `v-slot:foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 10,
line: 1,
column: index + 11
}
}
})
})
})
})