}"
`;
+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';
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
+}"
+`;
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: [
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', () => {
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')
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'),
() => {
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('}')
}
id: number
condition: IRExpression
positive: BlockFunctionIRNode
- negative?: BlockFunctionIRNode
+ negative?: BlockFunctionIRNode | IfIRNode
}
export interface TemplateFactoryIRNode extends BaseIRNode {
// 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>
>
template: string
- childrenTemplate: string[]
+ childrenTemplate: (string | null)[]
dynamic: IRDynamicInfo
inVOnce: boolean
}
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)
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)
}
}
import {
+ type ElementNode,
ElementTypes,
ErrorCodes,
NodeTypes,
import {
type BlockFunctionIRNode,
IRNodeTypes,
+ type OperationNode,
type VaporDirectiveNode,
} from '../ir'
import { extend } from '@vue/shared'
)
export function processIf(
- node: RootNode | TemplateChildNode,
+ node: ElementNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
) {
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()
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: {
}
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>)
+}