Skip to content

Commit 38e167c

Browse files
Doctor-wusxzz
andauthored
feat: implement inheritAttrs (#153)
Co-authored-by: 三咲智子 Kevin Deng <[email protected]>
1 parent 6fc5cfb commit 38e167c

File tree

13 files changed

+323
-22
lines changed

13 files changed

+323
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`generate component > generate multi root component 1`] = `
4+
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor';
5+
const t0 = _template("123")
6+
7+
export function render(_ctx) {
8+
const n1 = t0()
9+
const n0 = _createComponent(_resolveComponent("Comp"))
10+
return [n0, n1]
11+
}"
12+
`;
13+
14+
exports[`generate component > generate single root component (with props) 1`] = `
15+
"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
16+
17+
export function render(_ctx) {
18+
const n0 = _createComponent(_resolveComponent("Comp"), [{
19+
foo: () => (foo)
20+
}], true)
21+
return n0
22+
}"
23+
`;
24+
25+
exports[`generate component > generate single root component (without props) 1`] = `
26+
"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
27+
28+
export function render(_ctx) {
29+
const n0 = _createComponent(_resolveComponent("Comp"), null, true)
30+
return n0
31+
}"
32+
`;
33+
34+
exports[`generate component > should not generate withAttrs if component is not the root of the template 1`] = `
35+
"import { resolveComponent as _resolveComponent, createComponent as _createComponent, insert as _insert, template as _template } from 'vue/vapor';
36+
const t0 = _template("<div></div>")
37+
38+
export function render(_ctx) {
39+
const n1 = t0()
40+
const n0 = _createComponent(_resolveComponent("Comp"))
41+
_insert(n0, n1)
42+
return n1
43+
}"
44+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { compile } from '@vue/compiler-vapor'
2+
3+
describe('generate component', () => {
4+
test('generate single root component (without props)', () => {
5+
const { code } = compile(`<Comp/>`)
6+
expect(code).toMatchSnapshot()
7+
})
8+
9+
test('generate single root component (with props)', () => {
10+
const { code } = compile(`<Comp :foo="foo"/>`)
11+
expect(code).toMatchSnapshot()
12+
})
13+
14+
test('generate multi root component', () => {
15+
const { code } = compile(`<Comp/>123`)
16+
expect(code).toMatchSnapshot()
17+
})
18+
19+
test('should not generate withAttrs if component is not the root of the template', () => {
20+
const { code } = compile(`<div><Comp/></div>`)
21+
expect(code).toMatchSnapshot()
22+
})
23+
})

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ export function genCreateComponent(
2222
? genCall(vaporHelper('resolveComponent'), JSON.stringify(oper.tag))
2323
: [oper.tag]
2424

25+
const isRoot = oper.root
26+
const props = genProps()
27+
2528
return [
2629
NEWLINE,
2730
`const n${oper.id} = `,
28-
...genCall(vaporHelper('createComponent'), tag, genProps()),
31+
...genCall(
32+
vaporHelper('createComponent'),
33+
tag,
34+
props || (isRoot ? 'null' : false),
35+
isRoot && 'true',
36+
),
2937
]
3038

3139
function genProps() {

packages/compiler-vapor/src/ir.ts

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
182182
// TODO slots
183183

184184
resolve: boolean
185+
root: boolean
185186
}
186187

187188
export type IRNode = OperationNode | RootIRNode

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

+3
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,16 @@ function transformComponentElement(
6464
const { bindingMetadata } = context.options
6565
const resolve = !bindingMetadata[tag]
6666
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
67+
const root =
68+
context.root === context.parent && context.parent.node.children.length === 1
6769

6870
context.registerOperation({
6971
type: IRNodeTypes.CREATE_COMPONENT_NODE,
7072
id: context.reference(),
7173
tag,
7274
props: propsResult[0] ? propsResult[1] : [propsResult[1]],
7375
resolve,
76+
root,
7477
})
7578
}
7679

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import {
2+
createComponent,
3+
getCurrentInstance,
4+
nextTick,
5+
ref,
6+
setText,
7+
template,
8+
watchEffect,
9+
} from '../src'
10+
import { setCurrentInstance } from '../src/component'
11+
import { makeRender } from './_utils'
12+
13+
const define = makeRender<any>()
14+
15+
describe('attribute fallthrough', () => {
16+
it('should allow attrs to fallthrough', async () => {
17+
const t0 = template('<div>')
18+
const { component: Child } = define({
19+
props: ['foo'],
20+
render() {
21+
const instance = getCurrentInstance()!
22+
const n0 = t0()
23+
watchEffect(() => setText(n0, instance.props.foo))
24+
return n0
25+
},
26+
})
27+
28+
const foo = ref(1)
29+
const id = ref('a')
30+
const { instance, host } = define({
31+
setup() {
32+
return { foo, id }
33+
},
34+
render(_ctx: Record<string, any>) {
35+
return createComponent(
36+
Child,
37+
[
38+
{
39+
foo: () => _ctx.foo,
40+
id: () => _ctx.id,
41+
},
42+
],
43+
true,
44+
)
45+
},
46+
}).render()
47+
const reset = setCurrentInstance(instance)
48+
expect(host.innerHTML).toBe('<div id="a">1</div>')
49+
50+
foo.value++
51+
await nextTick()
52+
expect(host.innerHTML).toBe('<div id="a">2</div>')
53+
54+
id.value = 'b'
55+
await nextTick()
56+
expect(host.innerHTML).toBe('<div id="b">2</div>')
57+
reset()
58+
})
59+
60+
it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
61+
const t0 = template('<div>')
62+
const { component: Child } = define({
63+
props: ['foo'],
64+
inheritAttrs: false,
65+
render() {
66+
const instance = getCurrentInstance()!
67+
const n0 = t0()
68+
watchEffect(() => setText(n0, instance.props.foo))
69+
return n0
70+
},
71+
})
72+
73+
const foo = ref(1)
74+
const id = ref('a')
75+
const { instance, host } = define({
76+
setup() {
77+
return { foo, id }
78+
},
79+
render(_ctx: Record<string, any>) {
80+
return createComponent(
81+
Child,
82+
[
83+
{
84+
foo: () => _ctx.foo,
85+
id: () => _ctx.id,
86+
},
87+
],
88+
true,
89+
)
90+
},
91+
}).render()
92+
const reset = setCurrentInstance(instance)
93+
expect(host.innerHTML).toBe('<div>1</div>')
94+
95+
foo.value++
96+
await nextTick()
97+
expect(host.innerHTML).toBe('<div>2</div>')
98+
99+
id.value = 'b'
100+
await nextTick()
101+
expect(host.innerHTML).toBe('<div>2</div>')
102+
reset()
103+
})
104+
105+
it('should pass through attrs in nested single root components', async () => {
106+
const t0 = template('<div>')
107+
const { component: Grandson } = define({
108+
props: ['custom-attr'],
109+
render() {
110+
const instance = getCurrentInstance()!
111+
const n0 = t0()
112+
watchEffect(() => setText(n0, instance.attrs.foo))
113+
return n0
114+
},
115+
})
116+
117+
const { component: Child } = define({
118+
render() {
119+
const n0 = createComponent(
120+
Grandson,
121+
[
122+
{
123+
'custom-attr': () => 'custom-attr',
124+
},
125+
],
126+
true,
127+
)
128+
return n0
129+
},
130+
})
131+
132+
const foo = ref(1)
133+
const id = ref('a')
134+
const { instance, host } = define({
135+
setup() {
136+
return { foo, id }
137+
},
138+
render(_ctx: Record<string, any>) {
139+
return createComponent(
140+
Child,
141+
[
142+
{
143+
foo: () => _ctx.foo,
144+
id: () => _ctx.id,
145+
},
146+
],
147+
true,
148+
)
149+
},
150+
}).render()
151+
const reset = setCurrentInstance(instance)
152+
expect(host.innerHTML).toBe('<div foo="1" id="a">1</div>')
153+
154+
foo.value++
155+
await nextTick()
156+
expect(host.innerHTML).toBe('<div foo="2" id="a">2</div>')
157+
158+
id.value = 'b'
159+
await nextTick()
160+
expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
161+
reset()
162+
})
163+
})

packages/runtime-vapor/__tests__/componentProps.spec.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -238,24 +238,26 @@ describe('component: props', () => {
238238
return { foo, id }
239239
},
240240
render(_ctx: Record<string, any>) {
241-
return createComponent(Child, {
242-
foo: () => _ctx.foo,
243-
id: () => _ctx.id,
244-
})
241+
return createComponent(
242+
Child,
243+
{
244+
foo: () => _ctx.foo,
245+
id: () => _ctx.id,
246+
},
247+
true,
248+
)
245249
},
246250
}).render()
247251
const reset = setCurrentInstance(instance)
248-
// expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
249-
expect(host.innerHTML).toBe('<div>1</div>')
252+
expect(host.innerHTML).toBe('<div id="a">1</div>')
250253

251254
foo.value++
252255
await nextTick()
253-
// expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
254-
expect(host.innerHTML).toBe('<div>2</div>')
256+
expect(host.innerHTML).toBe('<div id="a">2</div>')
255257

256258
id.value = 'b'
257259
await nextTick()
258-
// expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
260+
expect(host.innerHTML).toBe('<div id="b">2</div>')
259261
reset()
260262
})
261263

@@ -441,6 +443,7 @@ describe('component: props', () => {
441443
// #5016
442444
test('handling attr with undefined value', () => {
443445
const { render, host } = define({
446+
inheritAttrs: false,
444447
render() {
445448
const instance = getCurrentInstance()!
446449
const t0 = template('<div></div>')

packages/runtime-vapor/src/apiCreateComponent.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ import {
55
} from './component'
66
import { setupComponent } from './apiRender'
77
import type { RawProps } from './componentProps'
8+
import { withAttrs } from './componentAttrs'
89

9-
export function createComponent(comp: Component, rawProps: RawProps = null) {
10+
export function createComponent(
11+
comp: Component,
12+
rawProps: RawProps | null = null,
13+
singleRoot: boolean = false,
14+
) {
1015
const current = currentInstance!
11-
const instance = createComponentInstance(comp, rawProps)
12-
setupComponent(instance)
16+
const instance = createComponentInstance(
17+
comp,
18+
singleRoot ? withAttrs(rawProps) : rawProps,
19+
)
20+
setupComponent(instance, singleRoot)
1321

1422
// register sub-component with current component for lifecycle management
1523
current.comps.add(instance)
1624

17-
return instance.block
25+
return instance
1826
}

packages/runtime-vapor/src/apiCreateFor.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createComment, createTextNode, insert, remove } from './dom/element'
44
import { renderEffect } from './renderEffect'
55
import { type Block, type Fragment, fragmentKey } from './apiRender'
66
import { warn } from './warning'
7+
import { componentKey } from './component'
78

89
interface ForBlock extends Fragment {
910
scope: EffectScope
@@ -343,6 +344,8 @@ function normalizeAnchor(node: Block): Node {
343344
return node
344345
} else if (isArray(node)) {
345346
return normalizeAnchor(node[0])
347+
} else if (componentKey in node) {
348+
return normalizeAnchor(node.block!)
346349
} else {
347350
return normalizeAnchor(node.nodes!)
348351
}

0 commit comments

Comments
 (0)