diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap
index ada1ac6f1ca..b17a825b7b8 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/transformText.spec.ts.snap
@@ -67,13 +67,13 @@ exports[`compiler: transform text element with custom directives and only one te
return function render(_ctx, _cache) {
with (_ctx) {
- const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+ const { toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, resolveDirective: _resolveDirective, openBlock: _openBlock, createElementBlock: _createElementBlock, withDirectives: _withDirectives } = _Vue
const _directive_foo = _resolveDirective(\\"foo\\")
return _withDirectives((_openBlock(), _createElementBlock(\\"p\\", null, [
_createTextVNode(_toDisplayString(foo), 1 /* TEXT */)
- ], 512 /* NEED_PATCH */)), [
+ ])), [
[_directive_foo]
])
}
diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
index eb29f346522..17e6b5bb14a 100644
--- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
@@ -1202,6 +1202,23 @@ describe('compiler: element transform', () => {
})
})
+ test('force block for runtime custom directive w/ children', () => {
+ const { node } = parseWithElementTransform(`
hello
`)
+ expect(node.isBlock).toBe(true)
+ })
+
+ test('force block for inline before-update handlers w/ children', () => {
+ expect(
+ parseWithElementTransform(`hello
`).node
+ .isBlock
+ ).toBe(true)
+
+ expect(
+ parseWithElementTransform(`hello
`).node
+ .isBlock
+ ).toBe(true)
+ })
+
// #938
test('element with dynamic keys should be forced into blocks', () => {
const ast = parse(``)
diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts
index f4d824f5d72..5eeb9f8c6ce 100644
--- a/packages/compiler-core/src/parse.ts
+++ b/packages/compiler-core/src/parse.ts
@@ -11,7 +11,7 @@ import {
advancePositionWithMutation,
advancePositionWithClone,
isCoreComponent,
- isBindKey
+ isStaticArgOf
} from './utils'
import {
Namespaces,
@@ -681,7 +681,7 @@ function isComponent(
} else if (
// :is on plain element - only treat as component in compat mode
p.name === 'bind' &&
- isBindKey(p.arg, 'is') &&
+ isStaticArgOf(p.arg, 'is') &&
__COMPAT__ &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts
index 215792ed403..5c8a10923bf 100644
--- a/packages/compiler-core/src/transforms/hoistStatic.ts
+++ b/packages/compiler-core/src/transforms/hoistStatic.ts
@@ -168,6 +168,13 @@ export function getConstantType(
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return ConstantTypes.NOT_CONSTANT
}
+ if (
+ codegenNode.isBlock &&
+ node.tag !== 'svg' &&
+ node.tag !== 'foreignObject'
+ ) {
+ return ConstantTypes.NOT_CONSTANT
+ }
const flag = getPatchFlag(codegenNode)
if (!flag) {
let returnType = ConstantTypes.CAN_STRINGIFY
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index d88bd7b9f37..fb0ddd528c7 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -56,7 +56,7 @@ import {
toValidAssetId,
findProp,
isCoreComponent,
- isBindKey,
+ isStaticArgOf,
findDir,
isStaticExp
} from '../utils'
@@ -120,10 +120,7 @@ export const transformElement: NodeTransform = (node, context) => {
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
- (tag === 'svg' ||
- tag === 'foreignObject' ||
- // #938: elements with dynamic keys should be forced into blocks
- findProp(node, 'key', true)))
+ (tag === 'svg' || tag === 'foreignObject'))
// props
if (props.length > 0) {
@@ -138,6 +135,10 @@ export const transformElement: NodeTransform = (node, context) => {
directives.map(dir => buildDirectiveArgs(dir, context))
) as DirectiveArguments)
: undefined
+
+ if (propsBuildResult.shouldUseBlock) {
+ shouldUseBlock = true
+ }
}
// children
@@ -386,12 +387,15 @@ export function buildProps(
directives: DirectiveNode[]
patchFlag: number
dynamicPropNames: string[]
+ shouldUseBlock: boolean
} {
- const { tag, loc: elementLoc } = node
+ const { tag, loc: elementLoc, children } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = []
const runtimeDirectives: DirectiveNode[] = []
+ const hasChildren = children.length > 0
+ let shouldUseBlock = false
// patchFlag analysis
let patchFlag = 0
@@ -526,7 +530,7 @@ export function buildProps(
if (
name === 'is' ||
(isVBind &&
- isBindKey(arg, 'is') &&
+ isStaticArgOf(arg, 'is') &&
(isComponentTag(tag) ||
(__COMPAT__ &&
isCompatEnabled(
@@ -541,6 +545,16 @@ export function buildProps(
continue
}
+ if (
+ // #938: elements with dynamic keys should be forced into blocks
+ (isVBind && isStaticArgOf(arg, 'key')) ||
+ // inline before-update hooks need to force block so that it is invoked
+ // before children
+ (isVOn && hasChildren && isStaticArgOf(arg, 'vnodeBeforeUpdate', true))
+ ) {
+ shouldUseBlock = true
+ }
+
// special case for v-bind and v-on with no argument
if (!arg && (isVBind || isVOn)) {
hasDynamicKeys = true
@@ -633,6 +647,11 @@ export function buildProps(
} else {
// no built-in transform, this is a user custom directive.
runtimeDirectives.push(prop)
+ // custom dirs may use beforeUpdate so they need to force blocks
+ // to ensure before-update gets called before children update
+ if (hasChildren) {
+ shouldUseBlock = true
+ }
}
}
@@ -700,6 +719,7 @@ export function buildProps(
}
}
if (
+ !shouldUseBlock &&
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
(hasRef || hasVnodeHook || runtimeDirectives.length > 0)
) {
@@ -784,7 +804,8 @@ export function buildProps(
props: propsExpression,
directives: runtimeDirectives,
patchFlag,
- dynamicPropNames
+ dynamicPropNames,
+ shouldUseBlock
}
}
diff --git a/packages/compiler-core/src/transforms/transformSlotOutlet.ts b/packages/compiler-core/src/transforms/transformSlotOutlet.ts
index 50f0e0bfc8c..394684b408d 100644
--- a/packages/compiler-core/src/transforms/transformSlotOutlet.ts
+++ b/packages/compiler-core/src/transforms/transformSlotOutlet.ts
@@ -7,7 +7,7 @@ import {
SlotOutletNode,
createFunctionExpression
} from '../ast'
-import { isSlotOutlet, isBindKey, isStaticExp } from '../utils'
+import { isSlotOutlet, isStaticArgOf, isStaticExp } from '../utils'
import { buildProps, PropsExpression } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors'
import { RENDER_SLOT } from '../runtimeHelpers'
@@ -75,7 +75,7 @@ export function processSlotOutlet(
}
}
} else {
- if (p.name === 'bind' && isBindKey(p.arg, 'name')) {
+ if (p.name === 'bind' && isStaticArgOf(p.arg, 'name')) {
if (p.exp) slotName = p.exp
} else {
if (p.name === 'bind' && p.arg && isStaticExp(p.arg)) {
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index 6e2443ea664..0c62f3043d9 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -42,7 +42,14 @@ import {
WITH_MEMO,
OPEN_BLOCK
} from './runtimeHelpers'
-import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared'
+import {
+ isString,
+ isObject,
+ hyphenate,
+ extend,
+ NOOP,
+ camelize
+} from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser'
import { Expression } from '@babel/types'
@@ -282,15 +289,23 @@ export function findProp(
} else if (
p.name === 'bind' &&
(p.exp || allowEmpty) &&
- isBindKey(p.arg, name)
+ isStaticArgOf(p.arg, name)
) {
return p
}
}
}
-export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
- return !!(arg && isStaticExp(arg) && arg.content === name)
+export function isStaticArgOf(
+ arg: DirectiveNode['arg'],
+ name: string,
+ camel?: boolean
+): boolean {
+ return !!(
+ arg &&
+ isStaticExp(arg) &&
+ (camel ? camelize(arg.content) : arg.content) === name
+ )
}
export function hasDynamicKeyVBind(node: ElementNode): boolean {
@@ -371,7 +386,8 @@ export function injectProp(
*
* we need to get the real props before normalization
*/
- let props = node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
+ let props =
+ node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
let callPath: CallExpression[] = []
let parentCall: CallExpression | undefined
if (
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
index c9515d27baa..659537b2c82 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
@@ -22,7 +22,7 @@ import {
TextNode,
hasDynamicKeyVBind,
MERGE_PROPS,
- isBindKey,
+ isStaticArgOf,
createSequenceExpression,
InterpolationNode,
isStaticExp,
@@ -335,7 +335,7 @@ function isTextareaWithValue(
return !!(
node.tag === 'textarea' &&
prop.name === 'bind' &&
- isBindKey(prop.arg, 'value')
+ isStaticArgOf(prop.arg, 'value')
)
}