Skip to content

Commit 52311fa

Browse files
authored
feat(runtime-vapor): component attrs (#124)
1 parent ab1121e commit 52311fa

File tree

5 files changed

+130
-31
lines changed

5 files changed

+130
-31
lines changed

packages/runtime-vapor/__tests__/_utils.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Data, isFunction } from '@vue/shared'
1+
import type { Data } from '@vue/shared'
22
import {
33
type ComponentInternalInstance,
44
type ObjectComponent,
@@ -24,13 +24,7 @@ export function makeRender<Component = ObjectComponent | SetupFn>(
2424
})
2525

2626
const define = (comp: Component) => {
27-
const component = defineComponent(
28-
isFunction(comp)
29-
? {
30-
setup: comp,
31-
}
32-
: comp,
33-
)
27+
const component = defineComponent(comp)
3428
let instance: ComponentInternalInstance
3529
const render = (
3630
props: Data = {},

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

+84-14
Original file line numberDiff line numberDiff line change
@@ -22,47 +22,72 @@ const define = makeRender<any>()
2222
describe('component props (vapor)', () => {
2323
test('stateful', () => {
2424
let props: any
25-
// TODO: attrs
25+
let attrs: any
2626

2727
const { render } = define({
2828
props: ['fooBar', 'barBaz'],
2929
render() {
3030
const instance = getCurrentInstance()!
3131
props = instance.props
32+
attrs = instance.attrs
3233
},
3334
})
3435

3536
render({
3637
get fooBar() {
3738
return 1
3839
},
40+
get bar() {
41+
return 2
42+
},
3943
})
4044
expect(props.fooBar).toEqual(1)
45+
expect(attrs.bar).toEqual(2)
4146

4247
// test passing kebab-case and resolving to camelCase
4348
render({
4449
get ['foo-bar']() {
4550
return 2
4651
},
52+
get bar() {
53+
return 3
54+
},
55+
get baz() {
56+
return 4
57+
},
4758
})
4859
expect(props.fooBar).toEqual(2)
60+
expect(attrs.bar).toEqual(3)
61+
expect(attrs.baz).toEqual(4)
4962

5063
// test updating kebab-case should not delete it (#955)
5164
render({
5265
get ['foo-bar']() {
5366
return 3
5467
},
68+
get bar() {
69+
return 3
70+
},
71+
get baz() {
72+
return 4
73+
},
5574
get barBaz() {
5675
return 5
5776
},
5877
})
5978
expect(props.fooBar).toEqual(3)
6079
expect(props.barBaz).toEqual(5)
80+
expect(attrs.bar).toEqual(3)
81+
expect(attrs.baz).toEqual(4)
6182

62-
render({})
83+
render({
84+
get qux() {
85+
return 5
86+
},
87+
})
6388
expect(props.fooBar).toBeUndefined()
6489
expect(props.barBaz).toBeUndefined()
65-
// expect(props.qux).toEqual(5) // TODO: attrs
90+
expect(attrs.qux).toEqual(5)
6691
})
6792

6893
test.todo('stateful with setup', () => {
@@ -71,59 +96,78 @@ describe('component props (vapor)', () => {
7196

7297
test('functional with declaration', () => {
7398
let props: any
74-
// TODO: attrs
99+
let attrs: any
75100

76101
const { component: Comp, render } = define((_props: any) => {
77102
const instance = getCurrentInstance()!
78103
props = instance.props
104+
attrs = instance.attrs
79105
return {}
80106
})
81107
Comp.props = ['foo']
82-
Comp.render = (() => {}) as any
83108

84109
render({
85110
get foo() {
86111
return 1
87112
},
113+
get bar() {
114+
return 2
115+
},
88116
})
89117
expect(props.foo).toEqual(1)
118+
expect(attrs.bar).toEqual(2)
90119

91120
render({
92121
get foo() {
93122
return 2
94123
},
124+
get bar() {
125+
return 3
126+
},
127+
get baz() {
128+
return 4
129+
},
95130
})
96131
expect(props.foo).toEqual(2)
132+
expect(attrs.bar).toEqual(3)
133+
expect(attrs.baz).toEqual(4)
97134

98-
render({})
135+
render({
136+
get qux() {
137+
return 5
138+
},
139+
})
99140
expect(props.foo).toBeUndefined()
141+
expect(attrs.qux).toEqual(5)
100142
})
101143

144+
// FIXME:
102145
test('functional without declaration', () => {
103146
let props: any
104-
// TODO: attrs
147+
let attrs: any
105148

106-
const { component: Comp, render } = define((_props: any) => {
149+
const { render } = define((_props: any, { attrs: _attrs }: any) => {
107150
const instance = getCurrentInstance()!
108151
props = instance.props
152+
attrs = instance.attrs
109153
return {}
110154
})
111-
Comp.props = undefined as any
112-
Comp.render = (() => {}) as any
113155

114156
render({
115157
get foo() {
116158
return 1
117159
},
118160
})
119-
expect(props.foo).toBeUndefined()
161+
expect(props.foo).toEqual(1)
162+
expect(attrs.foo).toEqual(1)
120163

121164
render({
122165
get foo() {
123166
return 2
124167
},
125168
})
126-
expect(props.foo).toBeUndefined()
169+
expect(props.foo).toEqual(2)
170+
expect(attrs.foo).toEqual(2)
127171
})
128172

129173
test('boolean casting', () => {
@@ -490,8 +534,34 @@ describe('component props (vapor)', () => {
490534
})
491535

492536
// #5016
493-
test.todo('handling attr with undefined value', () => {
494-
// TODO: attrs
537+
test('handling attr with undefined value', () => {
538+
const { render, host } = define({
539+
render() {
540+
const instance = getCurrentInstance()!
541+
const t0 = template('<div></div>')
542+
const n0 = t0()
543+
const n1 = children(n0, 0)
544+
watchEffect(() => {
545+
setText(
546+
n1,
547+
JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
548+
)
549+
})
550+
return n0
551+
},
552+
})
553+
554+
let attrs: any = {
555+
get foo() {
556+
return undefined
557+
},
558+
}
559+
560+
render(attrs)
561+
562+
expect(host.innerHTML).toBe(
563+
`<div>${JSON.stringify(attrs) + Object.keys(attrs)}</div>`,
564+
)
495565
})
496566

497567
// #6915

packages/runtime-vapor/src/component.ts

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface ComponentInternalInstance {
5858

5959
// state
6060
props: Data
61+
attrs: Data
6162
setupState: Data
6263
emit: EmitFn
6364
emitted: Record<string, boolean> | null
@@ -179,6 +180,7 @@ export const createComponentInstance = (
179180

180181
// state
181182
props: EMPTY_OBJ,
183+
attrs: EMPTY_OBJ,
182184
setupState: EMPTY_OBJ,
183185
refs: EMPTY_OBJ,
184186
metadata: new WeakMap(),

packages/runtime-vapor/src/componentProps.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type ComponentInternalInstance,
2020
setCurrentInstance,
2121
} from './component'
22+
import { isEmitListener } from './componentEmits'
2223

2324
export type ComponentPropsOptions<P = Data> =
2425
| ComponentObjectPropsOptions<P>
@@ -74,10 +75,13 @@ export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
7475
export function initProps(
7576
instance: ComponentInternalInstance,
7677
rawProps: Data | null,
78+
isStateful: boolean,
7779
) {
7880
const props: Data = {}
81+
const attrs: Data = {}
7982

8083
const [options, needCastKeys] = instance.propsOptions
84+
let hasAttrsChanged = false
8185
let rawCastValues: Data | undefined
8286
if (rawProps) {
8387
for (let key in rawProps) {
@@ -96,6 +100,7 @@ export function initProps(
96100
get() {
97101
return valueGetter()
98102
},
103+
enumerable: true,
99104
})
100105
} else {
101106
// NOTE: must getter
@@ -105,10 +110,22 @@ export function initProps(
105110
get() {
106111
return valueGetter()
107112
},
113+
enumerable: true,
108114
})
109115
}
110-
} else {
111-
// TODO:
116+
} else if (!isEmitListener(instance.emitsOptions, key)) {
117+
// if (!(key in attrs) || value !== attrs[key]) {
118+
if (!(key in attrs)) {
119+
// NOTE: must getter
120+
// attrs[key] = value
121+
Object.defineProperty(attrs, key, {
122+
get() {
123+
return valueGetter()
124+
},
125+
enumerable: true,
126+
})
127+
hasAttrsChanged = true
128+
}
112129
}
113130
}
114131
}
@@ -148,7 +165,18 @@ export function initProps(
148165
validateProps(rawProps || {}, props, instance)
149166
}
150167

151-
instance.props = shallowReactive(props)
168+
if (isStateful) {
169+
instance.props = shallowReactive(props)
170+
} else {
171+
if (instance.propsOptions === EMPTY_ARR) {
172+
instance.props = attrs
173+
} else {
174+
instance.props = props
175+
}
176+
}
177+
instance.attrs = attrs
178+
179+
return hasAttrsChanged
152180
}
153181

154182
function resolvePropValue(

packages/runtime-vapor/src/render.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { proxyRefs } from '@vue/reactivity'
2-
import { type Data, invokeArrayFns, isArray, isObject } from '@vue/shared'
2+
import {
3+
type Data,
4+
invokeArrayFns,
5+
isArray,
6+
isFunction,
7+
isObject,
8+
} from '@vue/shared'
39
import {
410
type Component,
511
type ComponentInternalInstance,
@@ -28,7 +34,7 @@ export function render(
2834
container: string | ParentNode,
2935
): ComponentInternalInstance {
3036
const instance = createComponentInstance(comp, props)
31-
initProps(instance, props)
37+
initProps(instance, props, !isFunction(instance.component))
3238
return mountComponent(instance, (container = normalizeContainer(container)))
3339
}
3440

@@ -46,11 +52,10 @@ export function mountComponent(
4652

4753
const reset = setCurrentInstance(instance)
4854
const block = instance.scope.run(() => {
49-
const { component, props, emit } = instance
50-
const ctx = { expose: () => {}, emit }
55+
const { component, props, emit, attrs } = instance
56+
const ctx = { expose: () => {}, emit, attrs }
5157

52-
const setupFn =
53-
typeof component === 'function' ? component : component.setup
58+
const setupFn = isFunction(component) ? component : component.setup
5459
const stateOrNode = setupFn && setupFn(props, ctx)
5560

5661
let block: Block | undefined

0 commit comments

Comments
 (0)