Skip to content

feat(modal): add useModal #6517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions components/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ export interface ModalLocale {
justOkText: string;
}

export const destroyFns = [];

export default defineComponent({
compatConfig: { MODE: 3 },
name: 'AModal',
Expand All @@ -167,6 +165,7 @@ export default defineComponent({
props,
);
const [wrapSSR, hashId] = useStyle(prefixCls);

warning(
props.visible === undefined,
'Modal',
Expand Down
12 changes: 12 additions & 0 deletions components/modal/__tests__/__snapshots__/demo.test.js.snap
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
3 changes: 2 additions & 1 deletion components/modal/confirm.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
101 changes: 101 additions & 0 deletions components/modal/demo/HookModal.vue
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 3 additions & 0 deletions components/modal/demo/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<custom-footer />
<confirm />
<info />
<HookModal />
<locale />
<manual />
<position />
Expand All @@ -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';
Expand All @@ -52,6 +54,7 @@ export default defineComponent({
ButtonProps,
Fullscreen,
modalRenderVue,
HookModal,
},
setup() {
return {};
Expand Down
2 changes: 2 additions & 0 deletions components/modal/destroyFns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const destroyFns: Array<Function> = [];
export default destroyFns;
45 changes: 34 additions & 11 deletions components/modal/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```tsx
import { getCurrentInstance } from 'vue';

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,
});
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:

```html
<template>
<contextHolder />
<!-- <component :is='contextHolder'/> -->
</template>
<script setup>
import { Modal } from 'ant-design-vue';
const [modal, contextHolder] = Modal.useModal();

modal.confirm({
// ...
});
</script>
```
9 changes: 6 additions & 3 deletions components/modal/index.tsx
Original file line number Diff line number Diff line change
@@ -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));
};
Expand Down Expand Up @@ -60,4 +61,6 @@ export default Modal as typeof Modal &
readonly confirm: ModalFunc;

readonly destroyAll: () => void;

readonly useModal: typeof useModal;
};
45 changes: 34 additions & 11 deletions components/modal/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 当你需要保留属性响应式时,你可以使用函数返回:

```tsx
import { getCurrentInstance } from 'vue';

const appContext = getCurrentInstance().appContext;
const title = ref('some message');
Modal.confirm({
title: () => title.value, // 此时 title 的改变,会同步更新 confirm 中的 title
appContext,
});
当你需要 context 信息(例如使用全局注册的组件)时,可以通过 Modal.useModal 方法会返回 modal 实体以及 contextHolder 节点。将其插入到你需要获取 context 位置即可:

```html
<template>
<contextHolder />
<!-- <component :is='contextHolder'/> -->
</template>
<script setup>
import { Modal } from 'ant-design-vue';
const [modal, contextHolder] = Modal.useModal();

modal.confirm({
// ...
});
</script>
```
73 changes: 73 additions & 0 deletions components/modal/useModal/HookModal.tsx
Original file line number Diff line number Diff line change
@@ -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}
/>
);
},
});
Loading