feat(compiler-vapor): `v-else` / `v-else-if` (#98)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-01-29 03:42:56 +08:00 committed by GitHub
parent 63aacf6194
commit 63a127b612
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 496 additions and 53 deletions

View File

@ -20,6 +20,30 @@ export function render(_ctx) {
}"
`;
exports[`compiler: v-if > comment between branches 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<!--foo--><p></p>")
const t2 = _template("<!--bar-->fine")
const t3 = _fragment()
const n0 = t3()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => _createIf(() => (_ctx.orNot), () => {
const n3 = t1()
return n3
}, () => {
const n4 = t2()
return n4
}))
_prepend(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > dedupe same template 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor';
@ -59,3 +83,67 @@ export function render(_ctx) {
return n0
}"
`;
exports[`compiler: v-if > v-if + v-else 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<p></p>")
const t2 = _fragment()
const n0 = t2()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => {
const n3 = t1()
return n3
})
_prepend(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<p></p>")
const t2 = _template("fine")
const t3 = _fragment()
const n0 = t3()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => _createIf(() => (_ctx.orNot), () => {
const n3 = t1()
return n3
}, () => {
const n4 = t2()
return n4
}))
_prepend(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > v-if + v-else-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _template("<p></p>")
const t2 = _fragment()
const n0 = t2()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
}, () => _createIf(() => (_ctx.orNot), () => {
const n3 = t1()
return n3
}))
_prepend(n0, n1)
return n0
}"
`;

View File

@ -1,11 +1,14 @@
import { makeCompile } from './_utils'
import {
IRNodeTypes,
type IfIRNode,
transformElement,
transformInterpolation,
transformOnce,
transformVIf,
transformVText,
} from '../../src'
import { NodeTypes } from '@vue/compiler-core'
const compileWithVIf = makeCompile({
nodeTransforms: [
@ -21,15 +24,87 @@ const compileWithVIf = makeCompile({
describe('compiler: v-if', () => {
test('basic v-if', () => {
const { code } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`)
const { code, vaporHelpers, ir, helpers } = compileWithVIf(
`<div v-if="ok">{{msg}}</div>`,
)
expect(vaporHelpers).contains('createIf')
expect(helpers.size).toBe(0)
expect(ir.template).lengthOf(2)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
type: IRNodeTypes.FRAGMENT_FACTORY,
},
])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'ok',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
},
{
type: IRNodeTypes.APPEND_NODE,
elements: [1],
parent: 0,
},
])
expect(ir.dynamic).toMatchObject({
id: 0,
children: { 0: { id: 1 } },
})
expect(ir.effect).toEqual([])
expect((ir.operation[0] as IfIRNode).positive.effect).lengthOf(1)
expect(code).matchSnapshot()
})
test('template v-if', () => {
const { code } = compileWithVIf(
const { code, ir } = compileWithVIf(
`<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(2)
expect(ir.template[0]).toMatchObject({
template: '<div></div>hello<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
})
expect(ir.effect).toEqual([])
expect((ir.operation[0] as IfIRNode).positive.effect).toMatchObject([
{
operations: [
{
type: IRNodeTypes.SET_TEXT,
element: 3,
value: {
content: 'msg',
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
},
},
],
},
])
expect((ir.operation[0] as IfIRNode).positive.dynamic).toMatchObject({
id: 2,
children: { 2: { id: 3 } },
})
})
test('dedupe same template', () => {
@ -42,10 +117,185 @@ describe('compiler: v-if', () => {
test.todo('v-if with v-once')
test.todo('component v-if')
test.todo('v-if + v-else')
test.todo('v-if + v-else-if')
test.todo('v-if + v-else-if + v-else')
test.todo('comment between branches')
test('v-if + v-else', () => {
const { code, ir, vaporHelpers, helpers } = compileWithVIf(
`<div v-if="ok"/><p v-else/>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(3)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
type: IRNodeTypes.FRAGMENT_FACTORY,
},
])
expect(vaporHelpers).contains('createIf')
expect(ir.effect).lengthOf(0)
expect(helpers).lengthOf(0)
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'ok',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
negative: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 1,
},
},
{
type: IRNodeTypes.PREPEND_NODE,
elements: [1],
parent: 0,
},
])
})
test('v-if + v-else-if', () => {
const { code, ir } = compileWithVIf(
`<div v-if="ok"/><p v-else-if="orNot"/>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(3)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{ type: IRNodeTypes.FRAGMENT_FACTORY },
])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'ok',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
negative: {
type: IRNodeTypes.IF,
condition: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'orNot',
isStatic: false,
},
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 1,
},
},
},
{
type: IRNodeTypes.PREPEND_NODE,
elements: [1],
parent: 0,
},
])
})
test('v-if + v-else-if + v-else', () => {
const { code, ir } = compileWithVIf(
`<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(4)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: 'fine',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{ type: IRNodeTypes.FRAGMENT_FACTORY },
])
expect(ir.operation).toMatchObject([
{
type: IRNodeTypes.IF,
id: 1,
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 0,
},
negative: {
type: IRNodeTypes.IF,
positive: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 1,
},
negative: {
type: IRNodeTypes.BLOCK_FUNCTION,
templateIndex: 2,
},
},
},
{
type: IRNodeTypes.PREPEND_NODE,
elements: [1],
parent: 0,
},
])
})
test('comment between branches', () => {
const { code, ir } = compileWithVIf(`
<div v-if="ok"/>
<!--foo-->
<p v-else-if="orNot"/>
<!--bar-->
<template v-else>fine</template>
`)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(4)
expect(ir.template).toMatchObject([
{
template: '<div></div>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<!--foo--><p></p>',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{
template: '<!--bar-->fine',
type: IRNodeTypes.TEMPLATE_FACTORY,
},
{ type: IRNodeTypes.FRAGMENT_FACTORY },
])
})
describe.todo('errors')
describe.todo('codegen')
test.todo('v-on with v-if')

View File

@ -1,12 +1,30 @@
import { type CodegenContext, genBlockFunctionContent } from '../generate'
import type { BlockFunctionIRNode, IfIRNode } from '../ir'
import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir'
import { genExpression } from './expression'
export function genIf(oper: IfIRNode, context: CodegenContext) {
const { pushFnCall, vaporHelper, pushNewline, push, withIndent } = context
export function genIf(
oper: IfIRNode,
context: CodegenContext,
isNested = false,
) {
const { pushFnCall, vaporHelper, pushNewline, push } = context
const { condition, positive, negative } = oper
pushNewline(`const n${oper.id} = `)
let positiveArg = () => genBlockFunction(positive, context)
let negativeArg: false | (() => void) = false
if (negative) {
if (negative.type === IRNodeTypes.BLOCK_FUNCTION) {
negativeArg = () => genBlockFunction(negative, context)
} else {
negativeArg = () => {
push('() => ')
genIf(negative!, context, true)
}
}
}
if (!isNested) pushNewline(`const n${oper.id} = `)
pushFnCall(
vaporHelper('createIf'),
() => {
@ -14,15 +32,17 @@ export function genIf(oper: IfIRNode, context: CodegenContext) {
genExpression(condition, context)
push(')')
},
() => genBlockFunction(positive),
!!negative && (() => genBlockFunction(negative!)),
positiveArg,
negativeArg,
)
function genBlockFunction(oper: BlockFunctionIRNode) {
push('() => {')
withIndent(() => {
genBlockFunctionContent(oper, context)
})
pushNewline('}')
}
}
function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) {
const { pushNewline, push, withIndent } = context
push('() => {')
withIndent(() => {
genBlockFunctionContent(oper, context)
})
pushNewline('}')
}

View File

@ -62,7 +62,7 @@ export interface IfIRNode extends BaseIRNode {
id: number
condition: IRExpression
positive: BlockFunctionIRNode
negative?: BlockFunctionIRNode
negative?: BlockFunctionIRNode | IfIRNode
}
export interface TemplateFactoryIRNode extends BaseIRNode {

View File

@ -42,9 +42,9 @@ export type DirectiveTransform = (
// A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = (
node: RootNode | TemplateChildNode,
node: ElementNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
context: TransformContext<ElementNode>,
) => void | (() => void)
export type TransformOptions = HackOptions<BaseTransformOptions>
@ -60,7 +60,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
>
template: string
childrenTemplate: string[]
childrenTemplate: (string | null)[]
dynamic: IRDynamicInfo
inVOnce: boolean
@ -311,15 +311,12 @@ function transformNode(
}
if (context.node.type === NodeTypes.ROOT)
context.template += context.childrenTemplate.join('')
context.template += context.childrenTemplate.filter(Boolean).join('')
}
function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
const { children } = ctx.node
let i = 0
// const nodeRemoved = () => {
// i--
// }
for (; i < children.length; i++) {
const child = children[i]
const childContext = createContext(child, ctx, i)
@ -405,7 +402,11 @@ export function createStructuralDirectiveTransform(
const exitFns = []
for (const prop of props) {
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
const onExit = fn(node, prop as VaporDirectiveNode, context)
const onExit = fn(
node,
prop as VaporDirectiveNode,
context as TransformContext<ElementNode>,
)
if (onExit) exitFns.push(onExit)
}
}

View File

@ -1,4 +1,5 @@
import {
type ElementNode,
ElementTypes,
ErrorCodes,
NodeTypes,
@ -15,6 +16,7 @@ import {
import {
type BlockFunctionIRNode,
IRNodeTypes,
type OperationNode,
type VaporDirectiveNode,
} from '../ir'
import { extend } from '@vue/shared'
@ -25,7 +27,7 @@ export const transformVIf = createStructuralDirectiveTransform(
)
export function processIf(
node: RootNode | TemplateChildNode,
node: ElementNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
) {
@ -40,7 +42,7 @@ export function processIf(
if (dir.name === 'if') {
const id = context.reference()
context.dynamic.ghost = true
const [branch, onExit] = createIfBranch(node, dir, context)
const [branch, onExit] = createIfBranch(node, context)
return () => {
onExit()
@ -52,37 +54,100 @@ export function processIf(
positive: branch,
})
}
} else {
// check the adjacent v-if
const parent = context.parent!
const siblings = parent.node.children
const templates = parent.childrenTemplate
const comments = []
let sibling: TemplateChildNode | undefined
let i = siblings.indexOf(node)
while (i-- >= -1) {
sibling = siblings[i]
if (sibling) {
if (sibling.type === NodeTypes.COMMENT) {
__DEV__ && comments.unshift(sibling)
templates[i] = null
continue
} else if (
sibling.type === NodeTypes.TEXT &&
!sibling.content.trim().length
) {
templates[i] = null
continue
}
}
break
}
const { operation } = context.block
let lastIfNode: OperationNode
if (
// check if v-if is the sibling node
!sibling ||
sibling.type !== NodeTypes.ELEMENT ||
!sibling.props.some(
({ type, name }) =>
type === NodeTypes.DIRECTIVE && ['if', 'else-if'].includes(name),
) ||
// check if IFNode is the last operation and get the root IFNode
!(lastIfNode = operation[operation.length - 1]) ||
lastIfNode.type !== IRNodeTypes.IF
) {
context.options.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
)
return
}
while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
lastIfNode = lastIfNode.negative
}
// Check if v-else was followed by v-else-if
if (dir.name === 'else-if' && lastIfNode.negative) {
context.options.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
)
}
// TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
if (__DEV__ && comments.length) {
node = wrapTemplate(node)
context.node = node = extend({}, node, {
children: [...comments, ...node.children],
})
}
const [branch, onExit] = createIfBranch(node, context)
if (dir.name === 'else') {
lastIfNode.negative = branch
} else {
lastIfNode.negative = {
type: IRNodeTypes.IF,
id: -1,
loc: dir.loc,
condition: dir.exp!,
positive: branch,
}
}
return () => onExit()
}
}
export function createIfBranch(
node: RootNode | TemplateChildNode,
dir: VaporDirectiveNode,
node: ElementNode,
context: TransformContext<RootNode | TemplateChildNode>,
): [BlockFunctionIRNode, () => void] {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType !== ElementTypes.TEMPLATE
) {
node = extend({}, node, {
type: NodeTypes.ELEMENT,
tag: 'template',
props: [],
tagType: ElementTypes.TEMPLATE,
children: [
extend({}, node, {
props: node.props.filter(
p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
),
} as TemplateChildNode),
],
} as Partial<TemplateNode>)
context.node = node
}
context.node = node = wrapTemplate(node)
const branch: BlockFunctionIRNode = {
type: IRNodeTypes.BLOCK_FUNCTION,
loc: dir.loc,
loc: node.loc,
node,
templateIndex: -1,
dynamic: {
@ -105,3 +170,22 @@ export function createIfBranch(
}
return [branch, onExit]
}
function wrapTemplate(node: ElementNode): TemplateNode {
if (node.tagType === ElementTypes.TEMPLATE) {
return node
}
return extend({}, node, {
type: NodeTypes.ELEMENT,
tag: 'template',
props: [],
tagType: ElementTypes.TEMPLATE,
children: [
extend({}, node, {
props: node.props.filter(
p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
),
} as TemplateChildNode),
],
} as Partial<TemplateNode>)
}