diff --git a/packages/compiler-vapor/__tests__/generators/__snapshots__/component.spec.ts.snap b/packages/compiler-vapor/__tests__/generators/__snapshots__/component.spec.ts.snap
new file mode 100644
index 000000000..999526e70
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/generators/__snapshots__/component.spec.ts.snap
@@ -0,0 +1,44 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`generate component > generate multi root component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor';
+const t0 = _template("123")
+
+export function render(_ctx) {
+ const n1 = t0()
+ const n0 = _createComponent(_resolveComponent("Comp"))
+ return [n0, n1]
+}"
+`;
+
+exports[`generate component > generate single root component (with props) 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const n0 = _createComponent(_resolveComponent("Comp"), [{
+ foo: () => (foo)
+ }], true)
+ return n0
+}"
+`;
+
+exports[`generate component > generate single root component (without props) 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const n0 = _createComponent(_resolveComponent("Comp"), null, true)
+ return n0
+}"
+`;
+
+exports[`generate component > should not generate withAttrs if component is not the root of the template 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent, insert as _insert, template as _template } from 'vue/vapor';
+const t0 = _template("
")
+
+export function render(_ctx) {
+ const n1 = t0()
+ const n0 = _createComponent(_resolveComponent("Comp"))
+ _insert(n0, n1)
+ return n1
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/generators/component.spec.ts b/packages/compiler-vapor/__tests__/generators/component.spec.ts
new file mode 100644
index 000000000..9d6ded902
--- /dev/null
+++ b/packages/compiler-vapor/__tests__/generators/component.spec.ts
@@ -0,0 +1,23 @@
+import { compile } from '@vue/compiler-vapor'
+
+describe('generate component', () => {
+ test('generate single root component (without props)', () => {
+ const { code } = compile(``)
+ expect(code).toMatchSnapshot()
+ })
+
+ test('generate single root component (with props)', () => {
+ const { code } = compile(``)
+ expect(code).toMatchSnapshot()
+ })
+
+ test('generate multi root component', () => {
+ const { code } = compile(`123`)
+ expect(code).toMatchSnapshot()
+ })
+
+ test('should not generate withAttrs if component is not the root of the template', () => {
+ const { code } = compile(`
`)
+ expect(code).toMatchSnapshot()
+ })
+})
diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts
index ad54d884f..80550bf6f 100644
--- a/packages/compiler-vapor/src/generators/component.ts
+++ b/packages/compiler-vapor/src/generators/component.ts
@@ -22,10 +22,18 @@ export function genCreateComponent(
? genCall(vaporHelper('resolveComponent'), JSON.stringify(oper.tag))
: [oper.tag]
+ const isRoot = oper.root
+ const props = genProps()
+
return [
NEWLINE,
`const n${oper.id} = `,
- ...genCall(vaporHelper('createComponent'), tag, genProps()),
+ ...genCall(
+ vaporHelper('createComponent'),
+ tag,
+ props || (isRoot ? 'null' : false),
+ isRoot && 'true',
+ ),
]
function genProps() {
diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts
index 848e98014..db0ff7e3e 100644
--- a/packages/compiler-vapor/src/ir.ts
+++ b/packages/compiler-vapor/src/ir.ts
@@ -182,6 +182,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
// TODO slots
resolve: boolean
+ root: boolean
}
export type IRNode = OperationNode | RootIRNode
diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts
index 5e80749e0..677ac85aa 100644
--- a/packages/compiler-vapor/src/transforms/transformElement.ts
+++ b/packages/compiler-vapor/src/transforms/transformElement.ts
@@ -64,6 +64,8 @@ function transformComponentElement(
const { bindingMetadata } = context.options
const resolve = !bindingMetadata[tag]
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
+ const root =
+ context.root === context.parent && context.parent.node.children.length === 1
context.registerOperation({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
@@ -71,6 +73,7 @@ function transformComponentElement(
tag,
props: propsResult[0] ? propsResult[1] : [propsResult[1]],
resolve,
+ root,
})
}
diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
new file mode 100644
index 000000000..8a1970eec
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
@@ -0,0 +1,163 @@
+import {
+ createComponent,
+ getCurrentInstance,
+ nextTick,
+ ref,
+ setText,
+ template,
+ watchEffect,
+} from '../src'
+import { setCurrentInstance } from '../src/component'
+import { makeRender } from './_utils'
+
+const define = makeRender()
+
+describe('attribute fallthrough', () => {
+ it('should allow attrs to fallthrough', async () => {
+ const t0 = template('')
+ const { component: Child } = define({
+ props: ['foo'],
+ render() {
+ const instance = getCurrentInstance()!
+ const n0 = t0()
+ watchEffect(() => setText(n0, instance.props.foo))
+ return n0
+ },
+ })
+
+ const foo = ref(1)
+ const id = ref('a')
+ const { instance, host } = define({
+ setup() {
+ return { foo, id }
+ },
+ render(_ctx: Record
) {
+ return createComponent(
+ Child,
+ [
+ {
+ foo: () => _ctx.foo,
+ id: () => _ctx.id,
+ },
+ ],
+ true,
+ )
+ },
+ }).render()
+ const reset = setCurrentInstance(instance)
+ expect(host.innerHTML).toBe('1
')
+
+ foo.value++
+ await nextTick()
+ expect(host.innerHTML).toBe('2
')
+
+ id.value = 'b'
+ await nextTick()
+ expect(host.innerHTML).toBe('2
')
+ reset()
+ })
+
+ it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
+ const t0 = template('')
+ const { component: Child } = define({
+ props: ['foo'],
+ inheritAttrs: false,
+ render() {
+ const instance = getCurrentInstance()!
+ const n0 = t0()
+ watchEffect(() => setText(n0, instance.props.foo))
+ return n0
+ },
+ })
+
+ const foo = ref(1)
+ const id = ref('a')
+ const { instance, host } = define({
+ setup() {
+ return { foo, id }
+ },
+ render(_ctx: Record
) {
+ return createComponent(
+ Child,
+ [
+ {
+ foo: () => _ctx.foo,
+ id: () => _ctx.id,
+ },
+ ],
+ true,
+ )
+ },
+ }).render()
+ const reset = setCurrentInstance(instance)
+ expect(host.innerHTML).toBe('1
')
+
+ foo.value++
+ await nextTick()
+ expect(host.innerHTML).toBe('2
')
+
+ id.value = 'b'
+ await nextTick()
+ expect(host.innerHTML).toBe('2
')
+ reset()
+ })
+
+ it('should pass through attrs in nested single root components', async () => {
+ const t0 = template('')
+ const { component: Grandson } = define({
+ props: ['custom-attr'],
+ render() {
+ const instance = getCurrentInstance()!
+ const n0 = t0()
+ watchEffect(() => setText(n0, instance.attrs.foo))
+ return n0
+ },
+ })
+
+ const { component: Child } = define({
+ render() {
+ const n0 = createComponent(
+ Grandson,
+ [
+ {
+ 'custom-attr': () => 'custom-attr',
+ },
+ ],
+ true,
+ )
+ return n0
+ },
+ })
+
+ const foo = ref(1)
+ const id = ref('a')
+ const { instance, host } = define({
+ setup() {
+ return { foo, id }
+ },
+ render(_ctx: Record
) {
+ return createComponent(
+ Child,
+ [
+ {
+ foo: () => _ctx.foo,
+ id: () => _ctx.id,
+ },
+ ],
+ true,
+ )
+ },
+ }).render()
+ const reset = setCurrentInstance(instance)
+ expect(host.innerHTML).toBe('1
')
+
+ foo.value++
+ await nextTick()
+ expect(host.innerHTML).toBe('2
')
+
+ id.value = 'b'
+ await nextTick()
+ expect(host.innerHTML).toBe('2
')
+ reset()
+ })
+})
diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts
index 12f97274a..ba85905c6 100644
--- a/packages/runtime-vapor/__tests__/componentProps.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts
@@ -238,24 +238,26 @@ describe('component: props', () => {
return { foo, id }
},
render(_ctx: Record) {
- return createComponent(Child, {
- foo: () => _ctx.foo,
- id: () => _ctx.id,
- })
+ return createComponent(
+ Child,
+ {
+ foo: () => _ctx.foo,
+ id: () => _ctx.id,
+ },
+ true,
+ )
},
}).render()
const reset = setCurrentInstance(instance)
- // expect(host.innerHTML).toBe('1
') // TODO: Fallthrough Attributes
- expect(host.innerHTML).toBe('1
')
+ expect(host.innerHTML).toBe('1
')
foo.value++
await nextTick()
- // expect(host.innerHTML).toBe('2
') // TODO: Fallthrough Attributes
- expect(host.innerHTML).toBe('2
')
+ expect(host.innerHTML).toBe('2
')
id.value = 'b'
await nextTick()
- // expect(host.innerHTML).toBe('2
') // TODO: Fallthrough Attributes
+ expect(host.innerHTML).toBe('2
')
reset()
})
@@ -441,6 +443,7 @@ describe('component: props', () => {
// #5016
test('handling attr with undefined value', () => {
const { render, host } = define({
+ inheritAttrs: false,
render() {
const instance = getCurrentInstance()!
const t0 = template('')
diff --git a/packages/runtime-vapor/src/apiCreateComponent.ts b/packages/runtime-vapor/src/apiCreateComponent.ts
index c74783b32..133b40fc5 100644
--- a/packages/runtime-vapor/src/apiCreateComponent.ts
+++ b/packages/runtime-vapor/src/apiCreateComponent.ts
@@ -5,14 +5,22 @@ import {
} from './component'
import { setupComponent } from './apiRender'
import type { RawProps } from './componentProps'
+import { withAttrs } from './componentAttrs'
-export function createComponent(comp: Component, rawProps: RawProps = null) {
+export function createComponent(
+ comp: Component,
+ rawProps: RawProps | null = null,
+ singleRoot: boolean = false,
+) {
const current = currentInstance!
- const instance = createComponentInstance(comp, rawProps)
- setupComponent(instance)
+ const instance = createComponentInstance(
+ comp,
+ singleRoot ? withAttrs(rawProps) : rawProps,
+ )
+ setupComponent(instance, singleRoot)
// register sub-component with current component for lifecycle management
current.comps.add(instance)
- return instance.block
+ return instance
}
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index cefcba1a8..362ac762c 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -4,6 +4,7 @@ import { createComment, createTextNode, insert, remove } from './dom/element'
import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { warn } from './warning'
+import { componentKey } from './component'
interface ForBlock extends Fragment {
scope: EffectScope
@@ -343,6 +344,8 @@ function normalizeAnchor(node: Block): Node {
return node
} else if (isArray(node)) {
return normalizeAnchor(node[0])
+ } else if (componentKey in node) {
+ return normalizeAnchor(node.block!)
} else {
return normalizeAnchor(node.nodes!)
}
diff --git a/packages/runtime-vapor/src/apiRender.ts b/packages/runtime-vapor/src/apiRender.ts
index 94efce891..3ddaeb5cc 100644
--- a/packages/runtime-vapor/src/apiRender.ts
+++ b/packages/runtime-vapor/src/apiRender.ts
@@ -1,21 +1,29 @@
import { isArray, isFunction, isObject } from '@vue/shared'
-import { type ComponentInternalInstance, setCurrentInstance } from './component'
+import {
+ type ComponentInternalInstance,
+ componentKey,
+ setCurrentInstance,
+} from './component'
import { insert, querySelector, remove } from './dom/element'
import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler'
import { proxyRefs } from '@vue/reactivity'
import { invokeLifecycle } from './componentLifecycle'
import { VaporLifecycleHooks } from './apiLifecycle'
+import { fallThroughAttrs } from './componentAttrs'
export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)
-export type Block = Node | Fragment | Block[]
+export type Block = Node | Fragment | ComponentInternalInstance | Block[]
export type Fragment = {
nodes: Block
anchor?: Node
[fragmentKey]: true
}
-export function setupComponent(instance: ComponentInternalInstance): void {
+export function setupComponent(
+ instance: ComponentInternalInstance,
+ singleRoot: boolean = false,
+): void {
const reset = setCurrentInstance(instance)
instance.scope.run(() => {
const { component, props, emit, attrs } = instance
@@ -30,9 +38,10 @@ export function setupComponent(instance: ComponentInternalInstance): void {
stateOrNode &&
(stateOrNode instanceof Node ||
isArray(stateOrNode) ||
- (stateOrNode as any)[fragmentKey])
+ fragmentKey in stateOrNode ||
+ componentKey in stateOrNode)
) {
- block = stateOrNode as Block
+ block = stateOrNode
} else if (isObject(stateOrNode)) {
instance.setupState = proxyRefs(stateOrNode)
}
@@ -47,7 +56,9 @@ export function setupComponent(instance: ComponentInternalInstance): void {
// TODO: warn no template
block = []
}
- return (instance.block = block)
+ instance.block = block
+ if (singleRoot) fallThroughAttrs(instance)
+ return block
})
reset()
}
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index b980cc7ec..79891c744 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -28,6 +28,7 @@ export type FunctionalComponent = SetupFn & Omit
export interface ObjectComponent {
props?: ComponentPropsOptions
+ inheritAttrs?: boolean
emits?: EmitsOptions
setup?: SetupFn
render?(ctx: any): Block
@@ -36,7 +37,10 @@ export interface ObjectComponent {
type LifecycleHook = TFn[] | null
+export const componentKey = Symbol(__DEV__ ? `componentKey` : ``)
+
export interface ComponentInternalInstance {
+ [componentKey]: true
uid: number
vapor: true
@@ -143,6 +147,7 @@ export function createComponentInstance(
rawProps: RawProps | null,
): ComponentInternalInstance {
const instance: ComponentInternalInstance = {
+ [componentKey]: true,
uid: uid++,
vapor: true,
diff --git a/packages/runtime-vapor/src/componentAttrs.ts b/packages/runtime-vapor/src/componentAttrs.ts
index 8eabb0449..74c4aadf8 100644
--- a/packages/runtime-vapor/src/componentAttrs.ts
+++ b/packages/runtime-vapor/src/componentAttrs.ts
@@ -1,6 +1,9 @@
-import { camelize, isFunction } from '@vue/shared'
-import type { ComponentInternalInstance } from './component'
+import { camelize, isArray, isFunction } from '@vue/shared'
+import { type ComponentInternalInstance, currentInstance } from './component'
import { isEmitListener } from './componentEmits'
+import { setDynamicProps } from './dom/prop'
+import type { RawProps } from './componentProps'
+import { renderEffect } from './renderEffect'
export function patchAttrs(instance: ComponentInternalInstance) {
const attrs = instance.attrs
@@ -42,3 +45,26 @@ export function patchAttrs(instance: ComponentInternalInstance) {
}
}
}
+
+export function withAttrs(props: RawProps): RawProps {
+ const instance = currentInstance!
+ if (instance.component.inheritAttrs === false) return props
+ const attrsGetter = () => instance.attrs
+ if (!props) return [attrsGetter]
+ if (isArray(props)) {
+ return [attrsGetter, ...props]
+ }
+ return [attrsGetter, props]
+}
+
+export function fallThroughAttrs(instance: ComponentInternalInstance) {
+ const {
+ block,
+ component: { inheritAttrs },
+ } = instance
+ if (inheritAttrs === false) return
+
+ if (block instanceof Element) {
+ renderEffect(() => setDynamicProps(block, instance.attrs))
+ }
+}
diff --git a/packages/runtime-vapor/src/dom/element.ts b/packages/runtime-vapor/src/dom/element.ts
index 63c515d4a..d7e450871 100644
--- a/packages/runtime-vapor/src/dom/element.ts
+++ b/packages/runtime-vapor/src/dom/element.ts
@@ -1,5 +1,6 @@
import { isArray, toDisplayString } from '@vue/shared'
import type { Block } from '../apiRender'
+import { componentKey } from '../component'
/*! #__NO_SIDE_EFFECTS__ */
export function normalizeBlock(block: Block): Node[] {
@@ -8,6 +9,8 @@ export function normalizeBlock(block: Block): Node[] {
nodes.push(block)
} else if (isArray(block)) {
block.forEach(child => nodes.push(...normalizeBlock(child)))
+ } else if (componentKey in block) {
+ nodes.push(...normalizeBlock(block.block!))
} else if (block) {
nodes.push(...normalizeBlock(block.nodes))
block.anchor && nodes.push(block.anchor)