diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap
new file mode 100644
index 00000000000..80d066e8a67
--- /dev/null
+++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap
@@ -0,0 +1,46 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`defineAttrs() > basic usage 1`] = `
+"import { useAttrs as _useAttrs, defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+ const attrs = _useAttrs()
+
+return { attrs }
+}
+
+})"
+`;
+
+exports[`defineAttrs() > w/o generic params 1`] = `
+"import { useAttrs as _useAttrs } from 'vue'
+
+export default {
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+ const attrs = _useAttrs()
+
+return { attrs }
+}
+
+}"
+`;
+
+exports[`defineAttrs() > w/o return value 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+
+
+return { }
+}
+
+})"
+`;
diff --git a/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts
new file mode 100644
index 00000000000..e155e3c6bfd
--- /dev/null
+++ b/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts
@@ -0,0 +1,40 @@
+import { compileSFCScript as compile, assertCode } from '../utils'
+
+describe('defineAttrs()', () => {
+ test('basic usage', () => {
+ const { content } = compile(`
+
+ `)
+ assertCode(content)
+ expect(content).toMatch(`const attrs = _useAttrs()`)
+ expect(content).not.toMatch('defineAttrs')
+ })
+
+ test('w/o return value', () => {
+ const { content } = compile(`
+
+ `)
+ assertCode(content)
+ expect(content).not.toMatch('defineAttrs')
+ expect(content).not.toMatch(`_useAttrs`)
+ })
+
+ test('w/o generic params', () => {
+ const { content } = compile(`
+
+ `)
+ assertCode(content)
+ expect(content).toMatch(`const attrs = _useAttrs()`)
+ expect(content).not.toMatch('defineAttrs')
+ })
+})
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 2a33f69936d..f5c902f3df9 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -53,6 +53,7 @@ import {
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
import { isImportUsed } from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
+import { processDefineAttrs } from './script/defineAttrs'
export interface SFCScriptCompileOptions {
/**
@@ -512,7 +513,8 @@ export function compileScript(
processDefineProps(ctx, expr) ||
processDefineEmits(ctx, expr) ||
processDefineOptions(ctx, expr) ||
- processDefineSlots(ctx, expr)
+ processDefineSlots(ctx, expr) ||
+ processDefineAttrs(ctx, expr)
) {
ctx.s.remove(node.start! + startOffset, node.end! + startOffset)
} else if (processDefineExpose(ctx, expr)) {
@@ -550,7 +552,8 @@ export function compileScript(
!isDefineProps && processDefineEmits(ctx, init, decl.id)
!isDefineEmits &&
(processDefineSlots(ctx, init, decl.id) ||
- processDefineModel(ctx, init, decl.id))
+ processDefineModel(ctx, init, decl.id) ||
+ processDefineAttrs(ctx, init, decl.id))
if (
isDefineProps &&
diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts
index 692eab3ab9e..0d1607ab46d 100644
--- a/packages/compiler-sfc/src/script/context.ts
+++ b/packages/compiler-sfc/src/script/context.ts
@@ -36,6 +36,7 @@ export class ScriptCompileContext {
hasDefineOptionsCall = false
hasDefineSlotsCall = false
hasDefineModelCall = false
+ hasDefineAttrsCall = false
// defineProps
propsCall: CallExpression | undefined
diff --git a/packages/compiler-sfc/src/script/defineAttrs.ts b/packages/compiler-sfc/src/script/defineAttrs.ts
new file mode 100644
index 00000000000..d2eda8d9962
--- /dev/null
+++ b/packages/compiler-sfc/src/script/defineAttrs.ts
@@ -0,0 +1,33 @@
+import { LVal, Node } from '@babel/types'
+import { isCallOf } from './utils'
+import { ScriptCompileContext } from './context'
+
+export const DEFINE_ATTRS = 'defineAttrs'
+
+export function processDefineAttrs(
+ ctx: ScriptCompileContext,
+ node: Node,
+ declId?: LVal
+): boolean {
+ if (!isCallOf(node, DEFINE_ATTRS)) {
+ return false
+ }
+ if (ctx.hasDefineAttrsCall) {
+ ctx.error(`duplicate ${DEFINE_ATTRS}() call`, node)
+ }
+ ctx.hasDefineAttrsCall = true
+
+ if (node.arguments.length > 0) {
+ ctx.error(`${DEFINE_ATTRS}() cannot accept arguments`, node)
+ }
+
+ if (declId) {
+ ctx.s.overwrite(
+ ctx.startOffset! + node.start!,
+ ctx.startOffset! + node.end!,
+ `${ctx.helper('useAttrs')}()`
+ )
+ }
+
+ return true
+}
diff --git a/packages/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx
index 7466249e10f..7a9e9737785 100644
--- a/packages/dts-test/defineComponent.test-d.tsx
+++ b/packages/dts-test/defineComponent.test-d.tsx
@@ -10,10 +10,13 @@ import {
SetupContext,
h,
SlotsType,
+ AttrsType,
Slots,
- VNode
+ VNode,
+ ImgHTMLAttributes,
+ StyleValue
} from 'vue'
-import { describe, expectType, IsUnion } from './utils'
+import { describe, expectType, IsUnion, test } from './utils'
describe('with object props', () => {
interface ExpectedProps {
@@ -1188,6 +1191,270 @@ describe('async setup', () => {
vm.a = 2
})
+describe('define attrs', () => {
+ test('define attrs w/ object props', () => {
+ const MyComp = defineComponent({
+ props: {
+ foo: String
+ },
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ expectType()
+ })
+
+ test('define attrs w/ array props', () => {
+ const MyComp = defineComponent({
+ props: ['foo'],
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ expectType()
+ })
+
+ test('define attrs w/ no props', () => {
+ const MyComp = defineComponent({
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ expectType()
+ })
+
+ test('define attrs w/ composition api', () => {
+ const MyComp = defineComponent({
+ props: {
+ foo: {
+ type: String,
+ required: true
+ }
+ },
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ setup(props, { attrs }) {
+ expectType(props.foo)
+ expectType(attrs.bar)
+ }
+ })
+ expectType()
+ })
+
+ test('define attrs w/ functional component', () => {
+ const MyComp = defineComponent(
+ (props: { foo: string }, ctx) => {
+ expectType(ctx.attrs.bar)
+ return () => (
+ // return a render function (both JSX and h() works)
+ {props.foo}
+ )
+ },
+ {
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>
+ }
+ )
+ expectType()
+ })
+
+ test('define attrs as low priority', () => {
+ const MyComp = defineComponent({
+ props: {
+ foo: String
+ },
+ attrs: Object as AttrsType<{
+ foo?: number
+ }>,
+ created() {
+ // @ts-expect-error
+ this.$attrs.foo
+
+ expectType(this.foo)
+ }
+ })
+ expectType()
+ })
+
+ test('define required attrs', () => {
+ const MyComp = defineComponent({
+ attrs: Object as AttrsType<{
+ bar: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ expectType()
+ // @ts-expect-error
+ expectType()
+ })
+
+ test('define no attrs w/ object props', () => {
+ const MyComp = defineComponent({
+ props: {
+ foo: String
+ },
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ // @ts-expect-error
+ expectType()
+ })
+
+ test('define no attrs w/ functional component', () => {
+ const MyComp = defineComponent((props: { foo: string }, ctx) => {
+ expectType(ctx.attrs.bar)
+ return () => (
+ // return a render function (both JSX and h() works)
+ {props.foo}
+ )
+ })
+ expectType()
+ // @ts-expect-error
+ expectType()
+ })
+ test('wrap elements', () => {
+ const MyImg = defineComponent({
+ props: {
+ foo: String
+ },
+ attrs: Object as AttrsType,
+ created() {
+ expectType(this.$attrs.class)
+ expectType(this.$attrs.style)
+ },
+ render() {
+ return
+ }
+ })
+ expectType()
+ })
+
+ test('wrap components', () => {
+ const Child = defineComponent({
+ props: {
+ foo: String
+ },
+ emits: {
+ baz: (val: number) => true
+ },
+ render() {
+ return {this.foo}
+ }
+ })
+ const Comp = defineComponent({
+ props: {
+ bar: Number
+ },
+ attrs: Object as AttrsType,
+ created() {
+ expectType(this.$attrs.class)
+ expectType(this.$attrs.style)
+ },
+ render() {
+ return
+ }
+ })
+ expectType(
+ {
+ expectType(val)
+ }}
+ />
+ )
+ })
+
+ test('wrap components w/ functional component', () => {
+ const Child = defineComponent((props: { foo: string }, ctx) => {
+ return () => {props.foo}
+ })
+ const Comp = defineComponent({
+ props: {
+ bar: Number
+ },
+ attrs: Object as AttrsType,
+ created() {
+ expectType(this.$attrs.class)
+ expectType(this.$attrs.style)
+ },
+ render() {
+ return
+ }
+ })
+ expectType(
+
+ )
+ })
+ test('ignore reserved props', () => {
+ defineComponent({
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ // @ts-expect-error reserved props
+ this.$attrs.key
+ // @ts-expect-error reserved props
+ this.$attrs.ref
+ }
+ })
+ defineComponent(
+ (props: { foo: string }, ctx) => {
+ // @ts-expect-error reserved props
+ ctx.attrs.key
+ // @ts-expect-error reserved props
+ ctx.attrs.ref
+ return () => {props.foo}
+ },
+ {
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>
+ }
+ )
+ })
+
+ test('always readonly', () => {
+ defineComponent({
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ // @ts-expect-error readonly
+ this.$attrs.class = 'test'
+ }
+ })
+ defineComponent(
+ (props: { foo: string }, ctx) => {
+ // @ts-expect-error readonly
+ ctx.attrs.bar = 1
+ return () => {props.foo}
+ },
+ {
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>
+ }
+ )
+ })
+})
+
// #5948
describe('DefineComponent should infer correct types when assigning to Component', () => {
let component: Component
diff --git a/packages/dts-test/defineCustomElement.test-d.ts b/packages/dts-test/defineCustomElement.test-d.ts
index 4e7cf228372..5d56a65c0c8 100644
--- a/packages/dts-test/defineCustomElement.test-d.ts
+++ b/packages/dts-test/defineCustomElement.test-d.ts
@@ -1,5 +1,5 @@
-import { defineCustomElement } from 'vue'
-import { expectType, describe } from './utils'
+import { defineCustomElement, AttrsType } from 'vue'
+import { describe, expectType, test } from './utils'
describe('inject', () => {
// with object inject
@@ -62,3 +62,96 @@ describe('inject', () => {
}
})
})
+
+describe('define attrs', () => {
+ test('define attrs w/ object props', () => {
+ defineCustomElement({
+ props: {
+ foo: String
+ },
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ })
+
+ test('define attrs w/ array props', () => {
+ defineCustomElement({
+ props: ['foo'],
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ })
+
+ test('define attrs w/ no props', () => {
+ defineCustomElement({
+ attrs: Object as AttrsType<{
+ bar?: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ })
+
+ test('define attrs as low priority', () => {
+ defineCustomElement({
+ props: {
+ foo: String
+ },
+ attrs: Object as AttrsType<{
+ foo: number
+ }>,
+ created() {
+ // @ts-expect-error
+ this.$attrs.foo
+ expectType(this.foo)
+ }
+ })
+ })
+
+ test('define attrs w/ no attrs', () => {
+ defineCustomElement({
+ props: {
+ foo: String
+ },
+ created() {
+ expectType(this.$attrs.bar)
+ expectType(this.$attrs.baz)
+ }
+ })
+ })
+
+ test('default attrs like class, style', () => {
+ defineCustomElement({
+ props: {
+ foo: String
+ },
+ created() {
+ expectType(this.$attrs.class)
+ expectType(this.$attrs.style)
+ }
+ })
+ })
+
+ test('define required attrs', () => {
+ defineCustomElement({
+ props: {
+ foo: String
+ },
+ attrs: Object as AttrsType<{
+ bar: number
+ }>,
+ created() {
+ expectType(this.$attrs.bar)
+ }
+ })
+ })
+})
diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts
index 272bb548751..66f8de4b65b 100644
--- a/packages/runtime-core/src/apiDefineComponent.ts
+++ b/packages/runtime-core/src/apiDefineComponent.ts
@@ -8,12 +8,15 @@ import {
RenderFunction,
ComponentOptionsBase,
ComponentInjectOptions,
- ComponentOptions
+ ComponentOptions,
+ AttrsType,
+ UnwrapAttrsType
} from './componentOptions'
import {
SetupContext,
AllowedComponentProps,
- ComponentCustomProps
+ ComponentCustomProps,
+ HasDefinedAttrs
} from './component'
import {
ExtractPropTypes,
@@ -54,7 +57,8 @@ export type DefineComponent<
PP = PublicProps,
Props = ResolveProps,
Defaults = ExtractDefaultPropTypes,
- S extends SlotsType = {}
+ S extends SlotsType = {},
+ Attrs extends AttrsType = Record
> = ComponentPublicInstanceConstructor<
CreateComponentPublicInstance<
Props,
@@ -69,7 +73,8 @@ export type DefineComponent<
Defaults,
true,
{},
- S
+ S,
+ Attrs
> &
Props
> &
@@ -86,7 +91,8 @@ export type DefineComponent<
Defaults,
{},
string,
- S
+ S,
+ Attrs
> &
PP
@@ -101,18 +107,23 @@ export function defineComponent<
Props extends Record,
E extends EmitsOptions = {},
EE extends string = string,
- S extends SlotsType = {}
+ S extends SlotsType = {},
+ Attrs extends AttrsType = Record,
+ PropsAttrs = HasDefinedAttrs extends true
+ ? UnwrapAttrsType>
+ : {}
>(
setup: (
props: Props,
- ctx: SetupContext
+ ctx: SetupContext
) => RenderFunction | Promise,
options?: Pick & {
props?: (keyof Props)[]
emits?: E | EE[]
slots?: S
+ attrs?: Attrs
}
-): (props: Props & EmitsToProps) => any
+): (props: Props & EmitsToProps & PropsAttrs) => any
export function defineComponent<
Props extends Record,
E extends EmitsOptions = {},
@@ -144,6 +155,7 @@ export function defineComponent<
E extends EmitsOptions = {},
EE extends string = string,
S extends SlotsType = {},
+ Attrs extends AttrsType = Record,
I extends ComponentInjectOptions = {},
II extends string = string
>(
@@ -159,7 +171,8 @@ export function defineComponent<
EE,
I,
II,
- S
+ S,
+ Attrs
>
): DefineComponent<
Props,
@@ -174,7 +187,8 @@ export function defineComponent<
PublicProps,
ResolveProps,
ExtractDefaultPropTypes,
- S
+ S,
+ Attrs
>
// overload 3: object format with array props declaration
@@ -191,6 +205,7 @@ export function defineComponent<
E extends EmitsOptions = {},
EE extends string = string,
S extends SlotsType = {},
+ Attrs extends AttrsType = Record,
I extends ComponentInjectOptions = {},
II extends string = string,
Props = Readonly<{ [key in PropNames]?: any }>
@@ -207,7 +222,8 @@ export function defineComponent<
EE,
I,
II,
- S
+ S,
+ Attrs
>
): DefineComponent<
Props,
@@ -222,7 +238,8 @@ export function defineComponent<
PublicProps,
ResolveProps,
ExtractDefaultPropTypes,
- S
+ S,
+ Attrs
>
// overload 4: object format with object props declaration
@@ -241,7 +258,8 @@ export function defineComponent<
EE extends string = string,
S extends SlotsType = {},
I extends ComponentInjectOptions = {},
- II extends string = string
+ II extends string = string,
+ Attrs extends AttrsType = Record
>(
options: ComponentOptionsWithObjectProps<
PropsOptions,
@@ -255,7 +273,8 @@ export function defineComponent<
EE,
I,
II,
- S
+ S,
+ Attrs
>
): DefineComponent<
PropsOptions,
@@ -270,7 +289,8 @@ export function defineComponent<
PublicProps,
ResolveProps,
ExtractDefaultPropTypes,
- S
+ S,
+ Attrs
>
// implementation, close to no-op
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
index 93200667081..b673d20e7f8 100644
--- a/packages/runtime-core/src/apiSetupHelpers.ts
+++ b/packages/runtime-core/src/apiSetupHelpers.ts
@@ -18,7 +18,8 @@ import {
ComponentOptionsMixin,
ComponentOptionsWithoutProps,
ComputedOptions,
- MethodOptions
+ MethodOptions,
+ StrictUnwrapAttrsType
} from './componentOptions'
import {
ComponentPropsOptions,
@@ -215,6 +216,15 @@ export function defineSlots<
return null as any
}
+export function defineAttrs<
+ Attrs extends Record = Record
+>(): StrictUnwrapAttrsType {
+ if (__DEV__) {
+ warnRuntimeUsage(`defineAttrs`)
+ }
+ return null as any
+}
+
/**
* (**Experimental**) Vue `