refactor: button date-picker drawer dropdown form by ts

pull/2996/head
tanjinzhou 2020-10-15 17:58:05 +08:00
parent 34b3e6366f
commit 2fdfa2b662
19 changed files with 511 additions and 259 deletions

View File

@ -10,7 +10,7 @@ const initDefaultProps = <T>(
: any;
},
): T => {
const propTypes = { ...types };
const propTypes: T = { ...types } as T;
Object.keys(defaultProps).forEach(k => {
const prop = propTypes[k] as VueTypeValidableDef;
if (prop) {

View File

@ -140,13 +140,14 @@ export default defineComponent({
},
render() {
this.iconCom = getComponent(this, 'icon');
const { type, htmlType, iconCom, disabled, handleClick, sLoading, $attrs } = this;
const { type, htmlType, iconCom, disabled, handleClick, sLoading,href,title, $attrs } = this;
const children = getSlot(this);
this.children = children;
const classes = this.getClasses();
const buttonProps = {
...$attrs,
title,
disabled,
class: classes,
onClick: handleClick,
@ -158,9 +159,9 @@ export default defineComponent({
this.insertSpace(child, this.isNeedInserted() && autoInsertSpace),
);
if ($attrs.href !== undefined) {
if (href !== undefined) {
return (
<a {...buttonProps} ref="buttonNode">
<a {...buttonProps} href={href} ref="buttonNode">
{iconNode}
{kids}
</a>

View File

@ -22,5 +22,7 @@ export default () => ({
ghost: PropTypes.looseBool,
block: PropTypes.looseBool,
icon: PropTypes.VNodeChild,
href: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
});

View File

@ -1,6 +1,6 @@
import moment from 'moment';
import { CSSProperties, DefineComponent, HTMLAttributes } from 'vue';
import { CSSProperties, DefineComponent } from 'vue';
import { tuple, VueNode } from '../_util/type';
export type RangePickerValue =

View File

@ -1,4 +1,4 @@
import { inject, provide, nextTick } from 'vue';
import { inject, provide, nextTick, defineComponent, App, CSSProperties } from 'vue';
import classnames from '../_util/classNames';
import omit from 'omit.js';
import VcDrawer from '../vc-drawer/src';
@ -7,8 +7,11 @@ import BaseMixin from '../_util/BaseMixin';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import { getComponent, getOptionProps } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import { tuple } from '../_util/type';
const Drawer = {
const PlacementTypes = tuple('top', 'right', 'bottom', 'left');
type placementType = typeof PlacementTypes[number];
const Drawer = defineComponent({
name: 'ADrawer',
inheritAttrs: false,
props: {
@ -22,16 +25,16 @@ const Drawer = {
bodyStyle: PropTypes.object,
headerStyle: PropTypes.object,
drawerStyle: PropTypes.object,
title: PropTypes.any,
title: PropTypes.VNodeChild,
visible: PropTypes.looseBool,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256),
zIndex: PropTypes.number,
prefixCls: PropTypes.string,
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']).def('right'),
placement: PropTypes.oneOf(PlacementTypes).def('right'),
level: PropTypes.any.def(null),
wrapClassName: PropTypes.string, // not use class like react, vue will add class to root dom
handle: PropTypes.any,
handle: PropTypes.VNodeChild,
afterVisibleChange: PropTypes.func,
keyboard: PropTypes.looseBool.def(true),
onClose: PropTypes.func,
@ -39,22 +42,21 @@ const Drawer = {
},
mixins: [BaseMixin],
data() {
this.destroyClose = false;
this.preVisible = this.$props.visible;
return {
_push: false,
sPush: false,
};
},
setup() {
setup(props) {
const configProvider = inject('configProvider', defaultConfigProvider);
return {
configProvider,
destroyClose: false,
preVisible: props.visible,
parentDrawer: inject('parentDrawer', null)
};
},
beforeCreate() {
const parentDrawer = inject('parentDrawer', null);
provide('parentDrawer', this);
this.parentDrawer = parentDrawer;
},
mounted() {
// fix: delete drawer in child and re-render, no push started.
@ -83,7 +85,7 @@ const Drawer = {
}
},
methods: {
close(e) {
close(e: Event) {
this.$emit('update:visible', false);
this.$emit('close', e);
},
@ -95,12 +97,12 @@ const Drawer = {
// },
push() {
this.setState({
_push: true,
sPush: true,
});
},
pull() {
this.setState({
_push: false,
sPush: false,
});
},
onDestroyTransitionEnd() {
@ -118,7 +120,7 @@ const Drawer = {
return this.destroyOnClose && !this.visible;
},
// get drawar push width or height
getPushTransform(placement) {
getPushTransform(placement?: placementType) {
if (placement === 'left' || placement === 'right') {
return `translateX(${placement === 'left' ? 180 : -180}px)`;
}
@ -128,14 +130,14 @@ const Drawer = {
},
getRcDrawerStyle() {
const { zIndex, placement, wrapStyle } = this.$props;
const { _push: push } = this.$data;
const { sPush: push } = this.$data;
return {
zIndex,
transform: push ? this.getPushTransform(placement) : undefined,
...wrapStyle,
};
},
renderHeader(prefixCls) {
renderHeader(prefixCls: string) {
const { closable, headerStyle } = this.$props;
const title = getComponent(this, 'title');
if (!title && !closable) {
@ -150,7 +152,7 @@ const Drawer = {
</div>
);
},
renderCloseIcon(prefixCls) {
renderCloseIcon(prefixCls: string) {
const { closable } = this;
return (
closable && (
@ -161,14 +163,14 @@ const Drawer = {
);
},
// render drawer body dom
renderBody(prefixCls) {
renderBody(prefixCls: string) {
if (this.destroyClose && !this.visible) {
return null;
}
this.destroyClose = false;
const { bodyStyle, drawerStyle } = this.$props;
const containerStyle = {};
const containerStyle: CSSProperties = {};
const isDestroyOnClose = this.getDestroyOnClose();
if (isDestroyOnClose) {
@ -192,7 +194,7 @@ const Drawer = {
},
},
render() {
const props = getOptionProps(this);
const props: any = getOptionProps(this);
const {
prefixCls: customizePrefixCls,
width,
@ -204,7 +206,7 @@ const Drawer = {
...rest
} = props;
const haveMask = mask ? '' : 'no-mask';
const offsetStyle = {};
const offsetStyle: CSSProperties = {};
if (placement === 'left' || placement === 'right') {
offsetStyle.width = typeof width === 'number' ? `${width}px` : width;
} else {
@ -213,8 +215,8 @@ const Drawer = {
const handler = getComponent(this, 'handle') || false;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('drawer', customizePrefixCls);
const { class: className } = this.$attrs;
const vcDrawerProps = {
const { class: className } = this.$attrs as any;
const vcDrawerProps: any = {
...this.$attrs,
...omit(rest, [
'closable',
@ -249,10 +251,10 @@ const Drawer = {
};
return <VcDrawer {...vcDrawerProps}>{this.renderBody(prefixCls)}</VcDrawer>;
},
};
});
/* istanbul ignore next */
Drawer.install = function(app) {
Drawer.install = function(app: App) {
app.component(Drawer.name, Drawer);
return app;
};

View File

@ -1,4 +1,4 @@
import { provide, inject } from 'vue';
import { provide, inject, defineComponent } from 'vue';
import Button from '../button';
import classNames from '../_util/classNames';
import buttonTypes from '../button/buttonTypes';
@ -9,6 +9,7 @@ import { hasProp, getComponent, getSlot } from '../_util/props-util';
import getDropdownProps from './getDropdownProps';
import { defaultConfigProvider } from '../config-provider';
import EllipsisOutlined from '@ant-design/icons-vue/EllipsisOutlined';
import { tuple } from '../_util/type';
const ButtonTypesProps = buttonTypes();
const DropdownProps = getDropdownProps();
@ -16,8 +17,8 @@ const ButtonGroup = Button.Group;
const DropdownButtonProps = {
...ButtonGroupProps,
...DropdownProps,
type: PropTypes.oneOf(['primary', 'ghost', 'dashed', 'danger', 'default']).def('default'),
size: PropTypes.oneOf(['small', 'large', 'default']).def('default'),
type: PropTypes.oneOf(tuple('primary', 'ghost', 'dashed', 'danger', 'default')).def('default'),
size: PropTypes.oneOf(tuple('small', 'large', 'default')).def('default'),
htmlType: ButtonTypesProps.htmlType,
href: PropTypes.string,
disabled: PropTypes.looseBool,
@ -30,20 +31,21 @@ const DropdownButtonProps = {
'onUpdate:visible': PropTypes.func,
};
export { DropdownButtonProps };
export default {
export default defineComponent({
name: 'ADropdownButton',
inheritAttrs: false,
props: DropdownButtonProps,
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
popupRef: null
};
},
created() {
provide('savePopupRef', this.savePopupRef);
},
methods: {
savePopupRef(ref) {
savePopupRef(ref: any) {
this.popupRef = ref;
},
handleClick(e) {
@ -72,12 +74,12 @@ export default {
href,
title,
...restProps
} = { ...this.$props, ...this.$attrs };
} = { ...this.$props, ...this.$attrs } as any;
const icon = getComponent(this, 'icon') || <EllipsisOutlined />;
const { getPopupContainer: getContextPopupContainer } = this.configProvider;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('dropdown-button', customizePrefixCls);
const dropdownProps = {
const dropdownProps: any = {
align,
disabled,
trigger: disabled ? [] : trigger,
@ -112,4 +114,4 @@ export default {
</ButtonGroup>
);
},
};
});

View File

@ -30,13 +30,14 @@ const Dropdown = defineComponent({
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
popupRef: null,
};
},
created() {
provide('savePopupRef', this.savePopupRef);
},
methods: {
savePopupRef(ref) {
savePopupRef(ref: any) {
this.popupRef = ref;
},
getTransitionName() {
@ -49,13 +50,13 @@ const Dropdown = defineComponent({
}
return 'slide-up';
},
renderOverlay(prefixCls) {
renderOverlay(prefixCls: string) {
const overlay = getComponent(this, 'overlay');
const overlayNode = Array.isArray(overlay) ? overlay[0] : overlay;
// menu cannot be selectable in dropdown defaultly
// menu should be focusable in dropdown defaultly
const overlayProps = overlayNode && getPropsData(overlayNode);
const { selectable = false, focusable = true } = overlayProps || {};
const { selectable = false, focusable = true } = (overlayProps || {}) as any;
const expandIcon = (
<span class={`${prefixCls}-menu-submenu-arrow`}>
<RightOutlined class={`${prefixCls}-menu-submenu-arrow-icon`} />
@ -72,7 +73,7 @@ const Dropdown = defineComponent({
: overlay;
return fixedModeOverlay;
},
handleVisibleChange(val) {
handleVisibleChange(val: boolean) {
this.$emit('update:visible', val);
this.$emit('visibleChange', val);
},

View File

@ -1,6 +1,11 @@
import { tuple } from '../_util/type';
import { PropType } from 'vue';
import PropTypes from '../_util/vue-types';
export default () => ({
trigger: PropTypes.array.def(['hover']),
trigger: {
type: Array as PropType<('click' | 'hover' | 'contextMenu')[]>,
default: () => ['hover'],
},
overlay: PropTypes.any,
visible: PropTypes.looseBool,
disabled: PropTypes.looseBool,
@ -8,16 +13,11 @@ export default () => ({
getPopupContainer: PropTypes.func,
prefixCls: PropTypes.string,
transitionName: PropTypes.string,
placement: PropTypes.oneOf([
'topLeft',
'topCenter',
'topRight',
'bottomLeft',
'bottomCenter',
'bottomRight',
]),
placement: PropTypes.oneOf(
tuple('topLeft', 'topCenter', 'topRight', 'bottomLeft', 'bottomCenter', 'bottomRight'),
),
overlayClassName: PropTypes.string,
overlayStyle: PropTypes.object,
overlayStyle: PropTypes.style,
forceRender: PropTypes.looseBool,
mouseEnterDelay: PropTypes.number,
mouseLeaveDelay: PropTypes.number,

View File

@ -1,16 +1,22 @@
import { App } from 'vue';
import Dropdown from './dropdown';
import DropdownButton from './dropdown-button';
export { DropdownProps } from './dropdown';
export { DropdownButtonProps } from './dropdown-button';
type Types = typeof Dropdown;
interface DropdownTypes extends Types {
Button: typeof DropdownButton;
}
Dropdown.Button = DropdownButton;
/* istanbul ignore next */
Dropdown.install = function(app) {
Dropdown.install = function(app: App) {
app.component(Dropdown.name, Dropdown);
app.component(DropdownButton.name, DropdownButton);
return app;
};
export default Dropdown;
export default Dropdown as DropdownTypes;

View File

@ -1,10 +1,9 @@
import { inject, provide } from 'vue';
import { defineComponent, inject, provide, PropType, computed } from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import isRegExp from 'lodash-es/isRegExp';
import warning from '../_util/warning';
import FormItem from './FormItem';
import { initDefaultProps, getSlot } from '../_util/props-util';
import { getSlot } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import { getNamePath, containsNamePath } from './utils/valueUtil';
import { defaultValidateMessages } from './utils/messages';
@ -12,57 +11,65 @@ import { allPromiseFinish } from './utils/asyncUtil';
import { toArray } from './utils/typeUtil';
import isEqual from 'lodash-es/isEqual';
import scrollIntoView from 'scroll-into-view-if-needed';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import { tuple, VueNode } from '../_util/type';
import { ColProps } from '../grid/col';
import { InternalNamePath, NamePath, ValidateOptions } from './interface';
export type ValidationRule = {
/** validation error message */
message?: VueNode;
/** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */
type?: string;
/** indicates whether field is required */
required?: boolean;
/** treat required fields that only contain whitespace as errors */
whitespace?: boolean;
/** validate the exact length of a field */
len?: number;
/** validate the min length of a field */
min?: number;
/** validate the max length of a field */
max?: number;
/** validate the value from a list of possible values */
enum?: string | string[];
/** validate from a regular expression */
pattern?: RegExp;
/** transform a value before validation */
transform?: (value: any) => any;
/** custom validate function (Note: callback must be called) */
validator?: (rule: any, value: any, callback: any, source?: any, options?: any) => any;
trigger?: string
};
export const FormProps = {
layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']),
labelCol: PropTypes.object,
wrapperCol: PropTypes.object,
layout: PropTypes.oneOf(tuple('horizontal', 'inline', 'vertical')),
labelCol: {type: Object as PropType<ColProps>},
wrapperCol: {type: Object as PropType<ColProps>},
colon: PropTypes.looseBool,
labelAlign: PropTypes.oneOf(['left', 'right']),
labelAlign: PropTypes.oneOf(tuple('left', 'right')),
prefixCls: PropTypes.string,
hideRequiredMark: PropTypes.looseBool,
model: PropTypes.object,
rules: PropTypes.object,
validateMessages: PropTypes.any,
rules: {type: Object as PropType<ValidationRule[]>},
validateMessages: PropTypes.object,
validateOnRuleChange: PropTypes.looseBool,
// 提交失败自动滚动到第一个错误字段
scrollToFirstError: PropTypes.looseBool,
onFinish: PropTypes.func,
onFinishFailed: PropTypes.func,
name: PropTypes.name,
validateTrigger: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
name: PropTypes.string,
validateTrigger: {type: [String, Array] as PropType<string | string[]>}
};
export const ValidationRule = {
/** validation error message */
message: PropTypes.string,
/** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */
type: PropTypes.string,
/** indicates whether field is required */
required: PropTypes.looseBool,
/** treat required fields that only contain whitespace as errors */
whitespace: PropTypes.looseBool,
/** validate the exact length of a field */
len: PropTypes.number,
/** validate the min length of a field */
min: PropTypes.number,
/** validate the max length of a field */
max: PropTypes.number,
/** validate the value from a list of possible values */
enum: PropTypes.oneOfType([String, PropTypes.arrayOf(String)]),
/** validate from a regular expression */
pattern: PropTypes.custom(isRegExp),
/** transform a value before validation */
transform: PropTypes.func,
/** custom validate function (Note: callback must be called) */
validator: PropTypes.func,
};
function isEqualName(name1, name2) {
function isEqualName(name1: any, name2: any) {
return isEqual(toArray(name1), toArray(name2));
}
const Form = {
const Form = defineComponent({
name: 'AForm',
inheritAttrs: false,
props: initDefaultProps(FormProps, {
@ -72,14 +79,15 @@ const Form = {
}),
Item: FormItem,
created() {
this.fields = [];
this.form = undefined;
this.lastValidatePromise = null;
provide('FormContext', this);
},
setup() {
setup(props) {
return {
configProvider: inject('configProvider', defaultConfigProvider),
fields: [],
form: undefined,
lastValidatePromise: null,
vertical: computed(()=>props.layout === 'vertical')
};
},
watch: {
@ -89,23 +97,18 @@ const Form = {
}
},
},
computed: {
vertical() {
return this.layout === 'vertical';
},
},
methods: {
addField(field) {
addField(field: any) {
if (field) {
this.fields.push(field);
}
},
removeField(field) {
removeField(field: any) {
if (field.fieldName) {
this.fields.splice(this.fields.indexOf(field), 1);
}
},
handleSubmit(e) {
handleSubmit(e: Event) {
e.preventDefault();
e.stopPropagation();
this.$emit('submit', e);
@ -129,7 +132,7 @@ const Form = {
);
}
},
resetFields(name) {
resetFields(name: NamePath) {
if (!this.model) {
warning(false, 'Form', 'model is required for resetFields to work.');
return;
@ -150,10 +153,10 @@ const Form = {
this.scrollToField(errorInfo.errorFields[0].name);
}
},
validate() {
return this.validateField(...arguments);
validate(...args: any[]) {
return this.validateField(...args);
},
scrollToField(name, options = {}) {
scrollToField(name: string | number , options = {}) {
const fields = this.getFieldsByNameList([name]);
if (fields.length) {
const fieldId = fields[0].fieldId;
@ -169,20 +172,20 @@ const Form = {
}
},
// eslint-disable-next-line no-unused-vars
getFieldsValue(nameList = true) {
const values = {};
getFieldsValue(nameList: NamePath[] | true = true) {
const values: any = {};
this.fields.forEach(({ fieldName, fieldValue }) => {
values[fieldName] = fieldValue;
});
if (nameList === true) {
return values;
} else {
const res = {};
toArray(nameList).forEach(namePath => (res[namePath] = values[namePath]));
const res: any = {};
toArray(nameList as NamePath[]).forEach((namePath) => (res[namePath as string] = values[namePath as string]));
return res;
}
},
validateFields(nameList, options) {
validateFields(nameList?: NamePath[], options?: ValidateOptions) {
warning(
!(nameList instanceof Function),
'Form',
@ -193,10 +196,13 @@ const Form = {
return Promise.reject('Form `model` is required for validateFields to work.');
}
const provideNameList = !!nameList;
const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : [];
const namePathList: InternalNamePath[] = provideNameList ? toArray(nameList).map(getNamePath) : [];
// Collect result in promise list
const promiseList = [];
const promiseList: Promise<{
name: InternalNamePath;
errors: string[];
}>[] = [];
this.fields.forEach(field => {
// Add field if not provide `nameList`
@ -225,7 +231,7 @@ const Form = {
promiseList.push(
promise
.then(() => ({ name: fieldNamePath, errors: [] }))
.catch(errors =>
.catch((errors: any) =>
Promise.reject({
name: fieldNamePath,
errors,
@ -259,8 +265,8 @@ const Form = {
return returnPromise;
},
validateField() {
return this.validateFields(...arguments);
validateField(...args: any[]) {
return this.validateFields(...args);
},
},
@ -282,6 +288,8 @@ const Form = {
</form>
);
},
};
});
export default Form;
export default Form as typeof Form & {
readonly Item: typeof FormItem
};

View File

@ -1,4 +1,4 @@
import { inject, provide, Transition } from 'vue';
import { inject, provide, Transition, PropType, defineComponent, computed } from 'vue';
import cloneDeep from 'lodash-es/cloneDeep';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
@ -6,7 +6,6 @@ import getTransitionProps from '../_util/getTransitionProps';
import Row from '../grid/Row';
import Col from '../grid/Col';
import hasProp, {
initDefaultProps,
findDOMNode,
getComponent,
getOptionProps,
@ -26,6 +25,9 @@ import { getNamePath } from './utils/valueUtil';
import { toArray } from './utils/typeUtil';
import { warning } from '../vc-util/warning';
import find from 'lodash-es/find';
import { ColProps } from '../grid/col';
import { tuple, VueNode } from '../_util/type';
import { ValidateOptions } from './interface';
const iconMap = {
success: CheckCircleFilled,
@ -34,7 +36,7 @@ const iconMap = {
validating: LoadingOutlined,
};
function getPropByPath(obj, namePathList, strict) {
function getPropByPath(obj: any, namePathList: any, strict?: boolean) {
let tempObj = obj;
const keyArr = namePathList;
@ -69,38 +71,106 @@ export const FormItemProps = {
id: PropTypes.string,
htmlFor: PropTypes.string,
prefixCls: PropTypes.string,
label: PropTypes.any,
help: PropTypes.any,
extra: PropTypes.any,
labelCol: PropTypes.object,
wrapperCol: PropTypes.object,
hasFeedback: PropTypes.looseBool,
label: PropTypes.VNodeChild,
help: PropTypes.VNodeChild,
extra: PropTypes.VNodeChild,
labelCol: {type: Object as PropType<ColProps>},
wrapperCol: {type: Object as PropType<ColProps>},
hasFeedback: PropTypes.looseBool.def(false),
colon: PropTypes.looseBool,
labelAlign: PropTypes.oneOf(['left', 'right']),
prop: PropTypes.oneOfType([Array, String, Number]),
name: PropTypes.oneOfType([Array, String, Number]),
labelAlign: PropTypes.oneOf(tuple('left', 'right')),
prop: {type: [String, Number, Array] as PropType<string | number | string[] | number[]>},
name: {type: [String, Number, Array] as PropType<string | number | string[] | number[]>},
rules: PropTypes.oneOfType([Array, Object]),
autoLink: PropTypes.looseBool,
autoLink: PropTypes.looseBool.def(true),
required: PropTypes.looseBool,
validateFirst: PropTypes.looseBool,
validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']),
validateTrigger: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
validateStatus: PropTypes.oneOf(tuple('', 'success', 'warning', 'error', 'validating')),
validateTrigger: {type: [String, Array] as PropType<string | string[]>},
messageVariables: {type: Object as PropType<Record<string, string>>},
};
export default {
export default defineComponent({
name: 'AFormItem',
mixins: [BaseMixin],
inheritAttrs: false,
__ANT_NEW_FORM_ITEM: true,
props: initDefaultProps(FormItemProps, {
hasFeedback: false,
autoLink: true,
}),
setup() {
props: FormItemProps,
setup(props) {
const FormContext = inject('FormContext', {}) as any
const fieldName = computed(()=>props.name || props.prop);
const namePath = computed(()=>{
const val = fieldName.value
return getNamePath(val)
})
const fieldId = computed(()=>{
const {id} = props;
if (id) {
return id;
} else if (!namePath.value.length) {
return undefined;
} else {
const formName = FormContext.name;
const mergedId = namePath.value.join('_');
return formName ? `${formName}_${mergedId}` : mergedId;
}
})
const fieldValue = computed(()=> {
const model = FormContext.model;
if (!model || !fieldName.value) {
return;
}
return getPropByPath(model, namePath.value, true).v;
})
const mergedValidateTrigger = computed(() => {
let validateTrigger =
props.validateTrigger !== undefined
? props.validateTrigger
: FormContext.validateTrigger;
validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger;
return toArray(validateTrigger);
});
const getRules = () => {
let formRules = FormContext.rules;
const selfRules = props.rules;
const requiredRule =
props.required !== undefined
? { required: !!props.required, trigger: mergedValidateTrigger.value }
: [];
const prop = getPropByPath(formRules, namePath.value);
formRules = formRules ? prop.o[prop.k] || prop.v : [];
const rules = [].concat(selfRules || formRules || []);
if (find(rules, rule => rule.required)) {
return rules;
} else {
return rules.concat(requiredRule);
}
};
const isRequired = computed(() => {
let rules = getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
});
}
return isRequired || props.required;
});
return {
isFormItemChildren: inject('isFormItemChildren', false),
configProvider: inject('configProvider', defaultConfigProvider),
FormContext: inject('FormContext', {}),
FormContext,
fieldId,
fieldName,
namePath,
isRequired,
getRules,
fieldValue,
mergedValidateTrigger
};
},
data() {
@ -112,56 +182,9 @@ export default {
validator: {},
helpShow: false,
errors: [],
initialValue: undefined
};
},
computed: {
fieldName() {
return this.name || this.prop;
},
namePath() {
return getNamePath(this.fieldName);
},
fieldId() {
if (this.id) {
return this.id;
} else if (!this.namePath.length) {
return undefined;
} else {
const formName = this.FormContext.name;
const mergedId = this.namePath.join('_');
return formName ? `${formName}_${mergedId}` : mergedId;
}
},
fieldValue() {
const model = this.FormContext.model;
if (!model || !this.fieldName) {
return;
}
return getPropByPath(model, this.namePath, true).v;
},
isRequired() {
let rules = this.getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
});
}
return isRequired || this.required;
},
mergedValidateTrigger() {
let validateTrigger =
this.validateTrigger !== undefined
? this.validateTrigger
: this.FormContext.validateTrigger;
validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger;
return toArray(validateTrigger);
},
},
watch: {
validateStatus(val) {
this.validateState = val;
@ -188,7 +211,7 @@ export default {
return fieldName !== undefined ? [...prefixName, ...this.namePath] : [];
},
validateRules(options) {
validateRules(options: ValidateOptions) {
const { validateFirst = false, messageVariables } = this.$props;
const { triggerName } = options || {};
const namePath = this.getNamePath();
@ -296,14 +319,14 @@ export default {
}
},
onHelpAnimEnd(_key, helpShow) {
onHelpAnimEnd(_key: string, helpShow: boolean) {
this.helpShow = helpShow;
if (!helpShow) {
this.$forceUpdate();
}
},
renderHelp(prefixCls) {
renderHelp(prefixCls: string) {
const help = this.getHelpMessage();
const children = help ? (
<div class={`${prefixCls}-explain`} key="help">
@ -324,12 +347,12 @@ export default {
);
},
renderExtra(prefixCls) {
renderExtra(prefixCls: string) {
const extra = getComponent(this, 'extra');
return extra ? <div class={`${prefixCls}-extra`}>{extra}</div> : null;
},
renderValidateWrapper(prefixCls, c1, c2, c3) {
renderValidateWrapper(prefixCls: string, c1: VueNode, c2: VueNode, c3: VueNode) {
const validateStatus = this.validateState;
let classes = `${prefixCls}-item-control`;
@ -362,8 +385,8 @@ export default {
);
},
renderWrapper(prefixCls, children) {
const { wrapperCol: contextWrapperCol } = this.isFormItemChildren ? {} : this.FormContext;
renderWrapper(prefixCls: string, children: VueNode) {
const { wrapperCol: contextWrapperCol } = (this.isFormItemChildren ? {} : this.FormContext) as any;
const { wrapperCol } = this;
const mergedWrapperCol = wrapperCol || contextWrapperCol || {};
const { style, id, ...restProps } = mergedWrapperCol;
@ -378,7 +401,7 @@ export default {
return <Col {...colProps}>{children}</Col>;
},
renderLabel(prefixCls) {
renderLabel(prefixCls: string) {
const {
vertical,
labelAlign: contextLabelAlign,
@ -437,7 +460,7 @@ export default {
</Col>
) : null;
},
renderChildren(prefixCls, child) {
renderChildren(prefixCls: string, child: VueNode) {
return [
this.renderLabel(prefixCls),
this.renderWrapper(
@ -451,9 +474,9 @@ export default {
),
];
},
renderFormItem(child) {
renderFormItem(child: any[]) {
const { prefixCls: customizePrefixCls } = this.$props;
const { class: className, ...restProps } = this.$attrs;
const { class: className, ...restProps } = this.$attrs as any;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const children = this.renderChildren(prefixCls, child);
@ -480,11 +503,11 @@ export default {
const originalChange = originalEvents.onChange;
firstChildren = cloneElement(firstChildren, {
...(this.fieldId ? { id: this.fieldId } : undefined),
onBlur: (...args) => {
onBlur: (...args: any[]) => {
originalBlur && originalBlur(...args);
this.onFieldBlur();
},
onChange: (...args) => {
onChange: (...args: any[]) => {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
originalChange[i](...args);
@ -498,4 +521,4 @@ export default {
}
return this.renderFormItem([firstChildren, children.slice(1)]);
},
};
});

View File

@ -1,7 +1,6 @@
import Form from './Form';
export { FormProps, ValidationRule } from './Form';
export { FormItemProps } from './FormItem';
export { FormProps } from './Form';
/* istanbul ignore next */
Form.install = function(app) {

View File

@ -0,0 +1,201 @@
import { VueNode } from '../_util/type';
export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;
export type StoreValue = any;
export interface Store {
[name: string]: StoreValue;
}
export interface Meta {
touched: boolean;
validating: boolean;
errors: string[];
name: InternalNamePath;
}
export interface InternalFieldData extends Meta {
value: StoreValue;
}
/**
* Used by `setFields` config
*/
export interface FieldData extends Partial<Omit<InternalFieldData, 'name'>> {
name: NamePath;
}
export type RuleType =
| 'string'
| 'number'
| 'boolean'
| 'method'
| 'regexp'
| 'integer'
| 'float'
| 'object'
| 'enum'
| 'date'
| 'url'
| 'hex'
| 'email';
type Validator = (
rule: RuleObject,
value: StoreValue,
callback: (error?: string) => void,
) => Promise<void> | void;
export interface ValidatorRule {
message?: string | VueNode;
validator: Validator;
}
interface BaseRule {
enum?: StoreValue[];
len?: number;
max?: number;
message?: string | VueNode;
min?: number;
pattern?: RegExp;
required?: boolean;
transform?: (value: StoreValue) => StoreValue;
type?: RuleType;
whitespace?: boolean;
/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
validateTrigger?: string | string[];
}
type AggregationRule = BaseRule & Partial<ValidatorRule>;
interface ArrayRule extends Omit<AggregationRule, 'type'> {
type: 'array';
defaultField?: RuleObject;
}
export type RuleObject = AggregationRule | ArrayRule;
export type Rule = RuleObject;
export interface ValidateErrorEntity<Values = any> {
values: Values;
errorFields: { name: InternalNamePath; errors: string[] }[];
outOfDate: boolean;
}
export interface FieldError {
name: InternalNamePath;
errors: string[];
}
export interface ValidateOptions {
triggerName?: string;
validateMessages?: ValidateMessages;
}
export type InternalValidateFields = (
nameList?: NamePath[],
options?: ValidateOptions,
) => Promise<Store>;
export type ValidateFields = (nameList?: NamePath[]) => Promise<Store>;
// >>>>>> Info
interface ValueUpdateInfo {
type: 'valueUpdate';
source: 'internal' | 'external';
}
interface ValidateFinishInfo {
type: 'validateFinish';
}
interface ResetInfo {
type: 'reset';
}
interface SetFieldInfo {
type: 'setField';
data: FieldData;
}
interface DependenciesUpdateInfo {
type: 'dependenciesUpdate';
/**
* Contains all the related `InternalNamePath[]`.
* a <- b <- c : change `a`
* relatedFields=[a, b, c]
*/
relatedFields: InternalNamePath[];
}
export type NotifyInfo =
| ValueUpdateInfo
| ValidateFinishInfo
| ResetInfo
| SetFieldInfo
| DependenciesUpdateInfo;
export type ValuedNotifyInfo = NotifyInfo & {
store: Store;
};
export interface Callbacks<Values = any> {
onValuesChange?: (changedValues: any, values: Values) => void;
onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void;
onFinish?: (values: Values) => void;
onFinishFailed?: (errorInfo: ValidateErrorEntity<Values>) => void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type EventArgs = any[];
type ValidateMessage = string | (() => string);
export interface ValidateMessages {
default?: ValidateMessage;
required?: ValidateMessage;
enum?: ValidateMessage;
whitespace?: ValidateMessage;
date?: {
format?: ValidateMessage;
parse?: ValidateMessage;
invalid?: ValidateMessage;
};
types?: {
string?: ValidateMessage;
method?: ValidateMessage;
array?: ValidateMessage;
object?: ValidateMessage;
number?: ValidateMessage;
date?: ValidateMessage;
boolean?: ValidateMessage;
integer?: ValidateMessage;
float?: ValidateMessage;
regexp?: ValidateMessage;
email?: ValidateMessage;
url?: ValidateMessage;
hex?: ValidateMessage;
};
string?: {
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;
};
number?: {
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;
};
array?: {
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;
};
pattern?: {
mismatch?: ValidateMessage;
};
}

View File

@ -1,4 +1,5 @@
export function allPromiseFinish(promiseList) {
import { FieldError } from '../interface';
export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
let hasError = false;
let count = promiseList.length;
const results = [];

View File

@ -1,4 +1,4 @@
export function toArray(value) {
export function toArray<T>(value?: T | T[] | null): T[] {
if (value === undefined || value === null) {
return [];
}

View File

@ -5,15 +5,16 @@ import { warning } from '../../vc-util/warning';
import { setValues } from './valueUtil';
import { defaultValidateMessages } from './messages';
import { isValidElement } from '../../_util/props-util';
import { InternalNamePath, RuleObject, ValidateMessages, ValidateOptions } from '../interface';
// Remove incorrect original ts define
const AsyncValidator = RawAsyncValidator;
const AsyncValidator: any = RawAsyncValidator;
/**
* Replace with template.
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
*/
function replaceMessage(template, kv) {
function replaceMessage(template: string, kv: Record<string, string>): string {
return template.replace(/\$\{\w+\}/g, str => {
const key = str.slice(2, -1);
return kv[key];
@ -24,18 +25,23 @@ function replaceMessage(template, kv) {
* We use `async-validator` to validate rules. So have to hot replace the message with validator.
* { required: '${name} is required' } => { required: () => 'field is required' }
*/
function convertMessages(messages, name, rule, messageVariables) {
function convertMessages(
messages: ValidateMessages,
name: string,
rule: RuleObject,
messageVariables?: Record<string, string>,
): ValidateMessages {
const kv = {
...rule,
...(rule as Record<string, string | number>),
name,
enum: (rule.enum || []).join(', '),
};
const replaceFunc = (template, additionalKV) => () =>
const replaceFunc = (template: string, additionalKV?: Record<string, string>) => () =>
replaceMessage(template, { ...kv, ...additionalKV });
/* eslint-disable no-param-reassign */
function fillTemplate(source, target = {}) {
function fillTemplate(source: ValidateMessages, target: ValidateMessages = {}) {
Object.keys(source).forEach(ruleName => {
const value = source[ruleName];
if (typeof value === 'string') {
@ -52,13 +58,19 @@ function convertMessages(messages, name, rule, messageVariables) {
}
/* eslint-enable */
return fillTemplate(setValues({}, defaultValidateMessages, messages));
return fillTemplate(setValues({}, defaultValidateMessages, messages)) as ValidateMessages;
}
async function validateRule(name, value, rule, options, messageVariables) {
async function validateRule(
name: string,
value: any,
rule: RuleObject,
options: ValidateOptions,
messageVariables?: Record<string, string>,
): Promise<string[]> {
const cloneRule = { ...rule };
// We should special handle array validate
let subRuleField = null;
let subRuleField: RuleObject = null;
if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
subRuleField = cloneRule.defaultField;
delete cloneRule.defaultField;
@ -77,19 +89,19 @@ async function validateRule(name, value, rule, options, messageVariables) {
await Promise.resolve(validator.validate({ [name]: value }, { ...options }));
} catch (errObj) {
if (errObj.errors) {
result = errObj.errors.map(({ message }, index) =>
result = errObj.errors.map(({ message }, index: number) =>
// Wrap VueNode with `key`
isValidElement(message) ? cloneVNode(message, { key: `error_${index}` }) : message,
);
} else {
console.error(errObj);
result = [messages.default()];
result = [(messages.default as () => string)()];
}
}
if (!result.length && subRuleField) {
const subResults = await Promise.all(
value.map((subValue, i) =>
const subResults: string[][] = await Promise.all(
(value as any[]).map((subValue: any, i: number) =>
validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables),
),
);
@ -104,11 +116,18 @@ async function validateRule(name, value, rule, options, messageVariables) {
* We use `async-validator` to validate the value.
* But only check one value in a time to avoid namePath validate issue.
*/
export function validateRules(namePath, value, rules, options, validateFirst, messageVariables) {
export function validateRules(
namePath: InternalNamePath,
value: any,
rules: RuleObject[],
options: ValidateOptions,
validateFirst: boolean | 'parallel',
messageVariables?: Record<string, string>,
) {
const name = namePath.join('.');
// Fill rule with context
const filledRules = rules.map(currentRule => {
const filledRules: RuleObject[] = rules.map(currentRule => {
const originValidatorFunc = currentRule.validator;
if (!originValidatorFunc) {
@ -116,11 +135,11 @@ export function validateRules(namePath, value, rules, options, validateFirst, me
}
return {
...currentRule,
validator(rule, val, callback) {
validator(rule: RuleObject, val: any, callback: (error?: string) => void) {
let hasPromise = false;
// Wrap callback only accept when promise not provided
const wrappedCallback = (...args) => {
const wrappedCallback = (...args: string[]) => {
// Wait a tick to make sure return type is a promise
Promise.resolve().then(() => {
warning(
@ -146,7 +165,7 @@ export function validateRules(namePath, value, rules, options, validateFirst, me
warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
if (hasPromise) {
promise
(promise as Promise<void>)
.then(() => {
callback();
})
@ -158,7 +177,7 @@ export function validateRules(namePath, value, rules, options, validateFirst, me
};
});
let summaryPromise;
let summaryPromise: Promise<string[]>;
if (validateFirst === true) {
// >>>>> Validate by serialization
@ -184,12 +203,12 @@ export function validateRules(namePath, value, rules, options, validateFirst, me
summaryPromise = (validateFirst
? finishOnFirstFailed(rulePromises)
: finishOnAllFailed(rulePromises)
).then(errors => {
).then((errors: string[]): string[] | Promise<string[]> => {
if (!errors.length) {
return [];
}
return Promise.reject(errors);
return Promise.reject<string[]>(errors);
});
}
@ -199,7 +218,7 @@ export function validateRules(namePath, value, rules, options, validateFirst, me
return summaryPromise;
}
async function finishOnAllFailed(rulePromises) {
async function finishOnAllFailed(rulePromises: Promise<string[]>[]): Promise<string[]> {
return Promise.all(rulePromises).then(errorsList => {
const errors = [].concat(...errorsList);
@ -207,7 +226,7 @@ async function finishOnAllFailed(rulePromises) {
});
}
async function finishOnFirstFailed(rulePromises) {
async function finishOnFirstFailed(rulePromises: Promise<string[]>[]): Promise<string[]> {
let count = 0;
return new Promise(resolve => {

View File

@ -1,4 +1,5 @@
import { toArray } from './typeUtil';
import { InternalNamePath, NamePath } from '../interface';
/**
* Convert name to internal supported format.
@ -7,35 +8,15 @@ import { toArray } from './typeUtil';
* 123 => [123]
* ['a', 123] => ['a', 123]
*/
export function getNamePath(path) {
export function getNamePath(path: NamePath | null): InternalNamePath {
return toArray(path);
}
// export function getValue(store: Store, namePath: InternalNamePath) {
// const value = get(store, namePath);
// return value;
// }
// export function setValue(store: Store, namePath: InternalNamePath, value: StoreValue): Store {
// const newStore = set(store, namePath, value);
// return newStore;
// }
// export function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store {
// let newStore = {};
// namePathList.forEach(namePath => {
// const value = getValue(store, namePath);
// newStore = setValue(newStore, namePath, value);
// });
// return newStore;
// }
export function containsNamePath(namePathList, namePath) {
export function containsNamePath(namePathList: InternalNamePath[], namePath: InternalNamePath) {
return namePathList && namePathList.some(path => matchNamePath(path, namePath));
}
function isObject(obj) {
function isObject(obj: any) {
return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype;
}
@ -43,8 +24,8 @@ function isObject(obj) {
* Copy values into store and return a new values object
* ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } }
*/
function internalSetValues(store, values) {
const newStore = Array.isArray(store) ? [...store] : { ...store };
function internalSetValues<T>(store: T, values: T): T {
const newStore: T = (Array.isArray(store) ? [...store] : { ...store }) as T;
if (!values) {
return newStore;
@ -62,11 +43,17 @@ function internalSetValues(store, values) {
return newStore;
}
export function setValues(store, ...restValues) {
return restValues.reduce((current, newStore) => internalSetValues(current, newStore), store);
export function setValues<T>(store: T, ...restValues: T[]): T {
return restValues.reduce(
(current: T, newStore: T) => internalSetValues(current, newStore),
store,
);
}
export function matchNamePath(namePath, changedNamePath) {
export function matchNamePath(
namePath: InternalNamePath,
changedNamePath: InternalNamePath | null,
) {
if (!namePath || !changedNamePath || namePath.length !== changedNamePath.length) {
return false;
}