refactor: form
parent
b68bb81652
commit
c2bba2eb28
|
@ -1,3 +1,4 @@
|
|||
import { RequiredMark } from '../../form/Form';
|
||||
import { computed, ComputedRef, inject, UnwrapRef } from 'vue';
|
||||
import {
|
||||
ConfigProviderProps,
|
||||
|
@ -17,6 +18,9 @@ export default (
|
|||
getTargetContainer: ComputedRef<() => HTMLElement>;
|
||||
space: ComputedRef<{ size: SizeType | number }>;
|
||||
pageHeader: ComputedRef<{ ghost: boolean }>;
|
||||
form?: ComputedRef<{
|
||||
requiredMark?: RequiredMark;
|
||||
}>;
|
||||
} => {
|
||||
const configProvider = inject<UnwrapRef<ConfigProviderProps>>(
|
||||
'configProvider',
|
||||
|
@ -26,7 +30,17 @@ export default (
|
|||
const direction = computed(() => configProvider.direction);
|
||||
const space = computed(() => configProvider.space);
|
||||
const pageHeader = computed(() => configProvider.pageHeader);
|
||||
const form = computed(() => configProvider.form);
|
||||
const size = computed(() => props.size || configProvider.componentSize);
|
||||
const getTargetContainer = computed(() => props.getTargetContainer);
|
||||
return { configProvider, prefixCls, direction, size, getTargetContainer, space, pageHeader };
|
||||
return {
|
||||
configProvider,
|
||||
prefixCls,
|
||||
direction,
|
||||
size,
|
||||
getTargetContainer,
|
||||
space,
|
||||
pageHeader,
|
||||
form,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import LocaleProvider, { Locale, ANT_MARK } from '../locale-provider';
|
|||
import { TransformCellTextProps } from '../table/interface';
|
||||
import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
||||
import { withInstall } from '../_util/type';
|
||||
import { RequiredMark } from '../form/Form';
|
||||
|
||||
export type SizeType = 'small' | 'middle' | 'large' | undefined;
|
||||
|
||||
|
@ -99,6 +100,9 @@ export const configProviderProps = {
|
|||
},
|
||||
virtual: PropTypes.looseBool,
|
||||
dropdownMatchSelectWidth: PropTypes.looseBool,
|
||||
form: {
|
||||
type: Object as PropType<{ requiredMark?: RequiredMark }>,
|
||||
},
|
||||
};
|
||||
|
||||
export type ConfigProviderProps = Partial<ExtractPropTypes<typeof configProviderProps>>;
|
||||
|
@ -159,7 +163,7 @@ const ConfigProvider = defineComponent({
|
|||
export const defaultConfigProvider: UnwrapRef<ConfigProviderProps> = reactive({
|
||||
getPrefixCls: (suffixCls: string, customizePrefixCls?: string) => {
|
||||
if (customizePrefixCls) return customizePrefixCls;
|
||||
return `ant-${suffixCls}`;
|
||||
return suffixCls ? `ant-${suffixCls}` : 'ant';
|
||||
},
|
||||
renderEmpty: defaultRenderEmpty,
|
||||
direction: 'ltr',
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { useInjectFormItemPrefix } from './context';
|
||||
import { VueNode } from '../_util/type';
|
||||
import { computed, defineComponent, ref, watch } from '@vue/runtime-core';
|
||||
import classNames from '../_util/classNames';
|
||||
import Transition, { getTransitionProps } from '../_util/transition';
|
||||
|
||||
export interface ErrorListProps {
|
||||
errors?: VueNode[];
|
||||
/** @private Internal Usage. Do not use in your production */
|
||||
help?: VueNode;
|
||||
/** @private Internal Usage. Do not use in your production */
|
||||
onDomErrorVisibleChange?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export default defineComponent<ErrorListProps>({
|
||||
name: 'ErrorList',
|
||||
setup(props) {
|
||||
const { prefixCls, status } = useInjectFormItemPrefix();
|
||||
const visible = computed(() => props.errors && props.errors.length);
|
||||
const innerStatus = ref(status.value);
|
||||
// Memo status in same visible
|
||||
watch([() => visible, () => status], () => {
|
||||
if (visible.value && status.value) {
|
||||
innerStatus.value = status.value;
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
const baseClassName = `${prefixCls.value}-item-explain`;
|
||||
const transitionProps = getTransitionProps('show-help', {
|
||||
onAfterLeave: () => props.onDomErrorVisibleChange?.(false),
|
||||
});
|
||||
return (
|
||||
<Transition {...transitionProps}>
|
||||
{visible ? (
|
||||
<div
|
||||
class={classNames(baseClassName, {
|
||||
[`${baseClassName}-${innerStatus}`]: innerStatus,
|
||||
})}
|
||||
key="help"
|
||||
>
|
||||
{props.errors?.map((error: any, index: number) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,4 +1,12 @@
|
|||
import { defineComponent, inject, provide, PropType, computed, ExtractPropTypes } from 'vue';
|
||||
import {
|
||||
defineComponent,
|
||||
inject,
|
||||
provide,
|
||||
PropType,
|
||||
computed,
|
||||
ExtractPropTypes,
|
||||
HTMLAttributes,
|
||||
} from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import classNames from '../_util/classNames';
|
||||
import warning from '../_util/warning';
|
||||
|
@ -16,6 +24,9 @@ import { tuple, VueNode } from '../_util/type';
|
|||
import { ColProps } from '../grid/Col';
|
||||
import { InternalNamePath, NamePath, ValidateErrorEntity, ValidateOptions } from './interface';
|
||||
|
||||
export type RequiredMark = boolean | 'optional';
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
|
||||
export type ValidationRule = {
|
||||
/** validation error message */
|
||||
message?: VueNode;
|
||||
|
@ -45,11 +56,13 @@ export type ValidationRule = {
|
|||
|
||||
export const formProps = {
|
||||
layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical')),
|
||||
labelCol: { type: Object as PropType<ColProps> },
|
||||
wrapperCol: { type: Object as PropType<ColProps> },
|
||||
labelCol: { type: Object as PropType<ColProps & HTMLAttributes> },
|
||||
wrapperCol: { type: Object as PropType<ColProps & HTMLAttributes> },
|
||||
colon: PropTypes.looseBool,
|
||||
labelAlign: PropTypes.oneOf(tuple('left', 'right')),
|
||||
prefixCls: PropTypes.string,
|
||||
requiredMark: { type: [String, Boolean] as PropType<RequiredMark> },
|
||||
/** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */
|
||||
hideRequiredMark: PropTypes.looseBool,
|
||||
model: PropTypes.object,
|
||||
rules: { type: Object as PropType<{ [k: string]: ValidationRule[] | ValidationRule }> },
|
||||
|
|
|
@ -36,6 +36,9 @@ import find from 'lodash-es/find';
|
|||
import { tuple, VueNode } from '../_util/type';
|
||||
import { ValidateOptions } from './interface';
|
||||
|
||||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
|
||||
export type ValidateStatus = typeof ValidateStatuses[number];
|
||||
|
||||
const iconMap = {
|
||||
success: CheckCircleFilled,
|
||||
warning: ExclamationCircleFilled,
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
|
||||
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
|
||||
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
|
||||
import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled';
|
||||
|
||||
import Col, { ColProps } from '../grid/col';
|
||||
import { useProvideForm, useInjectForm, useProvideFormItemPrefix } from './context';
|
||||
import ErrorList from './ErrorList';
|
||||
import classNames from '../_util/classNames';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
import { VueNode } from '../_util/type';
|
||||
import { computed, defineComponent, HTMLAttributes, onUnmounted } from 'vue';
|
||||
|
||||
interface FormItemInputMiscProps {
|
||||
prefixCls: string;
|
||||
errors: VueNode[];
|
||||
hasFeedback?: boolean;
|
||||
validateStatus?: ValidateStatus;
|
||||
onDomErrorVisibleChange: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export interface FormItemInputProps {
|
||||
wrapperCol?: ColProps;
|
||||
help?: VueNode;
|
||||
extra?: VueNode;
|
||||
status?: ValidateStatus;
|
||||
}
|
||||
|
||||
const iconMap: { [key: string]: any } = {
|
||||
success: CheckCircleFilled,
|
||||
warning: ExclamationCircleFilled,
|
||||
error: CloseCircleFilled,
|
||||
validating: LoadingOutlined,
|
||||
};
|
||||
const FormItemInput = defineComponent<FormItemInputProps & FormItemInputMiscProps>({
|
||||
slots: ['help', 'extra', 'errors'],
|
||||
setup(props, { slots }) {
|
||||
const formContext = useInjectForm();
|
||||
const { wrapperCol: contextWrapperCol } = formContext;
|
||||
|
||||
// Pass to sub FormItem should not with col info
|
||||
const subFormContext = { ...formContext };
|
||||
delete subFormContext.labelCol;
|
||||
delete subFormContext.wrapperCol;
|
||||
useProvideForm(subFormContext);
|
||||
|
||||
useProvideFormItemPrefix({
|
||||
prefixCls: computed(() => props.prefixCls),
|
||||
status: computed(() => props.status),
|
||||
});
|
||||
|
||||
return () => {
|
||||
const {
|
||||
prefixCls,
|
||||
wrapperCol,
|
||||
help = slots.help?.(),
|
||||
errors = slots.errors?.(),
|
||||
onDomErrorVisibleChange,
|
||||
hasFeedback,
|
||||
validateStatus,
|
||||
extra = slots.extra?.(),
|
||||
} = props;
|
||||
const baseClassName = `${prefixCls}-item`;
|
||||
|
||||
const mergedWrapperCol: ColProps & HTMLAttributes =
|
||||
wrapperCol || contextWrapperCol?.value || {};
|
||||
|
||||
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.class);
|
||||
|
||||
onUnmounted(() => {
|
||||
onDomErrorVisibleChange(false);
|
||||
});
|
||||
|
||||
// Should provides additional icon if `hasFeedback`
|
||||
const IconNode = validateStatus && iconMap[validateStatus];
|
||||
const icon =
|
||||
hasFeedback && IconNode ? (
|
||||
<span class={`${baseClassName}-children-icon`}>
|
||||
<IconNode />
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
const inputDom = (
|
||||
<div class={`${baseClassName}-control-input`}>
|
||||
<div class={`${baseClassName}-control-input-content`}>{slots.default?.()}</div>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
const errorListDom = (
|
||||
<ErrorList errors={errors} help={help} onDomErrorVisibleChange={onDomErrorVisibleChange} />
|
||||
);
|
||||
|
||||
// If extra = 0, && will goes wrong
|
||||
// 0&&error -> 0
|
||||
const extraDom = extra ? <div class={`${baseClassName}-extra`}>{extra}</div> : null;
|
||||
|
||||
return (
|
||||
<Col {...mergedWrapperCol} class={className}>
|
||||
{inputDom}
|
||||
{errorListDom}
|
||||
{extraDom}
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default FormItemInput;
|
|
@ -0,0 +1,92 @@
|
|||
import Col, { ColProps } from '../grid/col';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { useInjectForm } from './context';
|
||||
import { RequiredMark } from './Form';
|
||||
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
|
||||
import defaultLocale from '../locale/default';
|
||||
import classNames from '../_util/classNames';
|
||||
import { VueNode } from '../_util/type';
|
||||
import { FunctionalComponent, HTMLAttributes } from 'vue';
|
||||
|
||||
export interface FormItemLabelProps {
|
||||
colon?: boolean;
|
||||
htmlFor?: string;
|
||||
label?: VueNode;
|
||||
labelAlign?: FormLabelAlign;
|
||||
labelCol?: ColProps & HTMLAttributes;
|
||||
requiredMark?: RequiredMark;
|
||||
required?: boolean;
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
const FormItemLabel: FunctionalComponent<FormItemLabelProps> = (props, { slots }) => {
|
||||
const { prefixCls, htmlFor, labelCol, labelAlign, colon, required, requiredMark } = props;
|
||||
const [formLocale] = useLocaleReceiver('Form');
|
||||
const label = props.label ?? slots.label?.();
|
||||
if (!label) return null;
|
||||
const {
|
||||
vertical,
|
||||
labelAlign: contextLabelAlign,
|
||||
labelCol: contextLabelCol,
|
||||
colon: contextColon,
|
||||
} = useInjectForm();
|
||||
const mergedLabelCol: FormItemLabelProps['labelCol'] = labelCol || contextLabelCol?.value || {};
|
||||
|
||||
const mergedLabelAlign: FormLabelAlign | undefined = labelAlign || contextLabelAlign?.value;
|
||||
|
||||
const labelClsBasic = `${prefixCls}-item-label`;
|
||||
const labelColClassName = classNames(
|
||||
labelClsBasic,
|
||||
mergedLabelAlign === 'left' && `${labelClsBasic}-left`,
|
||||
mergedLabelCol.class,
|
||||
);
|
||||
|
||||
let labelChildren = label;
|
||||
// Keep label is original where there should have no colon
|
||||
const computedColon = colon === true || (contextColon?.value !== false && colon !== false);
|
||||
const haveColon = computedColon && !vertical.value;
|
||||
// Remove duplicated user input colon
|
||||
if (haveColon && typeof label === 'string' && (label as string).trim() !== '') {
|
||||
labelChildren = (label as string).replace(/[:|:]\s*$/, '');
|
||||
}
|
||||
|
||||
labelChildren = (
|
||||
<>
|
||||
{labelChildren}
|
||||
{slots.tooltip?.({ class: `${prefixCls}-item-tooltip` })}
|
||||
</>
|
||||
);
|
||||
|
||||
// Add required mark if optional
|
||||
if (requiredMark === 'optional' && !required) {
|
||||
labelChildren = (
|
||||
<>
|
||||
{labelChildren}
|
||||
<span class={`${prefixCls}-item-optional`}>
|
||||
{formLocale.value?.optional || defaultLocale.Form?.optional}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const labelClassName = classNames({
|
||||
[`${prefixCls}-item-required`]: required,
|
||||
[`${prefixCls}-item-required-mark-optional`]: requiredMark === 'optional',
|
||||
[`${prefixCls}-item-no-colon`]: !computedColon,
|
||||
});
|
||||
return (
|
||||
<Col {...mergedLabelCol} class={labelColClassName}>
|
||||
<label
|
||||
html-for={htmlFor}
|
||||
class={labelClassName}
|
||||
title={typeof label === 'string' ? label : ''}
|
||||
>
|
||||
{labelChildren}
|
||||
</label>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
FormItemLabel.displayName = 'FormItemLabel';
|
||||
|
||||
export default FormItemLabel;
|
|
@ -0,0 +1,49 @@
|
|||
import { inject, InjectionKey, provide, ComputedRef, computed } from 'vue';
|
||||
import { ColProps } from '../grid';
|
||||
import { RequiredMark } from './Form';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
import { FormLabelAlign } from './interface';
|
||||
|
||||
export interface FormContextProps {
|
||||
vertical: ComputedRef<boolean>;
|
||||
name?: ComputedRef<string>;
|
||||
colon?: ComputedRef<boolean>;
|
||||
labelAlign?: ComputedRef<FormLabelAlign>;
|
||||
labelCol?: ComputedRef<ColProps>;
|
||||
wrapperCol?: ComputedRef<ColProps>;
|
||||
requiredMark?: ComputedRef<RequiredMark>;
|
||||
//itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
|
||||
}
|
||||
|
||||
export const FormContextKey: InjectionKey<FormContextProps> = Symbol('formContextKey');
|
||||
|
||||
export const useProvideForm = (state: FormContextProps) => {
|
||||
provide(FormContextKey, state);
|
||||
};
|
||||
|
||||
export const useInjectForm = () => {
|
||||
return inject(FormContextKey, {
|
||||
labelAlign: computed(() => 'right' as FormLabelAlign),
|
||||
vertical: computed(() => false),
|
||||
});
|
||||
};
|
||||
|
||||
/** Used for ErrorList only */
|
||||
export interface FormItemPrefixContextProps {
|
||||
prefixCls: ComputedRef<string>;
|
||||
status?: ComputedRef<ValidateStatus>;
|
||||
}
|
||||
|
||||
export const FormItemPrefixContextKey: InjectionKey<FormItemPrefixContextProps> = Symbol(
|
||||
'formItemPrefixContextKey',
|
||||
);
|
||||
|
||||
export const useProvideFormItemPrefix = (state: FormItemPrefixContextProps) => {
|
||||
provide(FormItemPrefixContextKey, state);
|
||||
};
|
||||
|
||||
export const useInjectFormItemPrefix = () => {
|
||||
return inject(FormItemPrefixContextKey, {
|
||||
prefixCls: computed(() => ''),
|
||||
});
|
||||
};
|
|
@ -1,5 +1,7 @@
|
|||
import { VueNode } from '../_util/type';
|
||||
|
||||
export type FormLabelAlign = 'left' | 'right';
|
||||
|
||||
export type InternalNamePath = (string | number)[];
|
||||
export type NamePath = string | number | InternalNamePath;
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import interopDefault from '../_util/interopDefault';
|
|||
import { ModalLocale, changeConfirmLocale } from '../modal/locale';
|
||||
import warning from '../_util/warning';
|
||||
import { withInstall } from '../_util/type';
|
||||
import { ValidateMessages } from '../form/interface';
|
||||
export interface Locale {
|
||||
locale: string;
|
||||
Pagination?: Object;
|
||||
|
@ -17,6 +18,14 @@ export interface Locale {
|
|||
Transfer?: Object;
|
||||
Select?: Object;
|
||||
Upload?: Object;
|
||||
|
||||
Form?: {
|
||||
optional?: string;
|
||||
defaultValidateMessages: ValidateMessages;
|
||||
};
|
||||
Image?: {
|
||||
preview: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocaleProviderProps {
|
||||
|
|
|
@ -3,14 +3,13 @@ import DatePicker from '../date-picker/locale/en_US';
|
|||
import TimePicker from '../time-picker/locale/en_US';
|
||||
import Calendar from '../calendar/locale/en_US';
|
||||
// import ColorPicker from '../color-picker/locale/en_US';
|
||||
|
||||
const typeTemplate = '${label} is not a valid ${type}';
|
||||
export default {
|
||||
locale: 'en',
|
||||
Pagination,
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
Calendar,
|
||||
// ColorPicker,
|
||||
global: {
|
||||
placeholder: 'Please select',
|
||||
},
|
||||
|
@ -18,11 +17,18 @@ export default {
|
|||
filterTitle: 'Filter menu',
|
||||
filterConfirm: 'OK',
|
||||
filterReset: 'Reset',
|
||||
filterEmptyText: 'No filters',
|
||||
emptyText: 'No data',
|
||||
selectAll: 'Select current page',
|
||||
selectInvert: 'Invert current page',
|
||||
selectNone: 'Clear all data',
|
||||
selectionAll: 'Select all data',
|
||||
sortTitle: 'Sort',
|
||||
expand: 'Expand row',
|
||||
collapse: 'Collapse row',
|
||||
triggerDesc: 'Click to sort descending',
|
||||
triggerAsc: 'Click to sort ascending',
|
||||
cancelSort: 'Click to cancel sorting',
|
||||
},
|
||||
Modal: {
|
||||
okText: 'OK',
|
||||
|
@ -38,6 +44,12 @@ export default {
|
|||
searchPlaceholder: 'Search here',
|
||||
itemUnit: 'item',
|
||||
itemsUnit: 'items',
|
||||
remove: 'Remove',
|
||||
selectCurrent: 'Select current page',
|
||||
removeCurrent: 'Remove current page',
|
||||
selectAll: 'Select all data',
|
||||
removeAll: 'Remove all data',
|
||||
selectInvert: 'Invert current page',
|
||||
},
|
||||
Upload: {
|
||||
uploading: 'Uploading...',
|
||||
|
@ -61,4 +73,57 @@ export default {
|
|||
PageHeader: {
|
||||
back: 'Back',
|
||||
},
|
||||
Form: {
|
||||
optional: '(optional)',
|
||||
defaultValidateMessages: {
|
||||
default: 'Field validation error for ${label}',
|
||||
required: 'Please enter ${label}',
|
||||
enum: '${label} must be one of [${enum}]',
|
||||
whitespace: '${label} cannot be a blank character',
|
||||
date: {
|
||||
format: '${label} date format is invalid',
|
||||
parse: '${label} cannot be converted to a date',
|
||||
invalid: '${label} is an invalid date',
|
||||
},
|
||||
types: {
|
||||
string: typeTemplate,
|
||||
method: typeTemplate,
|
||||
array: typeTemplate,
|
||||
object: typeTemplate,
|
||||
number: typeTemplate,
|
||||
date: typeTemplate,
|
||||
boolean: typeTemplate,
|
||||
integer: typeTemplate,
|
||||
float: typeTemplate,
|
||||
regexp: typeTemplate,
|
||||
email: typeTemplate,
|
||||
url: typeTemplate,
|
||||
hex: typeTemplate,
|
||||
},
|
||||
string: {
|
||||
len: '${label} must be ${len} characters',
|
||||
min: '${label} must be at least ${min} characters',
|
||||
max: '${label} must be up to ${max} characters',
|
||||
range: '${label} must be between ${min}-${max} characters',
|
||||
},
|
||||
number: {
|
||||
len: '${label} must be equal to ${len}',
|
||||
min: '${label} must be minimum ${min}',
|
||||
max: '${label} must be maximum ${max}',
|
||||
range: '${label} must be between ${min}-${max}',
|
||||
},
|
||||
array: {
|
||||
len: 'Must be ${len} ${label}',
|
||||
min: 'At least ${min} ${label}',
|
||||
max: 'At most ${max} ${label}',
|
||||
range: 'The amount of ${label} must be between ${min}-${max}',
|
||||
},
|
||||
pattern: {
|
||||
mismatch: '${label} does not match the pattern ${pattern}',
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: {
|
||||
preview: 'Preview',
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue