mirror of https://github.com/vuejs/core.git
feat: implement inheritAttrs (#153)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
6fc5cfbc65
commit
38e167ceb8
|
@ -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("<div></div>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n1 = t0()
|
||||
const n0 = _createComponent(_resolveComponent("Comp"))
|
||||
_insert(n0, n1)
|
||||
return n1
|
||||
}"
|
||||
`;
|
|
@ -0,0 +1,23 @@
|
|||
import { compile } from '@vue/compiler-vapor'
|
||||
|
||||
describe('generate component', () => {
|
||||
test('generate single root component (without props)', () => {
|
||||
const { code } = compile(`<Comp/>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('generate single root component (with props)', () => {
|
||||
const { code } = compile(`<Comp :foo="foo"/>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('generate multi root component', () => {
|
||||
const { code } = compile(`<Comp/>123`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should not generate withAttrs if component is not the root of the template', () => {
|
||||
const { code } = compile(`<div><Comp/></div>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -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() {
|
||||
|
|
|
@ -182,6 +182,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
|
|||
// TODO slots
|
||||
|
||||
resolve: boolean
|
||||
root: boolean
|
||||
}
|
||||
|
||||
export type IRNode = OperationNode | RootIRNode
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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<any>()
|
||||
|
||||
describe('attribute fallthrough', () => {
|
||||
it('should allow attrs to fallthrough', async () => {
|
||||
const t0 = template('<div>')
|
||||
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<string, any>) {
|
||||
return createComponent(
|
||||
Child,
|
||||
[
|
||||
{
|
||||
foo: () => _ctx.foo,
|
||||
id: () => _ctx.id,
|
||||
},
|
||||
],
|
||||
true,
|
||||
)
|
||||
},
|
||||
}).render()
|
||||
const reset = setCurrentInstance(instance)
|
||||
expect(host.innerHTML).toBe('<div id="a">1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div id="a">2</div>')
|
||||
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div id="b">2</div>')
|
||||
reset()
|
||||
})
|
||||
|
||||
it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
|
||||
const t0 = template('<div>')
|
||||
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<string, any>) {
|
||||
return createComponent(
|
||||
Child,
|
||||
[
|
||||
{
|
||||
foo: () => _ctx.foo,
|
||||
id: () => _ctx.id,
|
||||
},
|
||||
],
|
||||
true,
|
||||
)
|
||||
},
|
||||
}).render()
|
||||
const reset = setCurrentInstance(instance)
|
||||
expect(host.innerHTML).toBe('<div>1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div>2</div>')
|
||||
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div>2</div>')
|
||||
reset()
|
||||
})
|
||||
|
||||
it('should pass through attrs in nested single root components', async () => {
|
||||
const t0 = template('<div>')
|
||||
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<string, any>) {
|
||||
return createComponent(
|
||||
Child,
|
||||
[
|
||||
{
|
||||
foo: () => _ctx.foo,
|
||||
id: () => _ctx.id,
|
||||
},
|
||||
],
|
||||
true,
|
||||
)
|
||||
},
|
||||
}).render()
|
||||
const reset = setCurrentInstance(instance)
|
||||
expect(host.innerHTML).toBe('<div foo="1" id="a">1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div foo="2" id="a">2</div>')
|
||||
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
|
||||
reset()
|
||||
})
|
||||
})
|
|
@ -238,24 +238,26 @@ describe('component: props', () => {
|
|||
return { foo, id }
|
||||
},
|
||||
render(_ctx: Record<string, any>) {
|
||||
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('<div id="a">1</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div>1</div>')
|
||||
expect(host.innerHTML).toBe('<div id="a">1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
// expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div>2</div>')
|
||||
expect(host.innerHTML).toBe('<div id="a">2</div>')
|
||||
|
||||
id.value = 'b'
|
||||
await nextTick()
|
||||
// expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div id="b">2</div>')
|
||||
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('<div></div>')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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!)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export type FunctionalComponent = SetupFn & Omit<ObjectComponent, 'setup'>
|
|||
|
||||
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 = Function> = 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,
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue