feat(@vben-core/popup-ui)!: add typed popup data contracts
Bind shared data to modal and drawer APIs and infer it from exposed connected components. BREAKING CHANGE: getData<T>() is replaced by getData(). Shared data defaults to unknown, and an unset payload returns undefined instead of an empty object.
This commit is contained in:
@@ -81,6 +81,16 @@ describe('drawerApi', () => {
|
||||
expect(drawerApi.getData()).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should return undefined before shared data is set', () => {
|
||||
expect(drawerApi.getData()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve null shared data', () => {
|
||||
const nullableDrawerApi = new DrawerApi<null | Record<string, unknown>>();
|
||||
nullableDrawerApi.setData(null);
|
||||
expect(nullableDrawerApi.getData()).toBeNull();
|
||||
});
|
||||
|
||||
it('should set state correctly using an object', () => {
|
||||
drawerApi.setState({ title: 'New Title' });
|
||||
expect(drawerApi.store.state.title).toBe('New Title');
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ExtendedDrawerApi, InferDrawerData } from '../drawer';
|
||||
import type { createVbenDrawer, useVbenDrawer } from '../use-drawer';
|
||||
import type TypedDrawer from './fixtures/typed-drawer.vue';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
interface TypedDrawerData {
|
||||
id: number;
|
||||
mode: 'edit' | 'view';
|
||||
}
|
||||
|
||||
type DrawerData = null | TypedDrawerData;
|
||||
|
||||
declare const createDrawer: typeof createVbenDrawer;
|
||||
declare const useDrawer: typeof useVbenDrawer;
|
||||
|
||||
describe('drawer public data types', () => {
|
||||
it('infers data from the connected component exposed api', () => {
|
||||
function assertInferredData(connectedComponent: typeof TypedDrawer) {
|
||||
const [, drawerApi] = useDrawer({ connectedComponent });
|
||||
|
||||
expectTypeOf(drawerApi).toEqualTypeOf<ExtendedDrawerApi<DrawerData>>();
|
||||
expectTypeOf(drawerApi.getData()).toEqualTypeOf<DrawerData | undefined>();
|
||||
expectTypeOf(drawerApi.setData).parameter(0).toEqualTypeOf<DrawerData>();
|
||||
expectTypeOf(drawerApi.setData(null).open()).toBeVoid();
|
||||
// @ts-expect-error invalid payload type
|
||||
drawerApi.setData({ id: '1', mode: 'edit' });
|
||||
}
|
||||
|
||||
expectTypeOf<
|
||||
InferDrawerData<typeof TypedDrawer>
|
||||
>().toEqualTypeOf<DrawerData>();
|
||||
expectTypeOf(assertInferredData).toBeFunction();
|
||||
});
|
||||
|
||||
it('supports explicit and pre-bound data contracts', () => {
|
||||
function assertExplicitData() {
|
||||
const [, explicitApi] = useDrawer<DrawerData>();
|
||||
const useTypedDrawer = createDrawer<DrawerData>();
|
||||
const [, preBoundApi] = useTypedDrawer();
|
||||
|
||||
expectTypeOf(explicitApi).toEqualTypeOf<ExtendedDrawerApi<DrawerData>>();
|
||||
expectTypeOf(preBoundApi).toEqualTypeOf<ExtendedDrawerApi<DrawerData>>();
|
||||
}
|
||||
|
||||
expectTypeOf(assertExplicitData).toBeFunction();
|
||||
});
|
||||
|
||||
it('falls back to unknown without a data contract', () => {
|
||||
function assertUnknownData() {
|
||||
const [, drawerApi] = useDrawer();
|
||||
|
||||
expectTypeOf(drawerApi).toEqualTypeOf<ExtendedDrawerApi<unknown>>();
|
||||
expectTypeOf(drawerApi.getData()).toBeUnknown();
|
||||
}
|
||||
|
||||
expectTypeOf(assertUnknownData).toBeFunction();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenDrawer } from '../../use-drawer';
|
||||
|
||||
interface TypedDrawerData {
|
||||
id: number;
|
||||
mode: 'edit' | 'view';
|
||||
}
|
||||
|
||||
const [, drawerApi] = useVbenDrawer<TypedDrawerData | null>();
|
||||
|
||||
defineExpose({ drawerApi });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@@ -79,6 +79,9 @@ describe('useVbenDrawer', () => {
|
||||
expect(initialApi).toBeDefined();
|
||||
if (!initialApi) return;
|
||||
expect(parentApi.store).toBe(initialApi.store);
|
||||
const initialData = { id: 1 };
|
||||
parentApi.setData(initialData);
|
||||
expect(initialApi.getData()).toBe(initialData);
|
||||
|
||||
await remountConsumer(consumerKey);
|
||||
const recreatedApi = getCurrentApi();
|
||||
@@ -88,6 +91,9 @@ describe('useVbenDrawer', () => {
|
||||
expect(recreatedApi).not.toBe(initialApi);
|
||||
expect(recreatedApi.store.state.title).toBe('Parent drawer title');
|
||||
expect(parentApi.store).toBe(recreatedApi.store);
|
||||
const recreatedData = { id: 2 };
|
||||
parentApi.setData(recreatedData);
|
||||
expect(recreatedApi.getData()).toBe(recreatedData);
|
||||
|
||||
parentApi.open();
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { DrawerApiOptions, DrawerState } from './drawer';
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
import { bindMethods, isFunction } from '@vben-core/shared/utils';
|
||||
|
||||
export class DrawerApi {
|
||||
export class DrawerApi<TData = unknown> {
|
||||
// 共享数据
|
||||
public sharedData: Record<'payload', any> = {
|
||||
payload: {},
|
||||
public sharedData: Record<'payload', TData | undefined> = {
|
||||
payload: undefined,
|
||||
};
|
||||
public store: Store<DrawerState>;
|
||||
|
||||
@@ -97,8 +97,8 @@ export class DrawerApi {
|
||||
}
|
||||
}
|
||||
|
||||
getData<T extends object = Record<string, any>>() {
|
||||
return (this.sharedData?.payload ?? {}) as T;
|
||||
getData(): TData | undefined {
|
||||
return this.sharedData.payload;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,7 +150,7 @@ export class DrawerApi {
|
||||
this.store.setState((prev) => ({ ...prev, isOpen: true }));
|
||||
}
|
||||
|
||||
setData<T>(payload: T) {
|
||||
setData(payload: TData) {
|
||||
this.sharedData.payload = payload;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -130,23 +130,36 @@ export interface DrawerProps {
|
||||
export interface DrawerState extends DrawerProps {
|
||||
/** 弹窗打开状态 */
|
||||
isOpen?: boolean;
|
||||
/**
|
||||
* 共享数据
|
||||
*/
|
||||
sharedData?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type ExtendedDrawerApi = DrawerApi & {
|
||||
export type ExtendedDrawerApi<TData = unknown> = DrawerApi<TData> & {
|
||||
useStore: <T = NoInfer<DrawerState>>(
|
||||
selector?: (state: NoInfer<DrawerState>) => T,
|
||||
) => Readonly<Ref<T>>;
|
||||
};
|
||||
|
||||
export interface DrawerApiOptions extends DrawerState {
|
||||
type DrawerComponentInstance<TComponent extends Component> =
|
||||
TComponent extends abstract new (...args: any[]) => infer TInstance
|
||||
? TInstance
|
||||
: never;
|
||||
|
||||
export type InferDrawerData<TComponent extends Component> = [
|
||||
DrawerComponentInstance<TComponent>,
|
||||
] extends [never]
|
||||
? unknown
|
||||
: DrawerComponentInstance<TComponent> extends {
|
||||
drawerApi: ExtendedDrawerApi<infer TData>;
|
||||
}
|
||||
? TData
|
||||
: unknown;
|
||||
|
||||
export interface DrawerApiOptions<
|
||||
TConnectedComponent extends Component = Component,
|
||||
> extends DrawerState {
|
||||
/**
|
||||
* 独立的抽屉组件
|
||||
*/
|
||||
connectedComponent?: Component;
|
||||
connectedComponent?: TConnectedComponent;
|
||||
/**
|
||||
* 关闭前的回调,返回 false 可以阻止关闭
|
||||
* @returns
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export type * from './drawer';
|
||||
export { default as VbenDrawer } from './drawer.vue';
|
||||
export { setDefaultDrawerProps, useVbenDrawer } from './use-drawer';
|
||||
export {
|
||||
createVbenDrawer,
|
||||
setDefaultDrawerProps,
|
||||
useVbenDrawer,
|
||||
} from './use-drawer';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type {
|
||||
DrawerApiOptions,
|
||||
DrawerProps,
|
||||
ExtendedDrawerApi,
|
||||
InferDrawerData,
|
||||
} from './drawer';
|
||||
|
||||
import {
|
||||
@@ -24,6 +27,26 @@ import VbenDrawer from './drawer.vue';
|
||||
|
||||
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
|
||||
|
||||
declare const DRAWER_DATA_NOT_PROVIDED: unique symbol;
|
||||
|
||||
type DrawerDataNotProvided = {
|
||||
readonly [DRAWER_DATA_NOT_PROVIDED]: true;
|
||||
};
|
||||
|
||||
type ResolvedDrawerData<
|
||||
TData,
|
||||
TConnectedComponent extends Component,
|
||||
> = TData extends DrawerDataNotProvided
|
||||
? InferDrawerData<TConnectedComponent>
|
||||
: TData;
|
||||
|
||||
interface DrawerInjectData<TData> {
|
||||
consumed?: boolean;
|
||||
extendApi?: (api: ExtendedDrawerApi<TData>) => void;
|
||||
options?: DrawerApiOptions;
|
||||
reCreateDrawer?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const { globalEscapeShortcutKey } = usePreferences();
|
||||
|
||||
/**
|
||||
@@ -36,8 +59,11 @@ export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
|
||||
}
|
||||
|
||||
export function useVbenDrawer<
|
||||
TParentDrawerProps extends DrawerProps = DrawerProps,
|
||||
>(options: DrawerApiOptions = {}) {
|
||||
TData = DrawerDataNotProvided,
|
||||
TConnectedComponent extends Component = Component,
|
||||
>(options: DrawerApiOptions<TConnectedComponent> = {}) {
|
||||
type TResolvedData = ResolvedDrawerData<TData, TConnectedComponent>;
|
||||
|
||||
// Drawer一般会抽离出来,所以如果有传入 connectedComponent,则表示为外部调用,与内部组件进行连接
|
||||
// 外部的Drawer通过provide/inject传递api
|
||||
|
||||
@@ -47,11 +73,11 @@ export function useVbenDrawer<
|
||||
};
|
||||
const { connectedComponent } = options;
|
||||
if (connectedComponent) {
|
||||
const extendedApi = shallowReactive({});
|
||||
const extendedApi = shallowReactive({}) as ExtendedDrawerApi<TResolvedData>;
|
||||
const isDrawerReady = ref(true);
|
||||
const Drawer = defineComponent(
|
||||
(props: TParentDrawerProps, { attrs, slots }) => {
|
||||
function rebindApi(api: ExtendedDrawerApi) {
|
||||
(props: DrawerProps, { attrs, slots }) => {
|
||||
function rebindApi(api: ExtendedDrawerApi<TResolvedData>) {
|
||||
Object.setPrototypeOf(extendedApi, markRaw(api));
|
||||
}
|
||||
|
||||
@@ -65,7 +91,7 @@ export function useVbenDrawer<
|
||||
isDrawerReady.value = true;
|
||||
},
|
||||
});
|
||||
checkProps(extendedApi as ExtendedDrawerApi, {
|
||||
checkProps(extendedApi, {
|
||||
...props,
|
||||
...attrs,
|
||||
...slots,
|
||||
@@ -84,10 +110,13 @@ export function useVbenDrawer<
|
||||
},
|
||||
);
|
||||
|
||||
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
||||
return [Drawer, extendedApi] as const;
|
||||
}
|
||||
|
||||
const injectData = inject<any>(USER_DRAWER_INJECT_KEY, {});
|
||||
const injectData = inject<DrawerInjectData<TResolvedData>>(
|
||||
USER_DRAWER_INJECT_KEY,
|
||||
{},
|
||||
);
|
||||
const isConsumed = injectData.consumed;
|
||||
const effectiveOptions = isConsumed ? {} : injectData.options;
|
||||
if (!isConsumed && injectData.consumed !== undefined) {
|
||||
@@ -122,9 +151,9 @@ export function useVbenDrawer<
|
||||
injectData.reCreateDrawer?.();
|
||||
}
|
||||
};
|
||||
const api = new DrawerApi(mergedOptions);
|
||||
const api = new DrawerApi<TResolvedData>(mergedOptions);
|
||||
|
||||
const extendedApi: ExtendedDrawerApi = api as never;
|
||||
const extendedApi = api as ExtendedDrawerApi<TResolvedData>;
|
||||
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useSelector(api.store, selector);
|
||||
@@ -145,7 +174,18 @@ export function useVbenDrawer<
|
||||
return [Drawer, extendedApi] as const;
|
||||
}
|
||||
|
||||
async function checkProps(api: ExtendedDrawerApi, attrs: Record<string, any>) {
|
||||
export function createVbenDrawer<TData = unknown>() {
|
||||
return function useTypedVbenDrawer<
|
||||
TConnectedComponent extends Component = Component,
|
||||
>(options: DrawerApiOptions<TConnectedComponent> = {}) {
|
||||
return useVbenDrawer<TData, TConnectedComponent>(options);
|
||||
};
|
||||
}
|
||||
|
||||
async function checkProps<TData>(
|
||||
api: ExtendedDrawerApi<TData>,
|
||||
attrs: Record<string, any>,
|
||||
) {
|
||||
if (!attrs || Object.keys(attrs).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '../../use-modal';
|
||||
|
||||
interface TypedModalData {
|
||||
id: number;
|
||||
mode: 'edit' | 'view';
|
||||
}
|
||||
|
||||
const [, modalApi] = useVbenModal<TypedModalData | null>();
|
||||
|
||||
defineExpose({ modalApi });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@@ -82,6 +82,16 @@ describe('modalApi', () => {
|
||||
expect(modalApi.getData()).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should return undefined before shared data is set', () => {
|
||||
expect(modalApi.getData()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve null shared data', () => {
|
||||
const nullableModalApi = new ModalApi<null | Record<string, unknown>>();
|
||||
nullableModalApi.setData(null);
|
||||
expect(nullableModalApi.getData()).toBeNull();
|
||||
});
|
||||
|
||||
it('should set state correctly using an object', () => {
|
||||
modalApi.setState({ title: 'New Title' });
|
||||
expect(modalApi.store.state.title).toBe('New Title');
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ExtendedModalApi, InferModalData } from '../modal';
|
||||
import type { createVbenModal, useVbenModal } from '../use-modal';
|
||||
import type TypedModal from './fixtures/typed-modal.vue';
|
||||
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
interface TypedModalData {
|
||||
id: number;
|
||||
mode: 'edit' | 'view';
|
||||
}
|
||||
|
||||
type ModalData = null | TypedModalData;
|
||||
|
||||
declare const createModal: typeof createVbenModal;
|
||||
declare const useModal: typeof useVbenModal;
|
||||
|
||||
describe('modal public data types', () => {
|
||||
it('infers data from the connected component exposed api', () => {
|
||||
function assertInferredData(connectedComponent: typeof TypedModal) {
|
||||
const [, modalApi] = useModal({ connectedComponent });
|
||||
|
||||
expectTypeOf(modalApi).toEqualTypeOf<ExtendedModalApi<ModalData>>();
|
||||
expectTypeOf(modalApi.getData()).toEqualTypeOf<ModalData | undefined>();
|
||||
expectTypeOf(modalApi.setData).parameter(0).toEqualTypeOf<ModalData>();
|
||||
expectTypeOf(modalApi.setData(null).open()).toBeVoid();
|
||||
// @ts-expect-error invalid payload type
|
||||
modalApi.setData({ id: '1', mode: 'edit' });
|
||||
}
|
||||
|
||||
expectTypeOf<
|
||||
InferModalData<typeof TypedModal>
|
||||
>().toEqualTypeOf<ModalData>();
|
||||
expectTypeOf(assertInferredData).toBeFunction();
|
||||
});
|
||||
|
||||
it('supports explicit and pre-bound data contracts', () => {
|
||||
function assertExplicitData() {
|
||||
const [, explicitApi] = useModal<ModalData>();
|
||||
const useTypedModal = createModal<ModalData>();
|
||||
const [, preBoundApi] = useTypedModal();
|
||||
|
||||
expectTypeOf(explicitApi).toEqualTypeOf<ExtendedModalApi<ModalData>>();
|
||||
expectTypeOf(preBoundApi).toEqualTypeOf<ExtendedModalApi<ModalData>>();
|
||||
}
|
||||
|
||||
expectTypeOf(assertExplicitData).toBeFunction();
|
||||
});
|
||||
|
||||
it('falls back to unknown without a data contract', () => {
|
||||
function assertUnknownData() {
|
||||
const [, modalApi] = useModal();
|
||||
|
||||
expectTypeOf(modalApi).toEqualTypeOf<ExtendedModalApi<unknown>>();
|
||||
expectTypeOf(modalApi.getData()).toBeUnknown();
|
||||
}
|
||||
|
||||
expectTypeOf(assertUnknownData).toBeFunction();
|
||||
});
|
||||
});
|
||||
@@ -79,6 +79,9 @@ describe('useVbenModal', () => {
|
||||
expect(initialApi).toBeDefined();
|
||||
if (!initialApi) return;
|
||||
expect(toRaw(parentApi.store)).toBe(initialApi.store);
|
||||
const initialData = { id: 1 };
|
||||
parentApi.setData(initialData);
|
||||
expect(initialApi.getData()).toBe(initialData);
|
||||
|
||||
await remountConsumer(consumerKey);
|
||||
const recreatedApi = getCurrentApi();
|
||||
@@ -88,6 +91,9 @@ describe('useVbenModal', () => {
|
||||
expect(recreatedApi).not.toBe(initialApi);
|
||||
expect(recreatedApi.store.state.title).toBe('Parent modal title');
|
||||
expect(toRaw(parentApi.store)).toBe(recreatedApi.store);
|
||||
const recreatedData = { id: 2 };
|
||||
parentApi.setData(recreatedData);
|
||||
expect(recreatedApi.getData()).toBe(recreatedData);
|
||||
|
||||
parentApi.open();
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export type * from './modal';
|
||||
export { default as VbenModal } from './modal.vue';
|
||||
export { setDefaultModalProps, useVbenModal } from './use-modal';
|
||||
export {
|
||||
createVbenModal,
|
||||
setDefaultModalProps,
|
||||
useVbenModal,
|
||||
} from './use-modal';
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { ModalApiOptions, ModalState } from './modal';
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
import { bindMethods, isFunction } from '@vben-core/shared/utils';
|
||||
|
||||
export class ModalApi {
|
||||
export class ModalApi<TData = unknown> {
|
||||
// 共享数据
|
||||
public sharedData: Record<'payload', any> = {
|
||||
payload: {},
|
||||
public sharedData: Record<'payload', TData | undefined> = {
|
||||
payload: undefined,
|
||||
};
|
||||
public store: Store<ModalState>;
|
||||
|
||||
@@ -106,8 +106,8 @@ export class ModalApi {
|
||||
}
|
||||
}
|
||||
|
||||
getData<T extends object = Record<string, any>>() {
|
||||
return (this.sharedData?.payload ?? {}) as T;
|
||||
getData(): TData | undefined {
|
||||
return this.sharedData.payload;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,7 +163,7 @@ export class ModalApi {
|
||||
}));
|
||||
}
|
||||
|
||||
setData<T>(payload: T) {
|
||||
setData(payload: TData) {
|
||||
this.sharedData.payload = payload;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -150,23 +150,36 @@ export interface ModalProps {
|
||||
export interface ModalState extends ModalProps {
|
||||
/** 弹窗打开状态 */
|
||||
isOpen?: boolean;
|
||||
/**
|
||||
* 共享数据
|
||||
*/
|
||||
sharedData?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type ExtendedModalApi = ModalApi & {
|
||||
export type ExtendedModalApi<TData = unknown> = ModalApi<TData> & {
|
||||
useStore: <T = NoInfer<ModalState>>(
|
||||
selector?: (state: NoInfer<ModalState>) => T,
|
||||
) => Readonly<Ref<T>>;
|
||||
};
|
||||
|
||||
export interface ModalApiOptions extends ModalState {
|
||||
type ModalComponentInstance<TComponent extends Component> =
|
||||
TComponent extends abstract new (...args: any[]) => infer TInstance
|
||||
? TInstance
|
||||
: never;
|
||||
|
||||
export type InferModalData<TComponent extends Component> = [
|
||||
ModalComponentInstance<TComponent>,
|
||||
] extends [never]
|
||||
? unknown
|
||||
: ModalComponentInstance<TComponent> extends {
|
||||
modalApi: ExtendedModalApi<infer TData>;
|
||||
}
|
||||
? TData
|
||||
: unknown;
|
||||
|
||||
export interface ModalApiOptions<
|
||||
TConnectedComponent extends Component = Component,
|
||||
> extends ModalState {
|
||||
/**
|
||||
* 独立的弹窗组件
|
||||
*/
|
||||
connectedComponent?: Component;
|
||||
connectedComponent?: TConnectedComponent;
|
||||
/**
|
||||
* 关闭前的回调,返回 false 可以阻止关闭
|
||||
* @returns
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type {
|
||||
ExtendedModalApi,
|
||||
InferModalData,
|
||||
ModalApiOptions,
|
||||
ModalProps,
|
||||
} from './modal';
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
@@ -20,6 +27,26 @@ import VbenModal from './modal.vue';
|
||||
|
||||
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
|
||||
|
||||
declare const MODAL_DATA_NOT_PROVIDED: unique symbol;
|
||||
|
||||
type ModalDataNotProvided = {
|
||||
readonly [MODAL_DATA_NOT_PROVIDED]: true;
|
||||
};
|
||||
|
||||
type ResolvedModalData<
|
||||
TData,
|
||||
TConnectedComponent extends Component,
|
||||
> = TData extends ModalDataNotProvided
|
||||
? InferModalData<TConnectedComponent>
|
||||
: TData;
|
||||
|
||||
interface ModalInjectData<TData> {
|
||||
consumed?: boolean;
|
||||
extendApi?: (api: ExtendedModalApi<TData>) => void;
|
||||
options?: ModalApiOptions;
|
||||
reCreateModal?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const { globalEscapeShortcutKey } = usePreferences();
|
||||
/**
|
||||
* 默认配置
|
||||
@@ -30,9 +57,12 @@ export function setDefaultModalProps(props: Partial<ModalProps>) {
|
||||
Object.assign(DEFAULT_MODAL_PROPS, props);
|
||||
}
|
||||
|
||||
export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
options: ModalApiOptions = {},
|
||||
) {
|
||||
export function useVbenModal<
|
||||
TData = ModalDataNotProvided,
|
||||
TConnectedComponent extends Component = Component,
|
||||
>(options: ModalApiOptions<TConnectedComponent> = {}) {
|
||||
type TResolvedData = ResolvedModalData<TData, TConnectedComponent>;
|
||||
|
||||
// Modal一般会抽离出来,所以如果有传入 connectedComponent,则表示为外部调用,与内部组件进行连接
|
||||
// 外部的Modal通过provide/inject传递api
|
||||
|
||||
@@ -42,11 +72,11 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
};
|
||||
const { connectedComponent } = options;
|
||||
if (connectedComponent) {
|
||||
const extendedApi = shallowReactive({});
|
||||
const extendedApi = shallowReactive({}) as ExtendedModalApi<TResolvedData>;
|
||||
const isModalReady = ref(true);
|
||||
const Modal = defineComponent(
|
||||
(props: TParentModalProps, { attrs, slots }) => {
|
||||
function rebindApi(api: ExtendedModalApi) {
|
||||
(props: ModalProps, { attrs, slots }) => {
|
||||
function rebindApi(api: ExtendedModalApi<TResolvedData>) {
|
||||
Object.setPrototypeOf(extendedApi, markRaw(api));
|
||||
}
|
||||
|
||||
@@ -60,7 +90,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
isModalReady.value = true;
|
||||
},
|
||||
});
|
||||
checkProps(extendedApi as ExtendedModalApi, {
|
||||
checkProps(extendedApi, {
|
||||
...props,
|
||||
...attrs,
|
||||
...slots,
|
||||
@@ -82,10 +112,13 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
},
|
||||
);
|
||||
|
||||
return [Modal, extendedApi as ExtendedModalApi] as const;
|
||||
return [Modal, extendedApi] as const;
|
||||
}
|
||||
|
||||
const injectData = inject<any>(USER_MODAL_INJECT_KEY, {});
|
||||
const injectData = inject<ModalInjectData<TResolvedData>>(
|
||||
USER_MODAL_INJECT_KEY,
|
||||
{},
|
||||
);
|
||||
const isConsumed = injectData.consumed;
|
||||
const effectiveOptions = isConsumed ? {} : injectData.options;
|
||||
if (!isConsumed && injectData.consumed !== undefined) {
|
||||
@@ -121,9 +154,9 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const api = new ModalApi(mergedOptions);
|
||||
const api = new ModalApi<TResolvedData>(mergedOptions);
|
||||
|
||||
const extendedApi: ExtendedModalApi = api as never;
|
||||
const extendedApi = api as ExtendedModalApi<TResolvedData>;
|
||||
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useSelector(api.store, selector);
|
||||
@@ -153,7 +186,18 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||
return [Modal, extendedApi] as const;
|
||||
}
|
||||
|
||||
async function checkProps(api: ExtendedModalApi, attrs: Record<string, any>) {
|
||||
export function createVbenModal<TData = unknown>() {
|
||||
return function useTypedVbenModal<
|
||||
TConnectedComponent extends Component = Component,
|
||||
>(options: ModalApiOptions<TConnectedComponent> = {}) {
|
||||
return useVbenModal<TData, TConnectedComponent>(options);
|
||||
};
|
||||
}
|
||||
|
||||
async function checkProps<TData>(
|
||||
api: ExtendedModalApi<TData>,
|
||||
attrs: Record<string, any>,
|
||||
) {
|
||||
if (!attrs || Object.keys(attrs).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user