Skip to content

Commit b184ee3

Browse files
committed
feat(types): infer attrs in defineCustomElement
1 parent 4f7d0fd commit b184ee3

File tree

4 files changed

+170
-37
lines changed

4 files changed

+170
-37
lines changed

packages/runtime-core/src/vnode.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
ConcreteComponent,
1919
ClassComponent,
2020
Component,
21-
isClassComponent
21+
isClassComponent,
22+
AllowedComponentProps
2223
} from './component'
2324
import { RawSlots } from './componentSlots'
2425
import { isProxy, Ref, toRaw, ReactiveFlags, isRef } from '@vue/reactivity'
@@ -813,7 +814,7 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
813814
vnode.shapeFlag |= type
814815
}
815816

816-
export function mergeProps(...args: (Data & VNodeProps)[]) {
817+
export function mergeProps(...args: (AllowedComponentProps | Data)[]) {
817818
const ret: Data = {}
818819
for (let i = 0; i < args.length; i++) {
819820
const toMerge = args[i]
@@ -826,7 +827,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
826827
ret.style = normalizeStyle([ret.style, toMerge.style])
827828
} else if (isOn(key)) {
828829
const existing = ret[key]
829-
const incoming = toMerge[key]
830+
const incoming = (toMerge as Data)[key]
830831
if (
831832
incoming &&
832833
existing !== incoming &&
@@ -837,7 +838,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
837838
: incoming
838839
}
839840
} else if (key !== '') {
840-
ret[key] = toMerge[key]
841+
ret[key] = (toMerge as Data)[key]
841842
}
842843
}
843844
}

packages/runtime-dom/__tests__/customElement.spec.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,14 @@ describe('defineCustomElement', () => {
286286
})
287287

288288
describe('attrs', () => {
289-
const E = defineCustomElement({
290-
render() {
291-
return [h('div', null, this.$attrs.foo as string)]
292-
}
293-
})
289+
const E = defineCustomElement(
290+
{
291+
render() {
292+
return [h('div', null, this.$attrs.foo)]
293+
}
294+
},
295+
{ attrs: {} as { foo: string } }
296+
)
294297
customElements.define('my-el-attrs', E)
295298

296299
test('attrs via attribute', async () => {

packages/runtime-dom/src/apiCustomElement.ts

+37-19
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export type VueElementConstructor<P = {}> = {
3333
// so most of the following overloads should be kept in sync w/ defineComponent.
3434

3535
// overload 1: direct setup function
36-
export function defineCustomElement<Props, RawBindings = object>(
36+
export function defineCustomElement<Props, RawBindings = object, Attrs = {}>(
3737
setup: (
3838
props: Readonly<Props>,
39-
ctx: SetupContext
39+
ctx: SetupContext<{}, Attrs>
4040
) => RawBindings | RenderFunction
4141
): VueElementConstructor<Props>
4242

@@ -52,9 +52,10 @@ export function defineCustomElement<
5252
E extends EmitsOptions = EmitsOptions,
5353
EE extends string = string,
5454
I extends ComponentInjectOptions = {},
55-
II extends string = string
55+
II extends string = string,
56+
Attrs = {}
5657
>(
57-
options: ComponentOptionsWithoutProps<
58+
comp: ComponentOptionsWithoutProps<
5859
Props,
5960
RawBindings,
6061
D,
@@ -65,8 +66,12 @@ export function defineCustomElement<
6566
E,
6667
EE,
6768
I,
68-
II
69-
> & { styles?: string[] }
69+
II,
70+
Attrs
71+
> & { styles?: string[] },
72+
options?: {
73+
attrs: Attrs
74+
}
7075
): VueElementConstructor<Props>
7176

7277
// overload 3: object format with array props declaration
@@ -81,9 +86,10 @@ export function defineCustomElement<
8186
E extends EmitsOptions = Record<string, any>,
8287
EE extends string = string,
8388
I extends ComponentInjectOptions = {},
84-
II extends string = string
89+
II extends string = string,
90+
Attrs = {}
8591
>(
86-
options: ComponentOptionsWithArrayProps<
92+
comp: ComponentOptionsWithArrayProps<
8793
PropNames,
8894
RawBindings,
8995
D,
@@ -94,8 +100,12 @@ export function defineCustomElement<
94100
E,
95101
EE,
96102
I,
97-
II
98-
> & { styles?: string[] }
103+
II,
104+
Attrs
105+
> & { styles?: string[] },
106+
options?: {
107+
attrs: Attrs
108+
}
99109
): VueElementConstructor<{ [K in PropNames]: any }>
100110

101111
// overload 4: object format with object props declaration
@@ -110,9 +120,10 @@ export function defineCustomElement<
110120
E extends EmitsOptions = Record<string, any>,
111121
EE extends string = string,
112122
I extends ComponentInjectOptions = {},
113-
II extends string = string
123+
II extends string = string,
124+
Attrs = {}
114125
>(
115-
options: ComponentOptionsWithObjectProps<
126+
comp: ComponentOptionsWithObjectProps<
116127
PropsOptions,
117128
RawBindings,
118129
D,
@@ -123,8 +134,12 @@ export function defineCustomElement<
123134
E,
124135
EE,
125136
I,
126-
II
127-
> & { styles?: string[] }
137+
II,
138+
Attrs
139+
> & { styles?: string[] },
140+
options?: {
141+
attrs: Attrs
142+
}
128143
): VueElementConstructor<ExtractPropTypes<PropsOptions>>
129144

130145
// overload 5: defining a custom element from the returned value of
@@ -134,14 +149,17 @@ export function defineCustomElement(options: {
134149
}): VueElementConstructor
135150

136151
export function defineCustomElement(
137-
options: any,
138-
hydrate?: RootHydrateFunction
152+
comp: any,
153+
options?: {
154+
hydrate?: RootHydrateFunction
155+
attrs?: any
156+
}
139157
): VueElementConstructor {
140-
const Comp = defineComponent(options as any)
158+
const Comp = defineComponent(comp as any)
141159
class VueCustomElement extends VueElement {
142160
static def = Comp
143161
constructor(initialProps?: Record<string, any>) {
144-
super(Comp, initialProps, hydrate)
162+
super(Comp, initialProps, options?.hydrate)
145163
}
146164
}
147165

@@ -150,7 +168,7 @@ export function defineCustomElement(
150168

151169
export const defineSSRCustomElement = ((options: any) => {
152170
// @ts-ignore
153-
return defineCustomElement(options, hydrate)
171+
return defineCustomElement(options, { hydrate })
154172
}) as typeof defineCustomElement
155173

156174
const BaseClass = (

test-dts/defineCustomElement.test-d.ts

+120-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { defineCustomElement, expectType, expectError } from './index'
1+
import {
2+
defineCustomElement,
3+
expectType,
4+
expectError,
5+
SetupContext
6+
} from './index'
27

38
describe('inject', () => {
49
// with object inject
@@ -8,13 +13,13 @@ describe('inject', () => {
813
},
914
inject: {
1015
foo: 'foo',
11-
bar: 'bar',
16+
bar: 'bar'
1217
},
1318
created() {
1419
expectType<unknown>(this.foo)
1520
expectType<unknown>(this.bar)
1621
// @ts-expect-error
17-
expectError(this.foobar = 1)
22+
expectError((this.foobar = 1))
1823
}
1924
})
2025

@@ -26,7 +31,7 @@ describe('inject', () => {
2631
expectType<unknown>(this.foo)
2732
expectType<unknown>(this.bar)
2833
// @ts-expect-error
29-
expectError(this.foobar = 1)
34+
expectError((this.foobar = 1))
3035
}
3136
})
3237

@@ -40,13 +45,13 @@ describe('inject', () => {
4045
bar: {
4146
from: 'pfoo',
4247
default: 'bar'
43-
},
48+
}
4449
},
4550
created() {
4651
expectType<unknown>(this.foo)
4752
expectType<unknown>(this.bar)
4853
// @ts-expect-error
49-
expectError(this.foobar = 1)
54+
expectError((this.foobar = 1))
5055
}
5156
})
5257

@@ -55,9 +60,115 @@ describe('inject', () => {
5560
props: ['a', 'b'],
5661
created() {
5762
// @ts-expect-error
58-
expectError(this.foo = 1)
63+
expectError((this.foo = 1))
5964
// @ts-expect-error
60-
expectError(this.bar = 1)
65+
expectError((this.bar = 1))
66+
}
67+
})
68+
})
69+
70+
describe('define attrs', () => {
71+
test('define attrs w/ object props', () => {
72+
type CompAttrs = {
73+
bar: number
74+
baz?: string
75+
}
76+
defineCustomElement(
77+
{
78+
props: {
79+
foo: String
80+
},
81+
created() {
82+
expectType<CompAttrs['bar']>(this.$attrs.bar)
83+
expectType<CompAttrs['baz']>(this.$attrs.baz)
84+
}
85+
},
86+
{
87+
attrs: {} as CompAttrs
88+
}
89+
)
90+
})
91+
92+
test('define attrs w/ array props', () => {
93+
type CompAttrs = {
94+
bar: number
95+
baz?: string
96+
}
97+
defineCustomElement(
98+
{
99+
props: ['foo'],
100+
created() {
101+
expectType<CompAttrs['bar']>(this.$attrs.bar)
102+
expectType<CompAttrs['baz']>(this.$attrs.baz)
103+
}
104+
},
105+
{
106+
attrs: {} as CompAttrs
107+
}
108+
)
109+
})
110+
111+
test('define attrs w/ no props', () => {
112+
type CompAttrs = {
113+
bar: number
114+
baz?: string
61115
}
116+
defineCustomElement(
117+
{
118+
created() {
119+
expectType<CompAttrs['bar']>(this.$attrs.bar)
120+
expectType<CompAttrs['baz']>(this.$attrs.baz)
121+
}
122+
},
123+
{
124+
attrs: {} as CompAttrs
125+
}
126+
)
127+
})
128+
129+
test('define attrs w/ function component', () => {
130+
type CompAttrs = {
131+
bar: number
132+
baz?: string
133+
}
134+
defineCustomElement(
135+
(_props: { foo: string }, ctx: SetupContext<{}, CompAttrs>) => {
136+
expectType<number>(ctx.attrs.bar)
137+
expectType<CompAttrs['bar']>(ctx.attrs.bar)
138+
expectType<CompAttrs['baz']>(ctx.attrs.baz)
139+
}
140+
)
141+
})
142+
143+
test('define attrs as low priority', () => {
144+
type CompAttrs = {
145+
foo: number
146+
}
147+
defineCustomElement(
148+
{
149+
props: {
150+
foo: String
151+
},
152+
created() {
153+
// @ts-expect-error
154+
console.log(this.$attrs.foo)
155+
}
156+
},
157+
{
158+
attrs: {} as CompAttrs
159+
}
160+
)
161+
})
162+
163+
test('define attrs w/ default attrs such as class, style', () => {
164+
defineCustomElement({
165+
props: {
166+
foo: String
167+
},
168+
created() {
169+
expectType<unknown>(this.$attrs.class)
170+
expectType<unknown>(this.$attrs.style)
171+
}
172+
})
62173
})
63-
})
174+
})

0 commit comments

Comments
 (0)