Skip to content

Commit 5f92ff8

Browse files
committed
feat(vapor): dynamic component
1 parent fab9917 commit 5f92ff8

File tree

7 files changed

+254
-22
lines changed

7 files changed

+254
-22
lines changed

packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap

+48
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,54 @@ export function render(_ctx) {
213213
}"
214214
`;
215215

216+
exports[`compiler: element transform > dynamic component > capitalized version w/ static binding 1`] = `
217+
"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
218+
219+
export function render(_ctx) {
220+
const n0 = _createComponent(_resolveDynamicComponent("foo"), null, null, true)
221+
return n0
222+
}"
223+
`;
224+
225+
exports[`compiler: element transform > dynamic component > dynamic binding 1`] = `
226+
"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
227+
228+
export function render(_ctx) {
229+
const n0 = _createComponent(_resolveDynamicComponent(_ctx.foo), null, null, true)
230+
return n0
231+
}"
232+
`;
233+
234+
exports[`compiler: element transform > dynamic component > dynamic binding shorthand 1`] = `
235+
"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
236+
237+
export function render(_ctx) {
238+
const n0 = _createComponent(_resolveDynamicComponent(_ctx.is), null, null, true)
239+
return n0
240+
}"
241+
`;
242+
243+
exports[`compiler: element transform > dynamic component > normal component with is prop 1`] = `
244+
"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
245+
246+
export function render(_ctx) {
247+
const _component_custom_input = _resolveComponent("custom-input")
248+
const n0 = _createComponent(_component_custom_input, [
249+
{ is: () => ("foo") }
250+
], null, true)
251+
return n0
252+
}"
253+
`;
254+
255+
exports[`compiler: element transform > dynamic component > static binding 1`] = `
256+
"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
257+
258+
export function render(_ctx) {
259+
const n0 = _createComponent(_resolveDynamicComponent("foo"), null, null, true)
260+
return n0
261+
}"
262+
`;
263+
216264
exports[`compiler: element transform > empty template 1`] = `
217265
"
218266
export function render(_ctx) {

packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts

+111
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,117 @@ describe('compiler: element transform', () => {
423423
})
424424
})
425425

426+
describe('dynamic component', () => {
427+
test('static binding', () => {
428+
const { code, ir, vaporHelpers } = compileWithElementTransform(
429+
`<component is="foo" />`,
430+
)
431+
expect(code).toMatchSnapshot()
432+
expect(vaporHelpers).toContain('resolveDynamicComponent')
433+
expect(ir.block.operation).toMatchObject([
434+
{
435+
type: IRNodeTypes.CREATE_COMPONENT_NODE,
436+
tag: 'component',
437+
asset: true,
438+
root: true,
439+
props: [[]],
440+
dynamic: {
441+
type: NodeTypes.SIMPLE_EXPRESSION,
442+
content: 'foo',
443+
isStatic: true,
444+
},
445+
},
446+
])
447+
})
448+
449+
test('capitalized version w/ static binding', () => {
450+
const { code, ir, vaporHelpers } = compileWithElementTransform(
451+
`<Component is="foo" />`,
452+
)
453+
expect(code).toMatchSnapshot()
454+
expect(vaporHelpers).toContain('resolveDynamicComponent')
455+
expect(ir.block.operation).toMatchObject([
456+
{
457+
type: IRNodeTypes.CREATE_COMPONENT_NODE,
458+
tag: 'Component',
459+
asset: true,
460+
root: true,
461+
props: [[]],
462+
dynamic: {
463+
type: NodeTypes.SIMPLE_EXPRESSION,
464+
content: 'foo',
465+
isStatic: true,
466+
},
467+
},
468+
])
469+
})
470+
471+
test('dynamic binding', () => {
472+
const { code, ir, vaporHelpers } = compileWithElementTransform(
473+
`<component :is="foo" />`,
474+
)
475+
expect(code).toMatchSnapshot()
476+
expect(vaporHelpers).toContain('resolveDynamicComponent')
477+
expect(ir.block.operation).toMatchObject([
478+
{
479+
type: IRNodeTypes.CREATE_COMPONENT_NODE,
480+
tag: 'component',
481+
asset: true,
482+
root: true,
483+
props: [[]],
484+
dynamic: {
485+
type: NodeTypes.SIMPLE_EXPRESSION,
486+
content: 'foo',
487+
isStatic: false,
488+
},
489+
},
490+
])
491+
})
492+
493+
test('dynamic binding shorthand', () => {
494+
const { code, ir, vaporHelpers } =
495+
compileWithElementTransform(`<component :is />`)
496+
expect(code).toMatchSnapshot()
497+
expect(vaporHelpers).toContain('resolveDynamicComponent')
498+
expect(ir.block.operation).toMatchObject([
499+
{
500+
type: IRNodeTypes.CREATE_COMPONENT_NODE,
501+
tag: 'component',
502+
asset: true,
503+
root: true,
504+
props: [[]],
505+
dynamic: {
506+
type: NodeTypes.SIMPLE_EXPRESSION,
507+
content: 'is',
508+
isStatic: false,
509+
},
510+
},
511+
])
512+
})
513+
514+
// #3934
515+
test('normal component with is prop', () => {
516+
const { code, ir, vaporHelpers } = compileWithElementTransform(
517+
`<custom-input is="foo" />`,
518+
{
519+
isNativeTag: () => false,
520+
},
521+
)
522+
expect(code).toMatchSnapshot()
523+
expect(vaporHelpers).toContain('resolveComponent')
524+
expect(vaporHelpers).not.toContain('resolveDynamicComponent')
525+
expect(ir.block.operation).toMatchObject([
526+
{
527+
type: IRNodeTypes.CREATE_COMPONENT_NODE,
528+
tag: 'custom-input',
529+
asset: true,
530+
root: true,
531+
props: [[{ key: { content: 'is' }, values: [{ content: 'foo' }] }]],
532+
},
533+
])
534+
})
535+
})
536+
426537
test('static props', () => {
427538
const { code, ir } = compileWithElementTransform(
428539
`<div id="foo" class="bar" />`,

packages/compiler-vapor/src/generators/component.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@ export function genCreateComponent(
6565
]
6666

6767
function genTag() {
68-
if (oper.asset) {
68+
if (oper.dynamic) {
69+
return genCall(
70+
vaporHelper('resolveDynamicComponent'),
71+
genExpression(oper.dynamic, context),
72+
)
73+
} else if (oper.asset) {
6974
return toValidAssetId(oper.tag, 'component')
7075
} else {
7176
return genExpression(

packages/compiler-vapor/src/ir/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
194194
asset: boolean
195195
root: boolean
196196
once: boolean
197+
dynamic?: SimpleExpressionNode
197198
}
198199

199200
export interface DeclareOldRefIRNode extends BaseIRNode {

packages/compiler-vapor/src/transforms/transformElement.ts

+69-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { isValidHTMLNesting } from '../html-nesting'
22
import {
33
type AttributeNode,
4+
type ComponentNode,
45
type ElementNode,
56
ElementTypes,
67
ErrorCodes,
78
NodeTypes,
9+
type PlainElementNode,
810
type SimpleExpressionNode,
911
createCompilerError,
1012
createSimpleExpression,
13+
isStaticArgOf,
1114
} from '@vue/compiler-dom'
1215
import {
1316
camelize,
@@ -33,6 +36,7 @@ import {
3336
type VaporDirectiveNode,
3437
} from '../ir'
3538
import { EMPTY_EXPRESSION } from './utils'
39+
import { findProp } from '../utils'
3640

3741
export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
3842
// the leading comma is intentional so empty string "" is also included
@@ -51,46 +55,56 @@ export const transformElement: NodeTransform = (node, context) => {
5155
)
5256
return
5357

54-
const { tag, tagType } = node
55-
const isComponent = tagType === ElementTypes.COMPONENT
58+
const isComponent = node.tagType === ElementTypes.COMPONENT
59+
const isDynamicComponent = isComponentTag(node.tag)
5660
const propsResult = buildProps(
5761
node,
5862
context as TransformContext<ElementNode>,
5963
isComponent,
64+
isDynamicComponent,
6065
)
6166

6267
;(isComponent ? transformComponentElement : transformNativeElement)(
63-
tag,
68+
node as any,
6469
propsResult,
6570
context as TransformContext<ElementNode>,
71+
isDynamicComponent,
6672
)
6773
}
6874
}
6975

7076
function transformComponentElement(
71-
tag: string,
77+
node: ComponentNode,
7278
propsResult: PropsResult,
7379
context: TransformContext,
80+
isDynamicComponent: boolean,
7481
) {
75-
let asset = true
82+
const dynamicComponent = isDynamicComponent
83+
? resolveDynamicComponent(node)
84+
: undefined
7685

77-
const fromSetup = resolveSetupReference(tag, context)
78-
if (fromSetup) {
79-
tag = fromSetup
80-
asset = false
81-
}
86+
let { tag } = node
87+
let asset = true
8288

83-
const dotIndex = tag.indexOf('.')
84-
if (dotIndex > 0) {
85-
const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
86-
if (ns) {
87-
tag = ns + tag.slice(dotIndex)
89+
if (!dynamicComponent) {
90+
const fromSetup = resolveSetupReference(tag, context)
91+
if (fromSetup) {
92+
tag = fromSetup
8893
asset = false
8994
}
90-
}
9195

92-
if (asset) {
93-
context.component.add(tag)
96+
const dotIndex = tag.indexOf('.')
97+
if (dotIndex > 0) {
98+
const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
99+
if (ns) {
100+
tag = ns + tag.slice(dotIndex)
101+
asset = false
102+
}
103+
}
104+
105+
if (asset) {
106+
context.component.add(tag)
107+
}
94108
}
95109

96110
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
@@ -106,10 +120,28 @@ function transformComponentElement(
106120
root,
107121
slots: [...context.slots],
108122
once: context.inVOnce,
123+
dynamic: dynamicComponent,
109124
})
110125
context.slots = []
111126
}
112127

128+
function resolveDynamicComponent(node: ComponentNode) {
129+
const isProp = findProp(node, 'is', false, true /* allow empty */)
130+
if (!isProp) return
131+
132+
if (isProp.type === NodeTypes.ATTRIBUTE) {
133+
return isProp.value && createSimpleExpression(isProp.value.content, true)
134+
} else {
135+
return (
136+
isProp.exp ||
137+
// #10469 handle :is shorthand
138+
extend(createSimpleExpression(`is`, false, isProp.arg!.loc), {
139+
ast: null,
140+
})
141+
)
142+
}
143+
}
144+
113145
function resolveSetupReference(name: string, context: TransformContext) {
114146
const bindings = context.options.bindingMetadata
115147
if (!bindings || bindings.__isScriptSetup === false) {
@@ -128,10 +160,11 @@ function resolveSetupReference(name: string, context: TransformContext) {
128160
}
129161

130162
function transformNativeElement(
131-
tag: string,
163+
node: PlainElementNode,
132164
propsResult: PropsResult,
133165
context: TransformContext<ElementNode>,
134166
) {
167+
const { tag } = node
135168
const { scopeId } = context.options
136169

137170
let template = ''
@@ -189,6 +222,7 @@ export function buildProps(
189222
node: ElementNode,
190223
context: TransformContext<ElementNode>,
191224
isComponent: boolean,
225+
isDynamicComponent: boolean,
192226
): PropsResult {
193227
const props = node.props as (VaporDirectiveNode | AttributeNode)[]
194228
if (props.length === 0) return [false, []]
@@ -252,6 +286,18 @@ export function buildProps(
252286
}
253287
}
254288

289+
// exclude `is` prop for <component>
290+
if (
291+
(isDynamicComponent &&
292+
prop.type === NodeTypes.ATTRIBUTE &&
293+
prop.name === 'is') ||
294+
(prop.type === NodeTypes.DIRECTIVE &&
295+
prop.name === 'bind' &&
296+
isStaticArgOf(prop.arg, 'is'))
297+
) {
298+
continue
299+
}
300+
255301
const result = transformProp(prop, node, context)
256302
if (result) {
257303
dynamicExpr.push(result.key, result.value)
@@ -362,3 +408,7 @@ function mergePropValues(existing: IRProp, incoming: IRProp) {
362408
const newValues = incoming.values
363409
existing.values.push(...newValues)
364410
}
411+
412+
function isComponentTag(tag: string) {
413+
return tag === 'component' || tag === 'Component'
414+
}

packages/runtime-vapor/src/helpers/resolveAssets.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { camelize, capitalize } from '@vue/shared'
1+
import { camelize, capitalize, isString } from '@vue/shared'
22
import { type Directive, warn } from '..'
33
import { type Component, currentInstance } from '../component'
44
import { getComponentName } from '../component'
@@ -79,3 +79,16 @@ function resolve(registry: Record<string, any> | undefined, name: string) {
7979
registry[capitalize(camelize(name))])
8080
)
8181
}
82+
83+
/**
84+
* @private
85+
*/
86+
export function resolveDynamicComponent(
87+
component: string | Component,
88+
): string | Component {
89+
if (isString(component)) {
90+
return resolveAsset(COMPONENTS, component, false) || component
91+
} else {
92+
return component
93+
}
94+
}

0 commit comments

Comments
 (0)