feat(runtime-vapor): component attrs (#124)

This commit is contained in:
ubugeeei 2024-02-10 14:07:13 +09:00 committed by GitHub
parent ab1121e512
commit 52311fa7ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 153 additions and 54 deletions

View File

@ -1,4 +1,4 @@
import { type Data, isFunction } from '@vue/shared'
import type { Data } from '@vue/shared'
import {
type ComponentInternalInstance,
type ObjectComponent,
@ -24,13 +24,7 @@ export function makeRender<Component = ObjectComponent | SetupFn>(
})
const define = (comp: Component) => {
const component = defineComponent(
isFunction(comp)
? {
setup: comp,
}
: comp,
)
const component = defineComponent(comp)
let instance: ComponentInternalInstance
const render = (
props: Data = {},

View File

@ -22,13 +22,14 @@ const define = makeRender<any>()
describe('component props (vapor)', () => {
test('stateful', () => {
let props: any
// TODO: attrs
let attrs: any
const { render } = define({
props: ['fooBar', 'barBaz'],
render() {
const instance = getCurrentInstance()!
props = instance.props
attrs = instance.attrs
},
})
@ -36,33 +37,57 @@ describe('component props (vapor)', () => {
get fooBar() {
return 1
},
get bar() {
return 2
},
})
expect(props.fooBar).toEqual(1)
expect(attrs.bar).toEqual(2)
// test passing kebab-case and resolving to camelCase
render({
get ['foo-bar']() {
return 2
},
get bar() {
return 3
},
get baz() {
return 4
},
})
expect(props.fooBar).toEqual(2)
expect(attrs.bar).toEqual(3)
expect(attrs.baz).toEqual(4)
// test updating kebab-case should not delete it (#955)
render({
get ['foo-bar']() {
return 3
},
get bar() {
return 3
},
get baz() {
return 4
},
get barBaz() {
return 5
},
})
expect(props.fooBar).toEqual(3)
expect(props.barBaz).toEqual(5)
expect(attrs.bar).toEqual(3)
expect(attrs.baz).toEqual(4)
render({})
render({
get qux() {
return 5
},
})
expect(props.fooBar).toBeUndefined()
expect(props.barBaz).toBeUndefined()
// expect(props.qux).toEqual(5) // TODO: attrs
expect(attrs.qux).toEqual(5)
})
test.todo('stateful with setup', () => {
@ -71,15 +96,62 @@ describe('component props (vapor)', () => {
test('functional with declaration', () => {
let props: any
// TODO: attrs
let attrs: any
const { component: Comp, render } = define((_props: any) => {
const instance = getCurrentInstance()!
props = instance.props
attrs = instance.attrs
return {}
})
Comp.props = ['foo']
Comp.render = (() => {}) as any
render({
get foo() {
return 1
},
get bar() {
return 2
},
})
expect(props.foo).toEqual(1)
expect(attrs.bar).toEqual(2)
render({
get foo() {
return 2
},
get bar() {
return 3
},
get baz() {
return 4
},
})
expect(props.foo).toEqual(2)
expect(attrs.bar).toEqual(3)
expect(attrs.baz).toEqual(4)
render({
get qux() {
return 5
},
})
expect(props.foo).toBeUndefined()
expect(attrs.qux).toEqual(5)
})
// FIXME:
test('functional without declaration', () => {
let props: any
let attrs: any
const { render } = define((_props: any, { attrs: _attrs }: any) => {
const instance = getCurrentInstance()!
props = instance.props
attrs = instance.attrs
return {}
})
render({
get foo() {
@ -87,6 +159,7 @@ describe('component props (vapor)', () => {
},
})
expect(props.foo).toEqual(1)
expect(attrs.foo).toEqual(1)
render({
get foo() {
@ -94,36 +167,7 @@ describe('component props (vapor)', () => {
},
})
expect(props.foo).toEqual(2)
render({})
expect(props.foo).toBeUndefined()
})
test('functional without declaration', () => {
let props: any
// TODO: attrs
const { component: Comp, render } = define((_props: any) => {
const instance = getCurrentInstance()!
props = instance.props
return {}
})
Comp.props = undefined as any
Comp.render = (() => {}) as any
render({
get foo() {
return 1
},
})
expect(props.foo).toBeUndefined()
render({
get foo() {
return 2
},
})
expect(props.foo).toBeUndefined()
expect(attrs.foo).toEqual(2)
})
test('boolean casting', () => {
@ -490,8 +534,34 @@ describe('component props (vapor)', () => {
})
// #5016
test.todo('handling attr with undefined value', () => {
// TODO: attrs
test('handling attr with undefined value', () => {
const { render, host } = define({
render() {
const instance = getCurrentInstance()!
const t0 = template('<div></div>')
const n0 = t0()
const n1 = children(n0, 0)
watchEffect(() => {
setText(
n1,
JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
)
})
return n0
},
})
let attrs: any = {
get foo() {
return undefined
},
}
render(attrs)
expect(host.innerHTML).toBe(
`<div>${JSON.stringify(attrs) + Object.keys(attrs)}</div>`,
)
})
// #6915

View File

@ -58,6 +58,7 @@ export interface ComponentInternalInstance {
// state
props: Data
attrs: Data
setupState: Data
emit: EmitFn
emitted: Record<string, boolean> | null
@ -179,6 +180,7 @@ export const createComponentInstance = (
// state
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
refs: EMPTY_OBJ,
metadata: new WeakMap(),

View File

@ -19,6 +19,7 @@ import {
type ComponentInternalInstance,
setCurrentInstance,
} from './component'
import { isEmitListener } from './componentEmits'
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
@ -74,10 +75,13 @@ export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: boolean,
) {
const props: Data = {}
const attrs: Data = {}
const [options, needCastKeys] = instance.propsOptions
let hasAttrsChanged = false
let rawCastValues: Data | undefined
if (rawProps) {
for (let key in rawProps) {
@ -96,6 +100,7 @@ export function initProps(
get() {
return valueGetter()
},
enumerable: true,
})
} else {
// NOTE: must getter
@ -105,10 +110,22 @@ export function initProps(
get() {
return valueGetter()
},
enumerable: true,
})
}
} else {
// TODO:
} else if (!isEmitListener(instance.emitsOptions, key)) {
// if (!(key in attrs) || value !== attrs[key]) {
if (!(key in attrs)) {
// NOTE: must getter
// attrs[key] = value
Object.defineProperty(attrs, key, {
get() {
return valueGetter()
},
enumerable: true,
})
hasAttrsChanged = true
}
}
}
}
@ -148,7 +165,18 @@ export function initProps(
validateProps(rawProps || {}, props, instance)
}
instance.props = shallowReactive(props)
if (isStateful) {
instance.props = shallowReactive(props)
} else {
if (instance.propsOptions === EMPTY_ARR) {
instance.props = attrs
} else {
instance.props = props
}
}
instance.attrs = attrs
return hasAttrsChanged
}
function resolvePropValue(

View File

@ -1,5 +1,11 @@
import { proxyRefs } from '@vue/reactivity'
import { type Data, invokeArrayFns, isArray, isObject } from '@vue/shared'
import {
type Data,
invokeArrayFns,
isArray,
isFunction,
isObject,
} from '@vue/shared'
import {
type Component,
type ComponentInternalInstance,
@ -28,7 +34,7 @@ export function render(
container: string | ParentNode,
): ComponentInternalInstance {
const instance = createComponentInstance(comp, props)
initProps(instance, props)
initProps(instance, props, !isFunction(instance.component))
return mountComponent(instance, (container = normalizeContainer(container)))
}
@ -46,11 +52,10 @@ export function mountComponent(
const reset = setCurrentInstance(instance)
const block = instance.scope.run(() => {
const { component, props, emit } = instance
const ctx = { expose: () => {}, emit }
const { component, props, emit, attrs } = instance
const ctx = { expose: () => {}, emit, attrs }
const setupFn =
typeof component === 'function' ? component : component.setup
const setupFn = isFunction(component) ? component : component.setup
const stateOrNode = setupFn && setupFn(props, ctx)
let block: Block | undefined