diff --git a/components/app/__tests__/__snapshots__/demo.test.js.snap b/components/app/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..cadd37a68 --- /dev/null +++ b/components/app/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/app/demo/basic.vue correctly 1`] = ` +
+ + + + +
+
+ +
+ +
+ +
+
+`; + +exports[`renders ./components/app/demo/myPage.vue correctly 1`] = ` +
+
+ +
+ +
+ +
+`; diff --git a/components/app/__tests__/demo.test.js b/components/app/__tests__/demo.test.js new file mode 100644 index 000000000..fbcb9a76c --- /dev/null +++ b/components/app/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('app'); diff --git a/components/app/context.ts b/components/app/context.ts new file mode 100644 index 000000000..f4210192c --- /dev/null +++ b/components/app/context.ts @@ -0,0 +1,44 @@ +import { provide, inject, reactive } from 'vue'; +import type { InjectionKey } from 'vue'; +import type { MessageInstance, ConfigOptions as MessageConfig } from '../message/interface'; +import type { NotificationInstance, NotificationConfig } from '../notification/interface'; +import type { ModalStaticFunctions } from '../modal/confirm'; + +export type AppConfig = { + message?: MessageConfig; + notification?: NotificationConfig; +}; + +export const AppConfigContextKey: InjectionKey = Symbol('appConfigContext'); + +export const useProvideAppConfigContext = (appConfigContext: AppConfig) => { + return provide(AppConfigContextKey, appConfigContext); +}; + +export const useInjectAppConfigContext = () => { + return inject(AppConfigContextKey, {}); +}; + +type ModalType = Omit; + +export interface useAppProps { + message: MessageInstance; + notification: NotificationInstance; + modal: ModalType; +} + +export const AppContextKey: InjectionKey = Symbol('appContext'); + +export const useProvideAppContext = (appContext: useAppProps) => { + return provide(AppContextKey, appContext); +}; + +const defaultAppContext: useAppProps = reactive({ + message: {}, + notification: {}, + modal: {}, +} as useAppProps); + +export const useInjectAppContext = () => { + return inject(AppContextKey, defaultAppContext); +}; diff --git a/components/app/demo/basic.vue b/components/app/demo/basic.vue new file mode 100644 index 000000000..7b0450e49 --- /dev/null +++ b/components/app/demo/basic.vue @@ -0,0 +1,26 @@ + +--- +order: 0 +title: + zh-CN: 基本使用 + en-US: Basic Usage +--- + +## zh-CN + +获取 `message`、`notification`、`modal` 静态方法。 + +## en-US + +Static method for `message`, `notification`, `modal`. + + + + + diff --git a/components/app/demo/index.vue b/components/app/demo/index.vue new file mode 100644 index 000000000..63a848fd2 --- /dev/null +++ b/components/app/demo/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/app/demo/myPage.vue b/components/app/demo/myPage.vue new file mode 100644 index 000000000..163b5e81b --- /dev/null +++ b/components/app/demo/myPage.vue @@ -0,0 +1,32 @@ + + + diff --git a/components/app/index.en-US.md b/components/app/index.en-US.md new file mode 100644 index 000000000..4c04d1cff --- /dev/null +++ b/components/app/index.en-US.md @@ -0,0 +1,129 @@ +--- +category: Components +cols: 1 +type: Other +title: App +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*TBTSR4PyVmkAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*JGb3RIzyOCkAAAAAAAAAAAAADrJ8AQ/original +--- + +Application wrapper for some global usages. + +## When To Use + +- Provide reset styles based on `.ant-app` element. +- You could use static methods of `message/notification/Modal` form `useApp` without writing `contextHolder` manually. + +## API + +### App + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| message | Global config for Message | [MessageConfig](/components/message/#messageconfig) | - | 4.x | +| notification | Global config for Notification | [NotificationConfig](/components/notification/#notificationconfig) | - | 4.x | + +## How to use + +### Basic usage + +App provides upstream and downstream method calls through `provide/inject`, because useApp needs to be used as a subcomponent, we recommend encapsulating App at the top level in the application. + +```html +/*myPage.vue*/ + + + +``` + +Note: App.useApp must be available under App. + +#### Embedded usage scenarios (if not necessary, try not to do nesting) + +```html + + + ... + ... + + +``` + +#### Sequence with ConfigProvider + +The App component can only use the token in the `ConfigProvider`, if you need to use the Token, the ConfigProvider and the App component must appear in pairs. + +```html + + ... + +``` + +#### Global scene (pinia scene) + +```ts +import { App } from 'ant-design-vue'; +import type { MessageInstance } from 'ant-design-vue/es/message/interface'; +import type { ModalStaticFunctions } from 'ant-design-vue/es/modal/confirm'; +import type { NotificationInstance } from 'ant-design-vue/es/notification/interface'; + +export const useGloablStore = defineStore('global', () => { + const message: MessageInstance = ref(); + const notification: NotificationInstance = ref(); + const modal: Omit = ref(); + (() => { + const staticFunction = App.useApp(); + message.value = staticFunction.message; + modal.value = staticFunction.modal; + notification.value = staticFunction.notification; + })(); + + return { message, notification, modal }; +}); +``` + +```html +// sub page + + + +``` diff --git a/components/app/index.tsx b/components/app/index.tsx new file mode 100644 index 000000000..0ee269a94 --- /dev/null +++ b/components/app/index.tsx @@ -0,0 +1,83 @@ +import { defineComponent, computed } from 'vue'; +import type { App as TypeApp, Plugin } from 'vue'; +import { initDefaultProps } from '../_util/props-util'; +import classNames from '../_util/classNames'; +import { objectType } from '../_util/type'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import useMessage from '../message/useMessage'; +import useModal from '../modal/useModal'; +import useNotification from '../notification/useNotification'; +import type { AppConfig } from './context'; +import { + useProvideAppConfigContext, + useInjectAppConfigContext, + useProvideAppContext, + useInjectAppContext, +} from './context'; +import useStyle from './style'; + +export const AppProps = () => { + return { + rootClassName: String, + message: objectType(), + notification: objectType(), + }; +}; + +const useApp = () => { + return useInjectAppContext(); +}; + +const App = defineComponent({ + name: 'AApp', + props: initDefaultProps(AppProps(), {}), + setup(props, { slots }) { + const { prefixCls } = useConfigInject('app', props); + const [wrapSSR, hashId] = useStyle(prefixCls); + const customClassName = computed(() => { + return classNames(hashId.value, prefixCls.value, props.rootClassName); + }); + + const appConfig = useInjectAppConfigContext(); + const mergedAppConfig = computed(() => ({ + message: { ...appConfig.message, ...props.message }, + notification: { ...appConfig.notification, ...props.notification }, + })); + useProvideAppConfigContext(mergedAppConfig.value); + + const [messageApi, messageContextHolder] = useMessage(mergedAppConfig.value.message); + const [notificationApi, notificationContextHolder] = useNotification( + mergedAppConfig.value.notification, + ); + const [ModalApi, ModalContextHolder] = useModal(); + + const memoizedContextValue = computed(() => ({ + message: messageApi, + notification: notificationApi, + modal: ModalApi, + })); + useProvideAppContext(memoizedContextValue.value); + + return () => { + return wrapSSR( +
+ {ModalContextHolder()} + {messageContextHolder()} + {notificationContextHolder()} + {slots.default?.()} +
, + ); + }; + }, +}); + +App.useApp = useApp; + +App.install = function (app: TypeApp) { + app.component(App.name, App); +}; + +export default App as typeof App & + Plugin & { + readonly useApp: typeof useApp; + }; diff --git a/components/app/index.zh-CN.md b/components/app/index.zh-CN.md new file mode 100644 index 000000000..e362ee065 --- /dev/null +++ b/components/app/index.zh-CN.md @@ -0,0 +1,130 @@ +--- +category: Components +subtitle: 包裹组件 +cols: 1 +type: 其它 +title: App +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*TBTSR4PyVmkAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*JGb3RIzyOCkAAAAAAAAAAAAADrJ8AQ/original +--- + +新的包裹组件,提供重置样式和提供消费上下文的默认环境。 + +## 何时使用 + +- 提供可消费 provide/inject 的 `message.xxx`、`Modal.xxx`、`notification.xxx` 的静态方法,可以简化 useMessage 等方法需要手动植入 `contextHolder` 的问题。 +- 提供基于 `.ant-app` 的默认重置样式,解决原生元素没有 antd 规范样式的问题。 + +## API + +### App + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| message | App 内 Message 的全局配置 | [MessageConfig](/components/message-cn/#messageconfig) | - | 4.x | +| notification | App 内 Notification 的全局配置 | [NotificationConfig](/components/notification-cn/#notificationconfig) | - | 4.x | + +## 如何使用 + +### 基础用法 + +App 组件通过 `provide/inject` 提供上下文方法调用,因而 useApp 需要作为子组件才能使用,我们推荐在应用中顶层包裹 App。 + +```html +/*myPage.vue*/ + + + +``` + +注意:App.useApp 必须在 App 之下方可使用。 + +#### 内嵌使用场景(如无必要,尽量不做嵌套) + +```html + + + ... + ... + + +``` + +#### 与 ConfigProvider 先后顺序 + +App 组件只能在 `ConfigProvider` 之下才能使用 Design Token, 如果需要使用其样式重置能力,则 ConfigProvider 与 App 组件必须成对出现。 + +```html + + ... + +``` + +#### 全局场景 (pinia 场景) + +```ts +import { App } from 'ant-design-vue'; +import type { MessageInstance } from 'ant-design-vue/es/message/interface'; +import type { ModalStaticFunctions } from 'ant-design-vue/es/modal/confirm'; +import type { NotificationInstance } from 'ant-design-vue/es/notification/interface'; + +export const useGloablStore = defineStore('global', () => { + const message: MessageInstance = ref(); + const notification: NotificationInstance = ref(); + const modal: Omit = ref(); + (() => { + const staticFunction = App.useApp(); + message.value = staticFunction.message; + modal.value = staticFunction.modal; + notification.value = staticFunction.notification; + })(); + + return { message, notification, modal }; +}); +``` + +```html +// sub page + + + +``` diff --git a/components/app/style/index.ts b/components/app/style/index.ts new file mode 100644 index 000000000..031b0ffd0 --- /dev/null +++ b/components/app/style/index.ts @@ -0,0 +1,22 @@ +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook } from '../../theme/internal'; + +export type ComponentToken = {}; + +interface AppToken extends FullToken<'App'> {} + +// =============================== Base =============================== +const genBaseStyle: GenerateStyle = token => { + const { componentCls, colorText, fontSize, lineHeight, fontFamily } = token; + return { + [componentCls]: { + color: colorText, + fontSize, + lineHeight, + fontFamily, + }, + }; +}; + +// ============================== Export ============================== +export default genComponentStyleHook('App', token => [genBaseStyle(token)]); diff --git a/components/components.ts b/components/components.ts index 7b741d1c6..2118711be 100644 --- a/components/components.ts +++ b/components/components.ts @@ -261,3 +261,6 @@ export { default as QRCode } from './qrcode'; export type { TourProps, TourStepProps } from './tour'; export { default as Tour } from './tour'; + +export type { AppProps } from './app'; +export { default as App } from './app'; diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts index 92f2fffb8..329cba4fd 100644 --- a/components/theme/interface/components.ts +++ b/components/theme/interface/components.ts @@ -46,7 +46,7 @@ import type { ComponentToken as TypographyComponentToken } from '../../typograph import type { ComponentToken as UploadComponentToken } from '../../upload/style'; import type { ComponentToken as TourComponentToken } from '../../tour/style'; import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style'; -// import type { ComponentToken as AppComponentToken } from '../../app/style'; +import type { ComponentToken as AppComponentToken } from '../../app/style'; import type { ComponentToken as WaveToken } from '../../_util/wave/style'; export interface ComponentTokenMap { @@ -112,7 +112,7 @@ export interface ComponentTokenMap { Progress?: ProgressComponentToken; Tour?: TourComponentToken; QRCode?: QRCodeComponentToken; - // App?: AppComponentToken; + App?: AppComponentToken; // /** @private Internal TS definition. Do not use. */ Wave?: WaveToken;