mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): component attrs (#124)
This commit is contained in:
parent
ab1121e512
commit
52311fa7ae
|
@ -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 = {},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue