export interface TransformOptions {
nodeTransforms?: NodeTransform[]
directiveTransforms?: Record<string, DirectiveTransform | undefined>
- ssrDirectiveTransforms?: Record<string, DirectiveTransform | undefined>
isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`.
// If this option is false, the generated code will be wrapped in a
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
- ssrDirectiveTransforms = {},
isBuiltInComponent = NOOP,
ssr = false,
onError = defaultOnError
cacheHandlers,
nodeTransforms,
directiveTransforms,
- ssrDirectiveTransforms,
isBuiltInComponent,
ssr,
onError,
export function buildProps(
node: ElementNode,
context: TransformContext,
- props: ElementNode['props'] = node.props
+ props: ElementNode['props'] = node.props,
+ ssr = false
): {
props: PropsExpression | undefined
directives: DirectiveNode[]
continue
}
- // special case for v-bind and v-on with no argument
const isBind = name === 'bind'
const isOn = name === 'on'
+
+ // skip v-on in SSR compilation
+ if (ssr && isOn) {
+ continue
+ }
+
+ // special case for v-bind and v-on with no argument
if (!arg && (isBind || isOn)) {
hasDynamicKeys = true
if (exp) {
if (directiveTransform) {
// has built-in directive transform.
const { props, needRuntime } = directiveTransform(prop, node, context)
- props.forEach(analyzePatchFlag)
+ !ssr && props.forEach(analyzePatchFlag)
properties.push(...props)
if (needRuntime) {
runtimeDirectives.push(prop)
const name = prop.key.content
const existing = knownProps.get(name)
if (existing) {
- if (
- name === 'style' ||
- name === 'class' ||
- name.startsWith('on') ||
- name.startsWith('vnode')
- ) {
+ if (name === 'style' || name === 'class' || name.startsWith('on')) {
mergeAsArray(existing, prop)
}
// unexpected duplicate, should have emitted error during parse
})
}
+export { transformStyle } from './transforms/transformStyle'
export { DOMErrorCodes } from './errors'
export * from '@vue/compiler-core'
import {
NodeTransform,
NodeTypes,
- createSimpleExpression
+ createSimpleExpression,
+ SimpleExpressionNode,
+ SourceLocation
} from '@vue/compiler-core'
// Parse inline CSS strings for static style attributes into an object.
node.props.forEach((p, i) => {
if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) {
// replace p with an expression node
- const parsed = JSON.stringify(parseInlineCSS(p.value.content))
- const exp = context.hoist(createSimpleExpression(parsed, false, p.loc))
+ const exp = context.hoist(parseInlineCSS(p.value.content, p.loc))
node.props[i] = {
type: NodeTypes.DIRECTIVE,
name: `bind`,
const listDelimiterRE = /;(?![^(]*\))/g
const propertyDelimiterRE = /:(.+)/
-function parseInlineCSS(cssText: string): Record<string, string> {
+function parseInlineCSS(
+ cssText: string,
+ loc: SourceLocation
+): SimpleExpressionNode {
const res: Record<string, string> = {}
cssText.split(listDelimiterRE).forEach(item => {
if (item) {
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
}
})
- return res
+ return createSimpleExpression(JSON.stringify(res), false, loc)
}
getCompiledString(`<textarea value="fo>o"/>`)
).toMatchInlineSnapshot(`"\`<textarea>fo>o</textarea>\`"`)
})
+
+ test('<textarea> with dynamic v-bind', () => {
+ // TODO
+ })
})
describe('attrs', () => {
)
})
+ test('static class + v-bind:class', () => {
+ expect(
+ getCompiledString(`<div class="foo" :class="bar"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderClass([_ctx.bar, \\"foo\\"])}></div>\`"`
+ )
+ })
+
test('v-bind:style', () => {
expect(
getCompiledString(`<div id="foo" :style="bar"></div>`)
)
})
+ test('static style + v-bind:style', () => {
+ expect(
+ getCompiledString(`<div style="color:red;" :style="bar"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderStyle([_hoisted_1, _ctx.bar])}></div>\`"`
+ )
+ })
+
test('v-bind:key (boolean)', () => {
expect(
getCompiledString(`<input type="checkbox" :checked="checked">`)
`"\`<div\${_renderAttr(\\"id\\", _ctx.id)} class=\\"bar\\"></div>\`"`
)
})
+
+ test('v-bind:[key]', () => {
+ expect(
+ getCompiledString(`<div v-bind:[key]="value"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderAttrs({ [_ctx.key]: _ctx.value })}></div>\`"`
+ )
+
+ expect(getCompiledString(`<div class="foo" v-bind:[key]="value"></div>`))
+ .toMatchInlineSnapshot(`
+ "\`<div\${_renderAttrs({
+ class: \\"foo\\",
+ [_ctx.key]: _ctx.value
+ })}></div>\`"
+ `)
+
+ expect(getCompiledString(`<div :id="id" v-bind:[key]="value"></div>`))
+ .toMatchInlineSnapshot(`
+ "\`<div\${_renderAttrs({
+ id: _ctx.id,
+ [_ctx.key]: _ctx.value
+ })}></div>\`"
+ `)
+ })
+
+ test('v-bind="obj"', () => {
+ expect(
+ getCompiledString(`<div v-bind="obj"></div>`)
+ ).toMatchInlineSnapshot(`"\`<div\${_renderAttrs(_ctx.obj)}></div>\`"`)
+
+ expect(
+ getCompiledString(`<div class="foo" v-bind="obj"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderAttrs(mergeProps({ class: \\"foo\\" }, _ctx.obj))}></div>\`"`
+ )
+
+ expect(
+ getCompiledString(`<div :id="id" v-bind="obj"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderAttrs(mergeProps({ id: _ctx.id }, _ctx.obj))}></div>\`"`
+ )
+
+ // dynamic key + v-bind="object"
+ expect(
+ getCompiledString(`<div :[key]="id" v-bind="obj"></div>`)
+ ).toMatchInlineSnapshot(
+ `"\`<div\${_renderAttrs(mergeProps({ [_ctx.key]: _ctx.id }, _ctx.obj))}></div>\`"`
+ )
+
+ // should merge class and :class
+ expect(getCompiledString(`<div class="a" :class="b" v-bind="obj"></div>`))
+ .toMatchInlineSnapshot(`
+ "\`<div\${_renderAttrs(mergeProps({
+ class: [\\"a\\", _ctx.b]
+ }, _ctx.obj))}></div>\`"
+ `)
+
+ // should merge style and :style
+ expect(
+ getCompiledString(
+ `<div style="color:red;" :style="b" v-bind="obj"></div>`
+ )
+ ).toMatchInlineSnapshot(`
+ "\`<div\${_renderAttrs(mergeProps({
+ style: [_hoisted_1, _ctx.b]
+ }, _ctx.obj))}></div>\`"
+ `)
+ })
+
+ test('should ignore v-on', () => {
+ expect(
+ getCompiledString(`<div id="foo" @click="bar"/>`)
+ ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\"></div>\`"`)
+ expect(
+ getCompiledString(`<div id="foo" v-on="bar"/>`)
+ ).toMatchInlineSnapshot(`"\`<div id=\\"foo\\"></div>\`"`)
+ expect(
+ getCompiledString(`<div v-bind="foo" v-on="bar"/>`)
+ ).toMatchInlineSnapshot(`"\`<div\${_renderAttrs(_ctx.foo)}></div>\`"`)
+ })
})
})
trackVForSlotScopes,
trackSlotScopes,
noopDirectiveTransform,
- transformBind
+ transformBind,
+ transformStyle
} from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement'
ssrTransformElement,
ssrTransformComponent,
trackSlotScopes,
+ transformStyle,
...(options.nodeTransforms || []) // user transforms
],
- ssrDirectiveTransforms: {
- on: noopDirectiveTransform,
- cloak: noopDirectiveTransform,
- bind: transformBind, // reusing core v-bind
+ directiveTransforms: {
+ // reusing core v-bind
+ bind: transformBind,
+ // model and show has dedicated SSR handling
model: ssrTransformModel,
show: ssrTransformShow,
- ...(options.ssrDirectiveTransforms || {}) // user transforms
+ // the following are ignored during SSR
+ on: noopDirectiveTransform,
+ cloak: noopDirectiveTransform,
+ once: noopDirectiveTransform,
+ ...(options.directiveTransforms || {}) // user transforms
}
})
createInterpolation,
createCallExpression,
createConditionalExpression,
- createSimpleExpression
+ createSimpleExpression,
+ buildProps,
+ DirectiveNode,
+ PlainElementNode,
+ createCompilerError,
+ ErrorCodes,
+ CallExpression,
+ createArrayExpression,
+ ExpressionNode,
+ JSChildNode
} from '@vue/compiler-dom'
import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
SSR_RENDER_ATTR,
SSR_RENDER_CLASS,
SSR_RENDER_STYLE,
- SSR_RENDER_DYNAMIC_ATTR
+ SSR_RENDER_DYNAMIC_ATTR,
+ SSR_RENDER_ATTRS
} from '../runtimeHelpers'
export const ssrTransformElement: NodeTransform = (node, context) => {
p.arg.type !== NodeTypes.SIMPLE_EXPRESSION || // v-bind:[_ctx.foo]
!p.arg.isStatic) // v-bind:[foo]
)
-
if (hasDynamicVBind) {
- // TODO
+ const { props } = buildProps(node, context, node.props, true /* ssr */)
+ if (props) {
+ openTag.push(
+ createCallExpression(context.helper(SSR_RENDER_ATTRS), [props])
+ )
+ }
}
+ // book keeping static/dynamic class merging.
+ let dynamicClassBinding: CallExpression | undefined = undefined
+ let staticClassBinding: string | undefined = undefined
+ // all style bindings are converted to dynamic by transformStyle.
+ // but we need to make sure to merge them.
+ let dynamicStyleBinding: CallExpression | undefined = undefined
+
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
// special cases with children override
rawChildren = prop.exp
} else if (prop.name === 'text' && prop.exp) {
node.children = [createInterpolation(prop.exp, prop.loc)]
- } else if (
- // v-bind:value on textarea
- node.tag === 'textarea' &&
- prop.name === 'bind' &&
- prop.exp &&
- prop.arg &&
- prop.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
- prop.arg.isStatic &&
- prop.arg.content === 'value'
- ) {
- node.children = [createInterpolation(prop.exp, prop.loc)]
- // TODO handle <textrea> with dynamic v-bind
- } else if (!hasDynamicVBind) {
+ } else if (prop.name === 'slot') {
+ context.onError(
+ createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
+ )
+ } else if (isTextareaWithValue(node, prop) && prop.exp) {
+ if (!hasDynamicVBind) {
+ node.children = [createInterpolation(prop.exp, prop.loc)]
+ } else {
+ // TODO handle <textrea> with dynamic v-bind
+ }
+ } else {
// Directive transforms.
- const directiveTransform = context.ssrDirectiveTransforms[prop.name]
- if (directiveTransform) {
+ const directiveTransform = context.directiveTransforms[prop.name]
+ if (!directiveTransform) {
+ // no corresponding ssr directive transform found.
+ context.onError(
+ createSSRCompilerError(
+ SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
+ prop.loc
+ )
+ )
+ } else if (!hasDynamicVBind) {
const { props } = directiveTransform(prop, node, context)
for (let j = 0; j < props.length; j++) {
const { key, value } = props[j]
// static key attr
if (attrName === 'class') {
openTag.push(
- createCallExpression(context.helper(SSR_RENDER_CLASS), [
- value
- ])
+ (dynamicClassBinding = createCallExpression(
+ context.helper(SSR_RENDER_CLASS),
+ [value]
+ ))
)
} else if (attrName === 'style') {
- openTag.push(
- createCallExpression(context.helper(SSR_RENDER_STYLE), [
- value
- ])
- )
+ if (dynamicStyleBinding) {
+ // already has style binding, merge into it.
+ mergeCall(dynamicStyleBinding, value)
+ } else {
+ openTag.push(
+ (dynamicStyleBinding = createCallExpression(
+ context.helper(SSR_RENDER_STYLE),
+ [value]
+ ))
+ )
+ }
} else if (isBooleanAttr(attrName)) {
openTag.push(
createConditionalExpression(
)
}
}
- } else {
- // no corresponding ssr directive transform found.
- context.onError(
- createSSRCompilerError(
- SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
- prop.loc
- )
- )
}
}
} else {
rawChildren = escapeHtml(prop.value.content)
} else if (!hasDynamicVBind) {
// static prop
+ if (prop.name === 'class' && prop.value) {
+ staticClassBinding = JSON.stringify(prop.value.content)
+ }
openTag.push(
` ${prop.name}` +
(prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
}
}
+ // handle co-existence of dynamic + static class bindings
+ if (dynamicClassBinding && staticClassBinding) {
+ mergeCall(dynamicClassBinding, staticClassBinding)
+ removeStaticBinding(openTag, 'class')
+ }
+
openTag.push(`>`)
if (rawChildren) {
openTag.push(rawChildren)
}
}
}
+
+function isTextareaWithValue(
+ node: PlainElementNode,
+ prop: DirectiveNode
+): boolean {
+ return !!(
+ node.tag === 'textarea' &&
+ prop.name === 'bind' &&
+ prop.arg &&
+ prop.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
+ prop.arg.isStatic &&
+ prop.arg.content === 'value'
+ )
+}
+
+function mergeCall(call: CallExpression, arg: string | JSChildNode) {
+ const existing = call.arguments[0] as ExpressionNode
+ call.arguments[0] = createArrayExpression([existing, arg])
+}
+
+function removeStaticBinding(
+ tag: TemplateLiteral['elements'],
+ binding: string
+) {
+ const i = tag.findIndex(
+ e => typeof e === 'string' && e.startsWith(` ${binding}=`)
+ )
+ if (i > -1) {
+ tag.splice(i, 1)
+ }
+}