feat(modal): add useModal (#6517)
* feat(modal): add useModal hook * feat(modal): add HookModal demo * test(modal): update HookModal demo snap * feat(modal): update modal docs * chore(modal): update modal typepull/6527/head
parent
0df6abe3a7
commit
69640c0af8
|
@ -149,8 +149,6 @@ export interface ModalLocale {
|
|||
justOkText: string;
|
||||
}
|
||||
|
||||
export const destroyFns = [];
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'AModal',
|
||||
|
@ -167,6 +165,7 @@ export default defineComponent({
|
|||
props,
|
||||
);
|
||||
const [wrapSSR, hashId] = useStyle(prefixCls);
|
||||
|
||||
warning(
|
||||
props.visible === undefined,
|
||||
'Modal',
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./components/modal/demo/HookModal.vue correctly 1`] = `
|
||||
<div><button class="ant-btn ant-btn-default" type="button">
|
||||
<!----><span>Confirm</span>
|
||||
</button><button class="ant-btn ant-btn-default" type="button">
|
||||
<!----><span>With promise</span>
|
||||
</button><button class="ant-btn ant-btn-dashed" type="button">
|
||||
<!----><span>Delete</span>
|
||||
</button><button class="ant-btn ant-btn-dashed" type="button">
|
||||
<!----><span>With extra props</span>
|
||||
</button></div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/modal/demo/async.vue correctly 1`] = `
|
||||
<div><button class="ant-btn ant-btn-primary" type="button">
|
||||
<!----><span>Open Modal with async logic</span>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { createVNode, render as vueRender } from 'vue';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import type { ModalFuncProps } from './Modal';
|
||||
import { destroyFns } from './Modal';
|
||||
import ConfigProvider, { globalConfigForApi } from '../config-provider';
|
||||
import omit from '../_util/omit';
|
||||
|
||||
import { getConfirmLocale } from './locale';
|
||||
import destroyFns from './destroyFns';
|
||||
|
||||
type ConfigUpdate = ModalFuncProps | ((prevConfig: ModalFuncProps) => ModalFuncProps);
|
||||
export type ModalStaticFunctions<T = ModalFunc> = Record<NonNullable<ModalFuncProps['type']>, T>;
|
||||
|
||||
export type ModalFunc = (props: ModalFuncProps) => {
|
||||
destroy: () => void;
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<docs>
|
||||
---
|
||||
order: 12
|
||||
title:
|
||||
zh-CN: 使用useModal获取上下文
|
||||
en-US: Use useModal to get context
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `Modal.useModal` to get `contextHolder` with context accessible issue.
|
||||
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-button @click="showConfirm">Confirm</a-button>
|
||||
<a-button @click="showPromiseConfirm">With promise</a-button>
|
||||
<a-button type="dashed" @click="showDeleteConfirm">Delete</a-button>
|
||||
<a-button type="dashed" @click="showPropsConfirm">With extra props</a-button>
|
||||
<contextHolder />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { createVNode } from 'vue';
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const showConfirm = () => {
|
||||
modal.confirm({
|
||||
title: 'Do you Want to delete these items?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: createVNode('div', { style: 'color:red;' }, 'Some descriptions'),
|
||||
onOk() {
|
||||
console.log('OK');
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
class: 'test',
|
||||
});
|
||||
};
|
||||
const showDeleteConfirm = () => {
|
||||
modal.confirm({
|
||||
title: 'Are you sure delete this task?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: 'Some descriptions',
|
||||
okText: 'Yes',
|
||||
okType: 'danger',
|
||||
cancelText: 'No',
|
||||
onOk() {
|
||||
console.log('OK');
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
const showPropsConfirm = () => {
|
||||
modal.confirm({
|
||||
title: 'Are you sure delete this task?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: 'Some descriptions',
|
||||
okText: 'Yes',
|
||||
okType: 'danger',
|
||||
okButtonProps: {
|
||||
disabled: true,
|
||||
},
|
||||
cancelText: 'No',
|
||||
onOk() {
|
||||
console.log('OK');
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function showPromiseConfirm() {
|
||||
modal.confirm({
|
||||
title: 'Do you want to delete these items?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: 'When clicked the OK button, this dialog will be closed after 1 second',
|
||||
async onOk() {
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(Math.random() > 0.5 ? resolve : reject, 1000);
|
||||
});
|
||||
} catch {
|
||||
return console.log('Oops errors!');
|
||||
}
|
||||
},
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -5,6 +5,7 @@
|
|||
<custom-footer />
|
||||
<confirm />
|
||||
<info />
|
||||
<HookModal />
|
||||
<locale />
|
||||
<manual />
|
||||
<position />
|
||||
|
@ -31,6 +32,7 @@ import Width from './width.vue';
|
|||
import Fullscreen from './fullscreen.vue';
|
||||
import ButtonProps from './button-props.vue';
|
||||
import modalRenderVue from './modal-render.vue';
|
||||
import HookModal from './HookModal.vue';
|
||||
import CN from '../index.zh-CN.md';
|
||||
import US from '../index.en-US.md';
|
||||
import { defineComponent } from 'vue';
|
||||
|
@ -52,6 +54,7 @@ export default defineComponent({
|
|||
ButtonProps,
|
||||
Fullscreen,
|
||||
modalRenderVue,
|
||||
HookModal,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
const destroyFns: Array<Function> = [];
|
||||
export default destroyFns;
|
|
@ -116,21 +116,44 @@ router.beforeEach((to, from, next) => {
|
|||
})
|
||||
```
|
||||
|
||||
### Modal.useModal()
|
||||
|
||||
When you need using Context, you can use `contextHolder` which created by `Modal.useModal` to insert into children. Modal created by hooks will get all the context where `contextHolder` are. Created `modal` has the same creating function with `Modal.method`.
|
||||
|
||||
```html
|
||||
<template>
|
||||
<contextHolder />
|
||||
<!-- <component :is='contextHolder'/> -->
|
||||
</template>
|
||||
<script setup>
|
||||
import { Modal } from 'ant-design-vue';
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
modal.confirm({
|
||||
// ...
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why can't the Modal method obtain global registered components, context, vuex, etc. and ConfigProvider `locale/prefixCls/theme` configuration, and can't update data responsively?
|
||||
|
||||
Call the Modal method directly, and the component will dynamically create a new Vue entity through `Vue.render`. Its context is not the same as the context where the current code is located, so the context information cannot be obtained.
|
||||
|
||||
When you need context information (for example, using a globally registered component), you can pass the current component context through the `appContext` property. When you need to keep the property responsive, you can use the function to return:
|
||||
When you need context information (for example, using a globally registered component), you can use `Modal.useModal` to get `modal` instance and `contextHolder` node. And put it in your children:
|
||||
|
||||
```tsx
|
||||
import { getCurrentInstance } from 'vue';
|
||||
```html
|
||||
<template>
|
||||
<contextHolder />
|
||||
<!-- <component :is='contextHolder'/> -->
|
||||
</template>
|
||||
<script setup>
|
||||
import { Modal } from 'ant-design-vue';
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
const appContext = getCurrentInstance().appContext;
|
||||
const title = ref('some message');
|
||||
Modal.confirm({
|
||||
title: () => title.value, // the change of title will update the title in confirm synchronously
|
||||
appContext,
|
||||
});
|
||||
modal.confirm({
|
||||
// ...
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import type { App, Plugin } from 'vue';
|
||||
import type { ModalFunc, ModalFuncProps } from './Modal';
|
||||
import Modal, { destroyFns } from './Modal';
|
||||
import Modal from './Modal';
|
||||
import confirm, { withWarn, withInfo, withSuccess, withError, withConfirm } from './confirm';
|
||||
|
||||
import useModal from './useModal';
|
||||
import destroyFns from './destroyFns';
|
||||
export type { ActionButtonProps } from '../_util/ActionButton';
|
||||
export type { ModalProps, ModalFuncProps } from './Modal';
|
||||
|
||||
function modalWarn(props: ModalFuncProps) {
|
||||
return confirm(withWarn(props));
|
||||
}
|
||||
|
||||
Modal.useModal = useModal;
|
||||
Modal.info = function infoFn(props: ModalFuncProps) {
|
||||
return confirm(withInfo(props));
|
||||
};
|
||||
|
@ -60,4 +61,6 @@ export default Modal as typeof Modal &
|
|||
readonly confirm: ModalFunc;
|
||||
|
||||
readonly destroyAll: () => void;
|
||||
|
||||
readonly useModal: typeof useModal;
|
||||
};
|
||||
|
|
|
@ -120,21 +120,44 @@ router.beforeEach((to, from, next) => {
|
|||
})
|
||||
```
|
||||
|
||||
### Modal.useModal()
|
||||
|
||||
当你需要使用 Context 时,可以通过 `Modal.useModal` 创建一个 `contextHolder` 插入子节点中。通过 hooks 创建的临时 Modal 将会得到 `contextHolder` 所在位置的所有上下文。创建的 `modal` 对象拥有与 [`Modal.method`](#modalmethod) 相同的创建通知方法。
|
||||
|
||||
```html
|
||||
<template>
|
||||
<contextHolder />
|
||||
<!-- <component :is='contextHolder'/> -->
|
||||
</template>
|
||||
<script setup>
|
||||
import { Modal } from 'ant-design-vue';
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
modal.confirm({
|
||||
// ...
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### 为什么 Modal 方法不能获取 全局注册组件、context、vuex 等内容和 ConfigProvider `locale/prefixCls/theme` 配置, 以及不能响应式更新数据 ?
|
||||
|
||||
直接调用 Modal 方法,组件会通过 `Vue.render` 动态创建新的 Vue 实体。其 context 与当前代码所在 context 并不相同,因而无法获取 context 信息。
|
||||
|
||||
当你需要 context 信息(例如使用全局注册的组件)时,可以通过 `appContext` 属性传递当前组件 context, 当你需要保留属性响应式时,你可以使用函数返回:
|
||||
当你需要 context 信息(例如使用全局注册的组件)时,可以通过 Modal.useModal 方法会返回 modal 实体以及 contextHolder 节点。将其插入到你需要获取 context 位置即可:
|
||||
|
||||
```tsx
|
||||
import { getCurrentInstance } from 'vue';
|
||||
```html
|
||||
<template>
|
||||
<contextHolder />
|
||||
<!-- <component :is='contextHolder'/> -->
|
||||
</template>
|
||||
<script setup>
|
||||
import { Modal } from 'ant-design-vue';
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
const appContext = getCurrentInstance().appContext;
|
||||
const title = ref('some message');
|
||||
Modal.confirm({
|
||||
title: () => title.value, // 此时 title 的改变,会同步更新 confirm 中的 title
|
||||
appContext,
|
||||
});
|
||||
modal.confirm({
|
||||
// ...
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import type { PropType } from 'vue';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { useConfigContextInject } from '../../config-provider/context';
|
||||
import { useLocaleReceiver } from '../../locale-provider/LocaleReceiver';
|
||||
import defaultLocale from '../../locale/en_US';
|
||||
import ConfirmDialog from '../ConfirmDialog';
|
||||
import type { ModalFuncProps } from '../Modal';
|
||||
import initDefaultProps from '../../_util/props-util/initDefaultProps';
|
||||
export interface HookModalProps {
|
||||
afterClose: () => void;
|
||||
config: ModalFuncProps;
|
||||
destroyAction: (...args: any[]) => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export interface HookModalRef {
|
||||
destroy: () => void;
|
||||
update: (config: ModalFuncProps) => void;
|
||||
}
|
||||
|
||||
const comfirmFuncProps = () => ({
|
||||
config: Object as PropType<ModalFuncProps>,
|
||||
afterClose: Function as PropType<() => void>,
|
||||
destroyAction: Function as PropType<(e: any) => void>,
|
||||
open: Boolean,
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HookModal',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(comfirmFuncProps(), {
|
||||
config: {
|
||||
width: 520,
|
||||
okType: 'primary',
|
||||
},
|
||||
}),
|
||||
setup(props: HookModalProps, { expose }) {
|
||||
const open = computed(() => props.open);
|
||||
const innerConfig = computed(() => props.config);
|
||||
const { direction, getPrefixCls } = useConfigContextInject();
|
||||
const prefixCls = getPrefixCls('modal');
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
|
||||
const afterClose = () => {
|
||||
props?.afterClose();
|
||||
innerConfig.value.afterClose?.();
|
||||
};
|
||||
|
||||
const close = (...args: any[]) => {
|
||||
props.destroyAction(...args);
|
||||
};
|
||||
|
||||
expose({ destroy: close });
|
||||
const mergedOkCancel = innerConfig.value.okCancel ?? innerConfig.value.type === 'confirm';
|
||||
const [contextLocale] = useLocaleReceiver('Modal', defaultLocale.Modal);
|
||||
return () => (
|
||||
<ConfirmDialog
|
||||
prefixCls={prefixCls}
|
||||
rootPrefixCls={rootPrefixCls}
|
||||
{...innerConfig.value}
|
||||
close={close}
|
||||
open={open.value}
|
||||
afterClose={afterClose}
|
||||
okText={
|
||||
innerConfig.value.okText ||
|
||||
(mergedOkCancel ? contextLocale?.value.okText : contextLocale?.value.justOkText)
|
||||
}
|
||||
direction={innerConfig.value.direction || direction.value}
|
||||
cancelText={innerConfig.value.cancelText || contextLocale?.value.cancelText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,150 @@
|
|||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { isRef, unref, computed, defineComponent, ref, watch } from 'vue';
|
||||
import type { VueNode } from '../../_util/type';
|
||||
import type { ModalFuncProps } from '../Modal';
|
||||
import type { HookModalRef } from './HookModal';
|
||||
import type { ModalStaticFunctions } from '../confirm';
|
||||
import { withConfirm, withError, withInfo, withSuccess, withWarn } from '../confirm';
|
||||
|
||||
import HookModal from './HookModal';
|
||||
import destroyFns from '../destroyFns';
|
||||
|
||||
let uuid = 0;
|
||||
|
||||
interface ElementsHolderRef {
|
||||
addModal: (modal: ComputedRef<JSX.Element>) => () => void;
|
||||
}
|
||||
|
||||
const ElementsHolder = defineComponent({
|
||||
name: 'ElementsHolder',
|
||||
inheritAttrs: false,
|
||||
setup(_, { expose }) {
|
||||
const modals = ref<ComputedRef<JSX.Element>[]>([]);
|
||||
const addModal = (modal: ComputedRef<JSX.Element>) => {
|
||||
modals.value.push(modal);
|
||||
return () => {
|
||||
modals.value = modals.value.filter(currentModal => currentModal !== modal);
|
||||
};
|
||||
};
|
||||
|
||||
expose({ addModal });
|
||||
return () => {
|
||||
return <>{modals.value.map(modal => modal.value)}</>;
|
||||
};
|
||||
},
|
||||
});
|
||||
export type ModalFuncWithRef = (props: Ref<ModalFuncProps> | ModalFuncProps) => {
|
||||
destroy: () => void;
|
||||
update: (configUpdate: ModalFuncProps) => void;
|
||||
};
|
||||
|
||||
function useModal(): readonly [
|
||||
Omit<ModalStaticFunctions<ModalFuncWithRef>, 'warn'>,
|
||||
() => VueNode,
|
||||
] {
|
||||
const holderRef = ref<ElementsHolderRef>(null);
|
||||
// ========================== Effect ==========================
|
||||
const actionQueue = ref([]);
|
||||
watch(
|
||||
actionQueue,
|
||||
() => {
|
||||
if (actionQueue.value.length) {
|
||||
const cloneQueue = [...actionQueue.value];
|
||||
cloneQueue.forEach(action => {
|
||||
action();
|
||||
});
|
||||
actionQueue.value = [];
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
// =========================== Hook ===========================
|
||||
const getConfirmFunc = (withFunc: (config: ModalFuncProps) => ModalFuncProps) =>
|
||||
function hookConfirm(config: Ref<ModalFuncProps> | ModalFuncProps) {
|
||||
uuid += 1;
|
||||
const open = ref(true);
|
||||
const modalRef = ref<HookModalRef>(null);
|
||||
const configRef = ref(unref(config));
|
||||
const updateConfig = ref({});
|
||||
watch(
|
||||
() => config,
|
||||
val => {
|
||||
updateAction({
|
||||
...(isRef(val) ? val.value : val),
|
||||
...updateConfig.value,
|
||||
});
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line prefer-const
|
||||
let closeFunc: Function | undefined;
|
||||
const modal = computed(() => (
|
||||
<HookModal
|
||||
key={`modal-${uuid}`}
|
||||
config={withFunc(configRef.value)}
|
||||
ref={modalRef}
|
||||
open={open.value}
|
||||
destroyAction={destroyAction}
|
||||
afterClose={() => {
|
||||
closeFunc?.();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
closeFunc = holderRef.value?.addModal(modal);
|
||||
|
||||
if (closeFunc) {
|
||||
destroyFns.push(closeFunc);
|
||||
}
|
||||
|
||||
const destroyAction = (...args: any[]) => {
|
||||
open.value = false;
|
||||
const triggerCancel = args.some(param => param && param.triggerCancel);
|
||||
if (configRef.value.onCancel && triggerCancel) {
|
||||
configRef.value.onCancel(() => {}, ...args.slice(1));
|
||||
}
|
||||
};
|
||||
|
||||
const updateAction = (newConfig: ModalFuncProps) => {
|
||||
configRef.value = {
|
||||
...configRef.value,
|
||||
...newConfig,
|
||||
};
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
if (modalRef.value) {
|
||||
destroyAction();
|
||||
} else {
|
||||
actionQueue.value = [...actionQueue.value, destroyAction];
|
||||
}
|
||||
};
|
||||
|
||||
const update = (newConfig: ModalFuncProps) => {
|
||||
updateConfig.value = newConfig;
|
||||
if (modalRef.value) {
|
||||
updateAction(newConfig);
|
||||
} else {
|
||||
actionQueue.value = [...actionQueue.value, () => updateAction(newConfig)];
|
||||
}
|
||||
};
|
||||
return {
|
||||
destroy,
|
||||
update,
|
||||
};
|
||||
};
|
||||
|
||||
const fns = computed(() => ({
|
||||
info: getConfirmFunc(withInfo),
|
||||
success: getConfirmFunc(withSuccess),
|
||||
error: getConfirmFunc(withError),
|
||||
warning: getConfirmFunc(withWarn),
|
||||
confirm: getConfirmFunc(withConfirm),
|
||||
}));
|
||||
|
||||
return [fns.value, () => <ElementsHolder key="modal-holder" ref={holderRef} />] as const;
|
||||
}
|
||||
|
||||
export default useModal;
|
Loading…
Reference in New Issue