perf: update formItem

v3-form
tangjinzhou 2021-09-21 21:47:30 +08:00
parent c4a60d6070
commit c487be05a8
13 changed files with 221 additions and 98 deletions

View File

@ -27,6 +27,7 @@ import { defaultConfigProvider } from '../config-provider';
import type { VueNode } from '../_util/type';
import { tuple, withInstall } from '../_util/type';
import type { RenderEmptyHandler } from '../config-provider/renderEmpty';
import { useInjectFormItemContext } from '../form/FormItemContext';
export interface CascaderOptionType {
value?: string | number;
@ -220,12 +221,14 @@ const Cascader = defineComponent({
inheritAttrs: false,
props: cascaderProps,
setup() {
const formItemContext = useInjectFormItemContext();
return {
configProvider: inject('configProvider', defaultConfigProvider),
localeData: inject('localeData', {} as any),
cachedOptions: [],
popupRef: undefined,
input: undefined,
formItemContext,
};
},
data() {
@ -323,6 +326,7 @@ const Cascader = defineComponent({
inputFocused: false,
});
this.$emit('blur', e);
this.formItemContext.onFieldBlur();
},
handleInputClick(e: MouseEvent & { nativeEvent?: any }) {
@ -354,6 +358,7 @@ const Cascader = defineComponent({
}
this.$emit('update:value', value);
this.$emit('change', value, selectedOptions);
this.formItemContext.onFieldChange();
},
getLabel() {
@ -474,7 +479,12 @@ const Cascader = defineComponent({
...otherProps
} = props as any;
const { onEvents, extraAttrs } = splitAttrs(this.$attrs);
const { class: className, style, ...restAttrs } = extraAttrs;
const {
class: className,
style,
id = this.formItemContext.id.value,
...restAttrs
} = extraAttrs;
const getPrefixCls = this.configProvider.getPrefixCls;
const renderEmpty = this.configProvider.renderEmpty;
const prefixCls = getPrefixCls('cascader', customizePrefixCls);
@ -570,6 +580,7 @@ const Cascader = defineComponent({
const inputProps = {
...restAttrs,
...tempInputProps,
id,
prefixCls: inputPrefixCls,
placeholder: value && value.length > 0 ? undefined : placeholder,
value: inputValue,

View File

@ -8,6 +8,7 @@ import { defaultConfigProvider } from '../config-provider';
import warning from '../_util/warning';
import type { RadioChangeEvent } from '../radio/interface';
import type { EventHandler } from '../_util/EventInterface';
import { useInjectFormItemContext } from '../form/FormItemContext';
function noop() {}
export const checkboxProps = () => {
@ -38,7 +39,9 @@ export default defineComponent({
props: checkboxProps(),
emits: ['change', 'update:checked'],
setup() {
const formItemContext = useInjectFormItemContext();
return {
formItemContext,
configProvider: inject('configProvider', defaultConfigProvider),
checkboxGroupContext: inject('checkboxGroupContext', undefined),
};
@ -96,7 +99,13 @@ export default defineComponent({
const props = getOptionProps(this);
const { checkboxGroupContext: checkboxGroup, $attrs } = this;
const children = getSlot(this);
const { indeterminate, prefixCls: customizePrefixCls, skipGroup, ...restProps } = props;
const {
indeterminate,
prefixCls: customizePrefixCls,
skipGroup,
id = this.formItemContext.id.value,
...restProps
} = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
const {
@ -109,12 +118,14 @@ export default defineComponent({
} = $attrs;
const checkboxProps: any = {
...restProps,
id,
prefixCls,
...restAttrs,
};
if (checkboxGroup && !skipGroup) {
checkboxProps.onChange = (...args) => {
this.$emit('change', ...args);
this.formItemContext.onFieldChange();
checkboxGroup.toggleOption({ label: children, value: props.value });
};
checkboxProps.name = checkboxGroup.name;

View File

@ -5,6 +5,7 @@ import Checkbox from './Checkbox';
import hasProp, { getSlot } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import type { VueNode } from '../_util/type';
import { useInjectFormItemContext } from '../form/FormItemContext';
export type CheckboxValueType = string | number | boolean;
export interface CheckboxOptionType {
@ -25,10 +26,13 @@ export default defineComponent({
options: { type: Array as PropType<Array<CheckboxOptionType | string>> },
disabled: PropTypes.looseBool,
onChange: PropTypes.func,
id: PropTypes.string,
},
emits: ['change', 'update:value'],
setup() {
const formItemContext = useInjectFormItemContext();
return {
formItemContext,
configProvider: inject('configProvider', defaultConfigProvider),
};
},
@ -95,11 +99,12 @@ export default defineComponent({
// this.$emit('input', val);
this.$emit('update:value', val);
this.$emit('change', val);
this.formItemContext.onFieldChange();
},
},
render() {
const { $props: props, $data: state } = this;
const { prefixCls: customizePrefixCls, options } = props;
const { prefixCls: customizePrefixCls, options, id = this.formItemContext.id.value } = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
let children = getSlot(this);
@ -120,6 +125,10 @@ export default defineComponent({
</Checkbox>
));
}
return <div class={groupPrefixCls}>{children}</div>;
return (
<div class={groupPrefixCls} id={id}>
{children}
</div>
);
},
});

View File

@ -15,6 +15,7 @@ import { commonProps, rangePickerProps } from './props';
import type { PanelMode, RangeValue } from '../../vc-picker/interface';
import type { RangePickerSharedProps } from '../../vc-picker/RangePicker';
import devWarning from '../../vc-util/devWarning';
import { useInjectFormItemContext } from 'ant-design-vue/es/form/FormItemContext';
export default function generateRangePicker<DateType, ExtraProps = {}>(
generateConfig: GenerateConfig<DateType>,
@ -52,6 +53,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
'blur',
],
setup(props, { expose, slots, attrs, emit }) {
const formItemContext = useInjectFormItemContext();
devWarning(
!attrs.getCalendarContainer,
'DatePicker',
@ -86,6 +88,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
const values = maybeToStrings(dates);
emit('update:value', values);
emit('change', values, dateStrings);
formItemContext.onFieldChange();
};
const onOpenChange = (open: boolean) => {
emit('update:open', open);
@ -96,6 +99,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
};
const onBlur = () => {
emit('blur');
formItemContext.onFieldBlur();
};
const onPanelChange = (dates: RangeValue<DateType>, modes: [PanelMode, PanelMode]) => {
const values = maybeToStrings(dates);
@ -154,6 +158,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
renderExtraFooter = slots.renderExtraFooter,
separator = slots.separator?.(),
clearIcon = slots.clearIcon?.(),
id = formItemContext.id.value,
...restProps
} = p;
const { format, showTime } = p as any;
@ -187,6 +192,7 @@ export default function generateRangePicker<DateType, ExtraProps = {}>(
transitionName={transitionName || `${rootPrefixCls.value}-slide-up`}
{...restProps}
{...additionalOverrideProps}
id={id}
value={value.value}
defaultValue={defaultValue.value}
defaultPickerValue={defaultPickerValue.value}

View File

@ -14,6 +14,7 @@ import classNames from '../../_util/classNames';
import { commonProps, datePickerProps } from './props';
import devWarning from '../../vc-util/devWarning';
import { useInjectFormItemContext } from 'ant-design-vue/es/form/FormItemContext';
export default function generateSinglePicker<DateType, ExtraProps = {}>(
generateConfig: GenerateConfig<DateType>,
@ -51,6 +52,7 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
'update:open',
],
setup(props, { slots, expose, attrs, emit }) {
const formItemContext = useInjectFormItemContext();
devWarning(
!(props.monthCellContentRender || slots.monthCellContentRender),
'DatePicker',
@ -91,6 +93,7 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
const value = maybeToString(date);
emit('update:value', value);
emit('change', value, dateString);
formItemContext.onFieldChange();
};
const onOpenChange = (open: boolean) => {
emit('update:open', open);
@ -101,6 +104,7 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
};
const onBlur = () => {
emit('blur');
formItemContext.onFieldBlur();
};
const onPanelChange = (date: DateType, mode: PanelMode | null) => {
const value = maybeToString(date);
@ -157,6 +161,7 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
(props as any).monthCellContentRender ||
slots.monthCellContentRender,
clearIcon = slots.clearIcon?.(),
id = formItemContext.id.value,
...restProps
} = p;
const showTime = p.showTime === '' ? true : p.showTime;
@ -198,6 +203,7 @@ export default function generateSinglePicker<DateType, ExtraProps = {}>(
transitionName={transitionName || `${rootPrefixCls.value}-slide-up`}
{...restProps}
{...additionalOverrideProps}
id={id}
picker={mergedPicker}
value={value.value}
defaultValue={defaultValue.value}

View File

@ -4,9 +4,8 @@ import cloneDeep from 'lodash-es/cloneDeep';
import PropTypes from '../_util/vue-types';
import Row from '../grid/Row';
import type { ColProps } from '../grid/Col';
import { isValidElement, flattenChildren, filterEmpty } from '../_util/props-util';
import { filterEmpty } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import { cloneElement } from '../_util/vnode';
import { validateRules as validateRulesUtil } from './utils/validateUtil';
import { getNamePath } from './utils/valueUtil';
import { toArray } from './utils/typeUtil';
@ -19,6 +18,7 @@ import { useInjectForm } from './context';
import FormItemLabel from './FormItemLabel';
import FormItemInput from './FormItemInput';
import type { ValidationRule } from './Form';
import { useProvideFormItemContext } from './FormItemContext';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number];
@ -271,6 +271,12 @@ export default defineComponent({
clearValidate,
resetField,
});
useProvideFormItemContext({
id: fieldId,
onFieldBlur,
onFieldChange,
});
let registered = false;
watch(
fieldName,
@ -318,36 +324,36 @@ export default defineComponent({
}));
return () => {
const help = props.help ?? (slots.help ? filterEmpty(slots.help()) : null);
const children = flattenChildren(slots.default?.());
let firstChildren = children[0];
if (fieldName.value && props.autoLink && isValidElement(firstChildren)) {
const originalEvents = firstChildren.props || {};
const originalBlur = originalEvents.onBlur;
const originalChange = originalEvents.onChange;
firstChildren = cloneElement(firstChildren, {
...(fieldId.value ? { id: fieldId.value } : undefined),
onBlur: (...args: any[]) => {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
originalBlur[i](...args);
}
} else if (originalBlur) {
originalBlur(...args);
}
onFieldBlur();
},
onChange: (...args: any[]) => {
if (Array.isArray(originalChange)) {
for (let i = 0, l = originalChange.length; i < l; i++) {
originalChange[i](...args);
}
} else if (originalChange) {
originalChange(...args);
}
onFieldChange();
},
});
}
// const children = flattenChildren(slots.default?.());
// let firstChildren = children[0];
// if (fieldName.value && props.autoLink && isValidElement(firstChildren)) {
// const originalEvents = firstChildren.props || {};
// const originalBlur = originalEvents.onBlur;
// const originalChange = originalEvents.onChange;
// firstChildren = cloneElement(firstChildren, {
// ...(fieldId.value ? { id: fieldId.value } : undefined),
// onBlur: (...args: any[]) => {
// if (Array.isArray(originalChange)) {
// for (let i = 0, l = originalChange.length; i < l; i++) {
// originalBlur[i](...args);
// }
// } else if (originalBlur) {
// originalBlur(...args);
// }
// onFieldBlur();
// },
// onChange: (...args: any[]) => {
// if (Array.isArray(originalChange)) {
// for (let i = 0, l = originalChange.length; i < l; i++) {
// originalChange[i](...args);
// }
// } else if (originalChange) {
// originalChange(...args);
// }
// onFieldChange();
// },
// });
// }
return (
<Row
{...attrs}
@ -357,32 +363,37 @@ export default defineComponent({
attrs.class,
]}
key="row"
>
{/* Label */}
<FormItemLabel
{...props}
htmlFor={fieldId.value}
required={isRequired.value}
requiredMark={formContext.requiredMark.value}
prefixCls={prefixCls.value}
onClick={onLabelClick}
label={props.label ?? slots.label?.()}
/>
{/* Input Group */}
<FormItemInput
{...props}
errors={help !== undefined && help !== null ? toArray(help) : errors.value}
prefixCls={prefixCls.value}
status={validateState.value}
onDomErrorVisibleChange={(v: boolean) => (domErrorVisible.value = v)}
validateStatus={validateState.value}
ref={inputRef}
help={help}
extra={props.extra ?? slots.extra?.()}
>
{[firstChildren, children.slice(1)]}
</FormItemInput>
</Row>
v-slots={{
default: () => (
<>
{/* Label */}
<FormItemLabel
{...props}
htmlFor={fieldId.value}
required={isRequired.value}
requiredMark={formContext.requiredMark.value}
prefixCls={prefixCls.value}
onClick={onLabelClick}
label={props.label ?? slots.label?.()}
/>
{/* Input Group */}
<FormItemInput
{...props}
errors={help !== undefined && help !== null ? toArray(help) : errors.value}
prefixCls={prefixCls.value}
status={validateState.value}
onDomErrorVisibleChange={(v: boolean) => (domErrorVisible.value = v)}
validateStatus={validateState.value}
ref={inputRef}
help={help}
extra={props.extra ?? slots.extra?.()}
v-slots={{ default: slots.default }}
// v-slots={{ default: () => [firstChildren, children.slice(1)] }}
></FormItemInput>
</>
),
}}
></Row>
);
};
},

View File

@ -0,0 +1,29 @@
import type { ComputedRef, InjectionKey } from 'vue';
import { computed, inject, provide } from 'vue';
export type FormItemContext = {
id: ComputedRef<string | number>;
onFieldBlur: () => void;
onFieldChange: () => void;
};
type ContextProps = FormItemContext;
const ContextKey: InjectionKey<ContextProps> = Symbol('ContextProps');
export const useProvideFormItemContext = (props: ContextProps) => {
provide(ContextKey, props);
};
export const useInjectFormItemContext = () => {
const defaultContext: ContextProps = {
id: computed(() => undefined),
onFieldBlur: () => {},
onFieldChange: () => {},
};
// We should prevent the passing of context for children
provide(ContextKey, defaultContext);
return inject(ContextKey, defaultContext);
};

View File

@ -86,33 +86,32 @@ const FormItemInput = defineComponent({
// 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>
<Col
{...mergedWrapperCol}
class={className}
v-slots={{
default: () => (
<>
<div class={`${baseClassName}-control-input`}>
<div class={`${baseClassName}-control-input-content`}>{slots.default?.()}</div>
{hasFeedback && IconNode ? (
<span class={`${baseClassName}-children-icon`}>
<IconNode />
</span>
) : null}
</div>
<ErrorList
errors={errors}
help={help}
onDomErrorVisibleChange={onDomErrorVisibleChange}
/>
{extra ? <div class={`${baseClassName}-extra`}>{extra}</div> : null}
</>
),
}}
></Col>
);
};
},

View File

@ -27,12 +27,13 @@ Just add the `rules` attribute for `Form` component, pass validation rules, and
<a-input v-model:value="formState.name" />
</a-form-item>
<a-form-item label="Activity zone" name="region">
<a-select v-model:value="formState.region" placeholder="please select your zone">
<a-select-option value="shanghai">Zone one</a-select-option>
<a-select-option value="beijing">Zone two</a-select-option>
</a-select>
<a-select
v-model:value="formState.region"
placeholder="please select your zone"
:options="option"
></a-select>
</a-form-item>
<a-form-item label="Activity time" required name="date1">
<!-- <a-form-item label="Activity time" required name="date1">
<a-date-picker
v-model:value="formState.date1"
show-time
@ -59,7 +60,7 @@ Just add the `rules` attribute for `Form` component, pass validation rules, and
</a-form-item>
<a-form-item label="Activity form" name="desc">
<a-textarea v-model:value="formState.desc" />
</a-form-item>
</a-form-item> -->
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" @click="onSubmit">Create</a-button>
<a-button style="margin-left: 10px" @click="resetForm">Reset</a-button>
@ -131,6 +132,12 @@ export default defineComponent({
rules,
onSubmit,
resetForm,
option: ref([
{
value: 'lucy',
label: 'Lucy',
},
]),
};
},
});

View File

@ -7,6 +7,7 @@ import inputProps from './inputProps';
import { hasProp, getComponent, getOptionProps } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import ClearableLabeledInput from './ClearableLabeledInput';
import { useInjectFormItemContext } from '../form/FormItemContext';
export function fixControlledValue(value: string | number) {
if (typeof value === 'undefined' || value === null) {
@ -56,11 +57,13 @@ export default defineComponent({
...inputProps,
},
setup() {
const formItemContext = useInjectFormItemContext();
return {
configProvider: inject('configProvider', defaultConfigProvider),
removePasswordTimeout: undefined,
input: null,
clearableInput: null,
formItemContext,
};
},
data() {
@ -100,6 +103,7 @@ export default defineComponent({
handleInputBlur(e: Event) {
this.isFocused = false;
this.onBlur && this.onBlur(e);
this.formItemContext.onFieldBlur();
},
focus() {
@ -138,6 +142,7 @@ export default defineComponent({
this.$emit('update:value', (e.target as HTMLInputElement).value);
this.$emit('change', e);
this.$emit('input', e);
this.formItemContext.onFieldChange();
},
handleReset(e: Event) {
this.setValue('', () => {
@ -240,6 +245,12 @@ export default defineComponent({
prefix,
isFocused,
};
return <ClearableLabeledInput {...props} ref={this.saveClearableInput} />;
return (
<ClearableLabeledInput
{...props}
id={props.id ?? this.formItemContext.id.value}
ref={this.saveClearableInput}
/>
);
},
});

View File

@ -7,6 +7,7 @@ import { defaultConfigProvider } from '../config-provider';
import { fixControlledValue, resolveOnChange } from './Input';
import classNames from '../_util/classNames';
import PropTypes, { withUndefined } from '../_util/vue-types';
import { useInjectFormItemContext } from '../form/FormItemContext';
const TextAreaProps = {
...inputProps,
@ -24,10 +25,12 @@ export default defineComponent({
...TextAreaProps,
},
setup() {
const formItemContext = useInjectFormItemContext();
return {
configProvider: inject('configProvider', defaultConfigProvider),
resizableTextArea: null,
clearableInput: null,
formItemContext,
};
},
data() {
@ -71,6 +74,7 @@ export default defineComponent({
this.$emit('update:value', (e.target as any).value);
this.$emit('change', e);
this.$emit('input', e);
this.formItemContext.onFieldChange();
},
handleChange(e: Event) {
const { value, composing, isComposing } = e.target as any;
@ -103,6 +107,10 @@ export default defineComponent({
});
resolveOnChange(this.resizableTextArea.textArea, e, this.triggerChange);
},
handleBlur(e: Event) {
this.$emit('blur', e);
this.formItemContext.onFieldBlur();
},
renderTextArea(prefixCls: string) {
const props = getOptionProps(this);
@ -118,7 +126,13 @@ export default defineComponent({
onChange: this.handleChange,
onKeydown: this.handleKeyDown,
};
return <ResizableTextArea {...resizeProps} ref={this.saveTextArea} />;
return (
<ResizableTextArea
{...resizeProps}
id={resizeProps.id ?? this.formItemContext.id.value}
ref={this.saveTextArea}
/>
);
},
},
render() {

View File

@ -8,6 +8,7 @@ import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject';
import omit from '../_util/omit';
import { useInjectFormItemContext } from '../form/FormItemContext';
type RawValue = string | number;
@ -49,7 +50,7 @@ const Select = defineComponent({
inheritAttrs: false,
props: selectProps(),
SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE',
emits: ['change', 'update:value'],
emits: ['change', 'update:value', 'blur'],
slots: [
'notFoundContent',
'suffixIcon',
@ -61,7 +62,7 @@ const Select = defineComponent({
],
setup(props, { attrs, emit, slots, expose }) {
const selectRef = ref();
const formItemContext = useInjectFormItemContext();
const focus = () => {
if (selectRef.value) {
selectRef.value.focus();
@ -99,6 +100,11 @@ const Select = defineComponent({
const triggerChange = (...args: any[]) => {
emit('update:value', args[0]);
emit('change', ...args);
formItemContext.onFieldChange();
};
const handleBlur = (e: InputEvent) => {
emit('blur', e);
formItemContext.onFieldBlur();
};
expose({
blur,
@ -113,6 +119,7 @@ const Select = defineComponent({
dropdownClassName,
virtual,
dropdownMatchSelectWidth,
id = formItemContext.id.value,
} = props;
const { renderEmpty, getPopupContainer: getContextPopupContainer } = configProvider;
@ -175,6 +182,8 @@ const Select = defineComponent({
getPopupContainer={getPopupContainer || getContextPopupContainer}
dropdownClassName={rcSelectRtlDropDownClassName}
onChange={triggerChange}
onBlur={handleBlur}
id={id}
dropdownRender={selectProps.dropdownRender || slots.dropdownRender}
v-slots={{ option: slots.option }}
>

View File

@ -1,5 +1,5 @@
// debugger tsx
import Demo from '../../components/form/demo/index.vue';
import Demo from '../../components/form/demo/validation.vue';
export default {
render() {