ErrorCodes,
BindingTypes,
NodeTransform,
- transformExpression
+ transformExpression,
+ baseCompile
} from '../../src'
import {
RESOLVE_COMPONENT,
return parseWithElementTransform(template, {
...options,
directiveTransforms: {
+ ...options?.directiveTransforms,
bind: transformBind
}
})
})
test('NEED_PATCH (vnode hooks)', () => {
- const { node } = parseWithBind(`<div @vnodeUpdated="foo" />`)
+ const root = baseCompile(`<div @vnodeUpdated="foo" />`, {
+ prefixIdentifiers: true,
+ cacheHandlers: true
+ }).ast
+ const node = (root as any).children[0].codegenNode
expect(node.patchFlag).toBe(genFlagText(PatchFlags.NEED_PATCH))
})
export {
transformElement,
resolveComponentType,
- buildProps
+ buildProps,
+ buildDirectiveArgs,
+ PropsExpression
} from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { generateCodeFrame } from '@vue/shared'
isObject,
isReservedProp,
capitalize,
- camelize
+ camelize,
+ isBuiltInDirective
} from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import {
directiveImportMap.set(prop, needRuntime)
}
}
- } else {
+ } else if (!isBuiltInDirective(name)) {
// no built-in transform, this is a user custom directive.
runtimeDirectives.push(prop)
// custom dirs may use beforeUpdate so they need to force blocks
}
}
-function buildDirectiveArgs(
+export function buildDirectiveArgs(
dir: DirectiveNode,
context: TransformContext
): ArrayExpression {
return function render(_ctx, _cache) {
with (_ctx) {
- const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
-
- const _directive_bind = _resolveDirective(\\"bind\\")
+ const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => ((model) = $event)
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
- [_directive_bind, val, key],
[_vModelDynamic, model]
])
}
return function render(_ctx, _cache) {
with (_ctx) {
- const { vModelDynamic: _vModelDynamic, resolveDirective: _resolveDirective, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
-
- const _directive_bind = _resolveDirective(\\"bind\\")
+ const { vModelDynamic: _vModelDynamic, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return _withDirectives((_openBlock(), _createElementBlock(\\"input\\", {
\\"onUpdate:modelValue\\": $event => ((model) = $event)
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
- [_directive_bind, foo, \\"type\\"],
[_vModelDynamic, model]
])
}
})
})
})
+
+ describe('custom directive', () => {
+ test('basic', () => {
+ expect(compile(`<foo v-xxx:x.y="z" />`).code).toMatchInlineSnapshot(`
+ "const { resolveComponent: _resolveComponent, resolveDirective: _resolveDirective, mergeProps: _mergeProps } = require(\\"vue\\")
+ const { ssrGetDirectiveProps: _ssrGetDirectiveProps, ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_foo = _resolveComponent(\\"foo\\")
+ const _directive_xxx = _resolveDirective(\\"xxx\\")
+
+ _push(_ssrRenderComponent(_component_foo, _mergeProps(_attrs, _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true })), null, _parent))
+ }"
+ `)
+ })
+ })
})
}></div>\`"
`)
})
+
+ test('custom dir', () => {
+ expect(getCompiledString(`<div v-xxx:x.y="z" />`)).toMatchInlineSnapshot(`
+ "\`<div\${
+ _ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, \\"x\\", { y: true }))
+ }></div>\`"
+ `)
+ })
+
+ test('custom dir with normal attrs', () => {
+ expect(getCompiledString(`<div class="foo" v-xxx />`))
+ .toMatchInlineSnapshot(`
+ "\`<div\${
+ _ssrRenderAttrs(_mergeProps({ class: \\"foo\\" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+ }></div>\`"
+ `)
+ })
+
+ test('custom dir with v-bind', () => {
+ expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
+ .toMatchInlineSnapshot(`
+ "\`<div\${
+ _ssrRenderAttrs(_mergeProps({
+ title: _ctx.foo,
+ class: _ctx.bar
+ }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+ }></div>\`"
+ `)
+ })
+
+ test('custom dir with object v-bind', () => {
+ expect(getCompiledString(`<div v-bind="x" v-xxx />`))
+ .toMatchInlineSnapshot(`
+ "\`<div\${
+ _ssrRenderAttrs(_mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+ }></div>\`"
+ `)
+ })
+
+ test('custom dir with object v-bind + normal bindings', () => {
+ expect(
+ getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`)
+ ).toMatchInlineSnapshot(`
+ "\`<div\${
+ _ssrRenderAttrs(_mergeProps(_ctx.x, {
+ class: \\"foo\\",
+ title: \\"bar\\"
+ }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
+ }></div>\`"
+ `)
+ })
})
})
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
export const SSR_RENDER_TELEPORT = Symbol(`ssrRenderTeleport`)
export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
+export const SSR_GET_DIRECTIVE_PROPS = Symbol(`ssrGetDirectiveProps`)
export const ssrHelpers = {
[SSR_INTERPOLATE]: `ssrInterpolate`,
[SSR_RENDER_DYNAMIC_MODEL]: `ssrRenderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `ssrGetDynamicModelProps`,
[SSR_RENDER_TELEPORT]: `ssrRenderTeleport`,
- [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`
+ [SSR_RENDER_SUSPENSE]: `ssrRenderSuspense`,
+ [SSR_GET_DIRECTIVE_PROPS]: `ssrGetDirectiveProps`
}
// Note: these are helpers imported from @vue/server-renderer
TELEPORT,
TRANSITION_GROUP,
CREATE_VNODE,
- CallExpression
+ CallExpression,
+ JSChildNode
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
import {
} from './ssrTransformSuspense'
import { ssrProcessTransitionGroup } from './ssrTransformTransitionGroup'
import { isSymbol, isObject, isArray } from '@vue/shared'
+import { buildSSRProps } from './ssrTransformElement'
// We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until
})
}
- const props =
- node.props.length > 0
- ? // note we are not passing ssr: true here because for components, v-on
- // handlers should still be passed
- buildProps(node, context).props || `null`
- : `null`
+ let propsExp: string | JSChildNode = `null`
+ if (node.props.length) {
+ // note we are not passing ssr: true here because for components, v-on
+ // handlers should still be passed
+ const { props, directives } = buildProps(node, context)
+ if (props || directives.length) {
+ propsExp = buildSSRProps(props, directives, context)
+ }
+ }
const wipEntries: WIPSlotEntry[] = []
wipMap.set(node, wipEntries)
`_push`,
createCallExpression(context.helper(CREATE_VNODE), [
component,
- props,
+ propsExp,
slots
]),
`_parent`
} else {
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_COMPONENT),
- [component, props, slots, `_parent`]
+ [component, propsExp, slots, `_parent`]
)
}
}
createSequenceExpression,
InterpolationNode,
isStaticExp,
- AttributeNode
+ AttributeNode,
+ buildDirectiveArgs,
+ TransformContext,
+ PropsExpression
} from '@vue/compiler-dom'
import {
escapeHtml,
isBooleanAttr,
+ isBuiltInDirective,
isSSRSafeAttrName,
NO,
propsToAttrMap
SSR_RENDER_ATTRS,
SSR_INTERPOLATE,
SSR_GET_DYNAMIC_MODEL_PROPS,
- SSR_INCLUDE_BOOLEAN_ATTR
+ SSR_INCLUDE_BOOLEAN_ATTR,
+ SSR_GET_DIRECTIVE_PROPS
} from '../runtimeHelpers'
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
const needTagForRuntime =
node.tag === 'textarea' || node.tag.indexOf('-') > 0
- // v-bind="obj" or v-bind:[key] can potentially overwrite other static
- // attrs and can affect final rendering result, so when they are present
- // we need to bail out to full `renderAttrs`
+ // v-bind="obj", v-bind:[key] and custom directives can potentially
+ // overwrite other static attrs and can affect final rendering result,
+ // so when they are present we need to bail out to full `renderAttrs`
const hasDynamicVBind = hasDynamicKeyVBind(node)
- if (hasDynamicVBind) {
- const { props } = buildProps(node, context, node.props, true /* ssr */)
- if (props) {
+ const hasCustomDir = node.props.some(
+ p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name)
+ )
+ const needMergeProps = hasDynamicVBind || hasCustomDir
+ if (needMergeProps) {
+ const { props, directives } = buildProps(
+ node,
+ context,
+ node.props,
+ true /* ssr */
+ )
+ if (props || directives.length) {
+ const mergedProps = buildSSRProps(props, directives, context)
const propsExp = createCallExpression(
context.helper(SSR_RENDER_ATTRS),
- [props]
+ [mergedProps]
)
if (node.tag === 'textarea') {
propsExp.arguments = [
createAssignmentExpression(
createSimpleExpression(tempId, false),
- props
+ mergedProps
)
]
rawChildrenMap.set(
const tempExp = createSimpleExpression(tempId, false)
propsExp.arguments = [
createSequenceExpression([
- createAssignmentExpression(tempExp, props),
+ createAssignmentExpression(tempExp, mergedProps),
createCallExpression(context.helper(MERGE_PROPS), [
tempExp,
createCallExpression(
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
)
} else if (isTextareaWithValue(node, prop) && prop.exp) {
- if (!hasDynamicVBind) {
+ if (!needMergeProps) {
node.children = [createInterpolation(prop.exp, prop.loc)]
}
- } else if (!hasDynamicVBind) {
+ } else if (!needMergeProps) {
// Directive transforms.
const directiveTransform = context.directiveTransforms[prop.name]
if (directiveTransform) {
// special case: value on <textarea>
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
rawChildrenMap.set(node, escapeHtml(prop.value.content))
- } else if (!hasDynamicVBind) {
+ } else if (!needMergeProps) {
if (prop.name === 'key' || prop.name === 'ref') {
continue
}
}
}
+export function buildSSRProps(
+ props: PropsExpression | undefined,
+ directives: DirectiveNode[],
+ context: TransformContext
+): JSChildNode {
+ let mergePropsArgs: JSChildNode[] = []
+ if (props) {
+ if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
+ // already a mergeProps call
+ mergePropsArgs = props.arguments as JSChildNode[]
+ } else {
+ mergePropsArgs.push(props)
+ }
+ }
+ if (directives.length) {
+ for (const dir of directives) {
+ context.directives.add(dir.name)
+ mergePropsArgs.push(
+ createCallExpression(context.helper(SSR_GET_DIRECTIVE_PROPS), [
+ `_ctx`,
+ ...buildDirectiveArgs(dir, context).elements
+ ] as JSChildNode[])
+ )
+ }
+ }
+
+ return mergePropsArgs.length > 1
+ ? createCallExpression(context.helper(MERGE_PROPS), mergePropsArgs)
+ : mergePropsArgs[0]
+}
+
function isTrueFalseValue(prop: DirectiveNode | AttributeNode) {
if (prop.type === NodeTypes.DIRECTIVE) {
return (
*/
import { VNode } from './vnode'
-import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared'
+import { isFunction, EMPTY_OBJ, isBuiltInDirective } from '@vue/shared'
import { warn } from './warning'
import { ComponentInternalInstance, Data } from './component'
import { currentRenderingInstance } from './componentRenderContext'
export type DirectiveModifiers = Record<string, boolean>
-const isBuiltInDirective = /*#__PURE__*/ makeMap(
- 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
-)
-
export function validateDirectiveName(name: string) {
if (isBuiltInDirective(name)) {
warn('Do not use built-in directive ids as custom directive id: ' + name)
vShow,
vModelText,
vModelRadio,
- vModelCheckbox
+ vModelCheckbox,
+ resolveDirective
} from 'vue'
+import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src'
describe('ssr: directives', () => {
describe('template v-show', () => {
})
})
- test('custom directive w/ getSSRProps', async () => {
+ test('custom directive w/ getSSRProps (vdom)', async () => {
expect(
await renderToString(
createApp({
)
).toBe(`<div id="foo"></div>`)
})
+
+ test('custom directive w/ getSSRProps (optimized)', async () => {
+ expect(
+ await renderToString(
+ createApp({
+ data() {
+ return {
+ x: 'foo'
+ }
+ },
+ directives: {
+ xxx: {
+ getSSRProps({ value, arg, modifiers }) {
+ return { id: [value, arg, modifiers.ok].join('-') }
+ }
+ }
+ },
+ ssrRender(_ctx, _push, _parent, _attrs) {
+ const _directive_xxx = resolveDirective('xxx')!
+ _push(
+ `<div${ssrRenderAttrs(
+ ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.x, 'arg', {
+ ok: true
+ })
+ )}></div>`
+ )
+ }
+ })
+ )
+ ).toBe(`<div id="foo-arg-true"></div>`)
+ })
})
--- /dev/null
+import { ComponentPublicInstance, Directive } from '@vue/runtime-core'
+
+export function ssrGetDirectiveProps(
+ instance: ComponentPublicInstance,
+ dir: Directive,
+ value?: any,
+ arg?: string,
+ modifiers: Record<string, boolean> = {}
+): Record<string, any> {
+ if (typeof dir !== 'function' && dir.getSSRProps) {
+ return (
+ dir.getSSRProps(
+ {
+ dir,
+ instance,
+ value,
+ oldValue: undefined,
+ arg,
+ modifiers
+ },
+ null as any
+ ) || {}
+ )
+ }
+ return {}
+}
export { ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList } from './helpers/ssrRenderList'
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
+export { ssrGetDirectiveProps } from './helpers/ssrGetDirectiveProps'
export { includeBooleanAttr as ssrIncludeBooleanAttr } from '@vue/shared'
// v-model helpers
'onVnodeBeforeUnmount,onVnodeUnmounted'
)
+export const isBuiltInDirective = /*#__PURE__*/ makeMap(
+ 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
+)
+
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
const cache: Record<string, string> = Object.create(null)
return ((str: string) => {