[element plus source code analysis] form form component

YL_ FE 2021-09-15 09:48:41

One 、 Component is introduced

el-form Form components , It is a component used frequently in daily development , Usually by the input box 、 Selectors 、 Radio buttons 、 Multi selection box and other controls , To collect 、 check 、 Submit data .

Everyone should be right about el-form Components are very familiar , Just look at the source code ^_^

Two 、 Source code analysis

el-form The source code of 3 part :form form-item label-wrap

2.1 form.vue

// packages\components\form\src\form.vue
<template>
<form class="el-form" :class="[ labelPosition ? 'el-form--label-' + labelPosition : '', { 'el-form--inline': inline } ]" >
<slot></slot>
</form>
</template>
<script lang="ts"> import { computed, defineComponent, provide, reactive, ref, toRefs, watch } from 'vue' import { FieldErrorList } from 'async-validator' import mitt from 'mitt' import { elFormEvents, elFormKey } from '@element-plus/tokens' import type { PropType } from 'vue' import type { ComponentSize } from '@element-plus/utils/types' import type { FormRulesMap } from './form.type' import type { ElFormItemContext as FormItemCtx, ValidateFieldCallback } from '@element-plus/tokens' // label Automatic width correlation  function useFormLabelWidth() { // Potential label Width array , Refers to those that are not specified label-width Of form-item Of label The calculated width that should be occupied  const potentialLabelWidthArr = ref([]) // The maximum of all potential widths is taken as all auto label-width form-item Of label Occupied width  const autoLabelWidth = computed(() => { if (!potentialLabelWidthArr.value.length) return '0' const max = Math.max(...potentialLabelWidthArr.value) return max ? `${max}px` : '' }) // according to label Width , Gets the value of the corresponding element in the potential width array index function getLabelWidthIndex(width: number) { const index = potentialLabelWidthArr.value.indexOf(width) if (index === -1) { console.warn('[Element Warn][ElementForm]unexpected width ' + width) } return index } // form-item Medium label register label Width , stay label-warp in label Called when the width changes  function registerLabelWidth(val: number, oldVal: number) { if (val && oldVal) { // There are new and old values , Explain that you have previously registered , Then find the old one index, Replace values  const index = getLabelWidthIndex(oldVal) potentialLabelWidthArr.value.splice(index, 1, val) } else if (val) { // Only new values , It is regarded as new  potentialLabelWidthArr.value.push(val) } } // Anti registration , Delete the corresponding width value from the potential width array  function deregisterLabelWidth(val: number) { const index = getLabelWidthIndex(val) index > -1 && potentialLabelWidthArr.value.splice(index, 1) } return { autoLabelWidth, registerLabelWidth, deregisterLabelWidth, } } export interface Callback { (isValid?: boolean, invalidFields?: FieldErrorList): void } export default defineComponent({ name: 'ElForm', props: { // form The value of the binding  model: Object, // Field validation rules rules: Object as PropType<FormRulesMap>, // Where the label is located Optional top right left labelPosition: String, // label Width , You can specify a value , You can also set it to 'auto' labelWidth: { type: [String, Number], default: '', }, // label The suffix , For example, it can be set to colon : labelSuffix: { type: String, default: '', }, // In line layout , One form-item Don't occupy a single line  inline: Boolean, // In line form , Show verification information  inlineMessage: Boolean, // Whether in input The verification result icon is displayed in the box  statusIcon: Boolean, // Whether to display verification information  showMessage: { type: Boolean, default: true, }, // Size medium small mini size: String as PropType<ComponentSize>, // Ban  disabled: Boolean, // rules When the change , Immediately trigger a verification  validateOnRuleChange: { type: Boolean, default: true, }, // Hide the red asterisk in front of the required items  hideRequiredAsterisk: { type: Boolean, default: false, }, // Scroll to the wrong field scrollToError: Boolean, }, emits: ['validate'], setup(props, { emit }) { // Create an event bus  const formMitt = mitt() // The storage unit form-item const fields: FormItemCtx[] = [] // monitor rules change  watch( () => props.rules, () => { // every last form-item fields.forEach(field => { // First remove the pair el.form.blur el.form.change Event monitoring  field.removeValidateEvents() // According to the new rules, Re monitor  field.addValidateEvents() }) // Immediately trigger a verification  if (props.validateOnRuleChange) { validate(() => ({})) } }, ) // Event bus , monitor form-item Registration events  formMitt.on<FormItemCtx>(elFormEvents.addField, field => { if (field) { fields.push(field) } }) // Event bus , monitor form-item The cancellation of the event  formMitt.on<FormItemCtx>(elFormEvents.removeField, field => { if (field.prop) { fields.splice(fields.indexOf(field), 1) } }) // Reset form , And remove the verification result  const resetFields = () => { if (!props.model) { console.warn( '[Element Warn][Form]model is required for resetFields to work.', ) return } fields.forEach(field => { // One by one call form-item Of resetField Method  field.resetField() }) } // Clear the verification result , The parameter can be a string array or a string  const clearValidate = (props: string | string[] = []) => { const fds = props.length ? typeof props === 'string' ? fields.filter(field => props === field.prop) : fields.filter(field => props.indexOf(field.prop) > -1) : fields // Execute the matching fields one by one clearValidate fds.forEach(field => { field.clearValidate() }) } // Check the whole form const validate = (callback?: Callback) => { if (!props.model) { console.warn( '[Element Warn][Form]model is required for validate to work!', ) return } let promise: Promise<boolean> | undefined // If not callback, be return Go out for one promise, Users can use validate().then Take the next step  if (typeof callback !== 'function') { promise = new Promise((resolve, reject) => { callback = function (valid, invalidFields) { if (valid) { resolve(true) } else { reject(invalidFields) } } }) } if (fields.length === 0) { callback(true) } let valid = true let count = 0 let invalidFields = {} let firstInvalidFields // Field by field verification  for (const field of fields) { // The first parameter is trigger, Pass in '' Indicates all that match the field rule field.validate('', (message, field) => { if (message) { // A field verification failed , Then the whole form The position of the check mark is false valid = false // Record the first field that failed verification  firstInvalidFields || (firstInvalidFields = field) } invalidFields = { ...invalidFields, ...field } if (++count === fields.length) { // End of verification , Call the incoming callback Method  callback(valid, invalidFields) } }) } // If form Check failed , also scrollToError by true if (!valid && props.scrollToError) { // Scroll to the first field where the verification failed  scrollToField(Object.keys(firstInvalidFields)[0]) } return promise } // Verify the specified field  const validateField = (props: string | string[], cb: ValidateFieldCallback) => { props = [].concat(props) // according to props Find the corresponding field  const fds = fields.filter(field => props.indexOf(field.prop) !== -1) if (!fields.length) { console.warn('[Element Warn]please pass correct props!') return } // Matched fields , Check one by one  fds.forEach(field => { field.validate('', cb) }) } // Scroll to the specified field const scrollToField = (prop: string) => { fields.forEach(item => { if (item.prop === prop) { // call dom The native api item.$el.scrollIntoView() } }) } const elForm = reactive({ formMitt, ...toRefs(props), resetFields, clearValidate, validateField, emit, ...useFormLabelWidth(), }) // Provide data to child components  provide(elFormKey, elForm) return { validate, // export resetFields, clearValidate, validateField, scrollToField, } }, }) </script>
 Copy code 

2.2 form-item.vue

// packages\components\form\src\form-item.vue
<template>
<div ref="formItemRef" class="el-form-item" :class="formItemClass">
<!-- label part -->
<LabelWrap :is-auto-width="labelStyle.width === 'auto'" :update-all="elForm.labelWidth === 'auto'" >
<label v-if="label || $slots.label" :for="labelFor" class="el-form-item__label" :style="labelStyle" >
<slot name="label" :label="label + elForm.labelSuffix">{{ label + elForm.labelSuffix }}</slot>
</label>
</LabelWrap>
<!-- The content part -->
<div class="el-form-item__content" :style="contentStyle">
<slot></slot>
<!-- Prompt for verification failure -->
<transition name="el-zoom-in-top">
<slot v-if="shouldShowError" name="error" :error="validateMessage">
<div class="el-form-item__error" :class="{ 'el-form-item__error--inline': typeof inlineMessage === 'boolean' ? inlineMessage : elForm.inlineMessage || false }" >{{ validateMessage }}</div>
</slot>
</transition>
</div>
</div>
</template>
<script lang="ts"> import { computed, defineComponent, getCurrentInstance, inject, nextTick, onBeforeUnmount, onMounted, provide, reactive, ref, toRefs, watch, } from 'vue' import AsyncValidator from 'async-validator' import mitt from 'mitt' import { NOOP } from '@vue/shared' import { addUnit, getPropByPath, useGlobalConfig } from '@element-plus/utils/util' import { isValidComponentSize } from '@element-plus/utils/validators' import LabelWrap from './label-wrap' import { elFormEvents, elFormItemKey, elFormKey } from '@element-plus/tokens' import type { PropType, CSSProperties } from 'vue' import type { ComponentSize } from '@element-plus/utils/types' import type { ElFormContext, ValidateFieldCallback } from '@element-plus/tokens' import type { FormItemRule } from './form.type' export default defineComponent({ name: 'ElFormItem', componentName: 'ElFormItem', components: { LabelWrap, }, props: { label: String, labelWidth: { type: [String, Number], default: '', }, prop: String, required: { type: Boolean, default: undefined, }, rules: [Object, Array] as PropType<FormItemRule | FormItemRule[]>, error: String, validateStatus: String, for: String, inlineMessage: { type: [String, Boolean], default: '', }, showMessage: { type: Boolean, default: true, }, size: { type: String as PropType<ComponentSize>, validator: isValidComponentSize, }, }, setup(props, { slots }) { const formItemMitt = mitt() const $ELEMENT = useGlobalConfig() const elForm = inject(elFormKey, {} as ElFormContext) const validateState = ref('') const validateMessage = ref('') const validateDisabled = ref(false) const computedLabelWidth = ref('') const formItemRef = ref<HTMLDivElement>() const vm = getCurrentInstance() const isNested = computed(() => { let parent = vm.parent while (parent && parent.type.name !== 'ElForm') { if (parent.type.name === 'ElFormItem') { return true } parent = parent.parent } return false }) let initialValue = undefined watch( () => props.error, val => { validateMessage.value = val validateState.value = val ? 'error' : '' }, { immediate: true, }, ) watch( () => props.validateStatus, val => { validateState.value = val }, ) const labelFor = computed(() => props.for || props.prop) // label part style: Take up the width of  const labelStyle = computed(() => { const ret: CSSProperties = {} if (elForm.labelPosition === 'top') return ret // addUnit Is the way to add units  // If the parameter is a number , Then it is converted to band px String ,eg:addUnit(80) = '80px'; If the parameter is a string , directly return character string  // form-item Incoming labelWidth Priority over form Upper labelWidth const labelWidth = addUnit(props.labelWidth) || addUnit(elForm.labelWidth) if (labelWidth) { ret.width = labelWidth } return ret }) // Content area style: No, label when , need margin-left const contentStyle = computed(() => { const ret: CSSProperties = {} if (elForm.labelPosition === 'top' || elForm.inline) { return ret } if (!props.label && !props.labelWidth && isNested.value) { return ret } const labelWidth = addUnit(props.labelWidth) || addUnit(elForm.labelWidth) // When there is no label when , The content needs to be offset to the right ( Yes label when ,label Takes up a certain width , The content will automatically go to the right ) if (!props.label && !slots.label) { ret.marginLeft = labelWidth } return ret }) const fieldValue = computed(() => { const model = elForm.model // If form No binding model, perhaps form-item No, prop attribute , return undefined if (!model || !props.prop) { return } let path = props.prop // In the path : Replace with . if (path.indexOf(':') !== -1) { path = path.replace(/:/, '.') } // according to path, stay form Of model Get the corresponding value in  return getPropByPath(model, path, true).v }) // If required  const isRequired = computed(() => { let rules = getRules() let required = false if (rules && rules.length) { rules.every(rule => { if (rule.required) { required = true return false } return true }) } return required }) const elFormItemSize = computed(() => props.size || elForm.size) const sizeClass = computed<ComponentSize>(() => { return elFormItemSize.value || $ELEMENT.size }) const validate = (trigger: string, callback: ValidateFieldCallback = NOOP) => { validateDisabled.value = false const rules = getFilteredRule(trigger) if ((!rules || rules.length === 0) && props.required === undefined) { callback() return } validateState.value = 'validating' const descriptor = {} if (rules && rules.length > 0) { rules.forEach(rule => { delete rule.trigger }) } descriptor[props.prop] = rules const validator = new AsyncValidator(descriptor) const model = {} model[props.prop] = fieldValue.value validator.validate( model, { firstFields: true }, (errors, invalidFields) => { validateState.value = !errors ? 'success' : 'error' validateMessage.value = errors ? errors[0].message : '' callback(validateMessage.value, invalidFields) elForm.emit?.( 'validate', props.prop, !errors, validateMessage.value || null, ) }, ) } const clearValidate = () => { validateState.value = '' validateMessage.value = '' validateDisabled.value = false } // Reset form item  const resetField = () => { // Clear the verification result  validateState.value = '' validateMessage.value = '' let model = elForm.model let value = fieldValue.value let path = props.prop if (path.indexOf(':') !== -1) { path = path.replace(/:/, '.') } // stay model According to the path, Find Ben form-item Corresponding properties  let prop = getPropByPath(model, path, true) // Reset value , Check is forbidden  validateDisabled.value = true // Restore to in mounted Initial value recorded at  if (Array.isArray(value)) { prop.o[prop.k] = [].concat(initialValue) } else { prop.o[prop.k] = initialValue } // reset validateDisabled after onFieldChange triggered nextTick(() => { validateDisabled.value = false }) } // Get the current form-item Of rules const getRules = () => { // form Total rules, Contains all the form-item Of rules const formRules = elForm.rules // form-item Self rules const selfRules = props.rules // Incoming required attribute  const requiredRule = props.required !== undefined ? { required: !!props.required } : [] // according to prop, from form Total rules Current found in form-item Of rules const prop = getPropByPath(formRules, props.prop || '', false) const normalizedRule = formRules ? (prop.o[props.prop || ''] || prop.v) : [] // selfRules Prior to the normalizedRule return [].concat(selfRules || normalizedRule || []).concat(requiredRule) } // according to trigger type , Get the corresponding rules const getFilteredRule = trigger => { const rules = getRules() return rules .filter(rule => { // If rule No settings trigger perhaps trigger Set to '' if (!rule.trigger || trigger === '') return true if (Array.isArray(rule.trigger)) { // rule Medium trigger When it's an array , Judge the filtered trigger In the array  return rule.trigger.indexOf(trigger) > -1 } else { // otherwise rule.trigger With filtered trigger identical  return rule.trigger === trigger } }) // an , Reassemble , Is it for shallow copy ? .map(rule => ({ ...rule })) } // When you lose focus , Trigger validate blur const onFieldBlur = () => { validate('blur') } // Value changes , Trigger validate change const onFieldChange = () => { // Ban validate Sign bit is true when  if (validateDisabled.value) { // The flag bit is set to false validateDisabled.value = false // Don't do it this time validate return } validate('change') } // Update calculated label width, stay label-wrap In the called  const updateComputedLabelWidth = (width: string | number) => { computedLabelWidth.value = width ? `${width}px` : '' } // add to validate Event monitoring  const addValidateEvents = () => { const rules = getRules() // If form-item There is rules // props.required !== undefined It's not necessary , because props.required When it's worth it ,rules It must have elements  if (rules.length || props.required !== undefined) { // Use Mitt Event bus , monitor el.form.blur el.form.change event  formItemMitt.on('el.form.blur', onFieldBlur) formItemMitt.on('el.form.change', onFieldChange) } } // Cancellation of listening  const removeValidateEvents = () => { formItemMitt.off('el.form.blur', onFieldBlur) formItemMitt.off('el.form.change', onFieldChange) } const elFormItem = reactive({ ...toRefs(props), size: sizeClass, validateState, $el: formItemRef, formItemMitt, removeValidateEvents, addValidateEvents, resetField, clearValidate, validate, updateComputedLabelWidth, }) onMounted(() => { // If you pass in prop Field  if (props.prop) { // formMitt The bus sends el.form.addField event Parameter is elFormItem elForm.formMitt?.emit(elFormEvents.addField, elFormItem) let value = fieldValue.value // Record the initial value , stay resetField Used to restore the initial state  // If it's an array , By extending + Reassemble for light copy  initialValue = Array.isArray(value) ? [...value] : value // monitor el.form.blur el.form.change event  addValidateEvents() } }) onBeforeUnmount(() => { // formMitt The bus sends el.form.removeField event Parameter is elFormItem elForm.formMitt?.emit(elFormEvents.removeField, elFormItem) }) // Provide data to child components  provide(elFormItemKey, elFormItem) // dynamic class const formItemClass = computed(() => [ { // statusIcon It refers to whether the verification result feedback icon is displayed in the input box  'el-form-item--feedback': elForm.statusIcon, // Various states  'is-error': validateState.value === 'error', 'is-validating': validateState.value === 'validating', 'is-success': validateState.value === 'success', 'is-required': isRequired.value || props.required, // Hide required fields label Next to the red asterisk  'is-no-asterisk': elForm.hideRequiredAsterisk, }, sizeClass.value ? 'el-form-item--' + sizeClass.value : '', ]) // Whether error messages should be displayed  const shouldShowError = computed(() => { // Check failed , And form and form-item The binding of showMessage by true( The default is true) return validateState.value === 'error' && props.showMessage && elForm.showMessage }) return { formItemRef, formItemClass, shouldShowError, elForm, labelStyle, contentStyle, validateMessage, labelFor, resetField, clearValidate, } }, }) </script>
 Copy code 

2.3 label-wrap.ts

// packages\components\form\src\label-wrap.ts
import { defineComponent, Fragment, h, inject, nextTick, onBeforeUnmount, onMounted, onUpdated, ref, watch } from "vue";
import { addResizeListener, removeResizeListener, ResizableElement } from "@element-plus/utils/resize-event";
import { elFormItemKey, elFormKey } from "@element-plus/tokens";
import type { CSSProperties } from "vue";
import type { Nullable } from "@element-plus/utils/types";
export default defineComponent({
name: "ElLabelWrap",
props: {
isAutoWidth: Boolean,
// When el-form Of labelWidth yes auto when , by true
updateAll: Boolean,
},
setup(props, { slots }) {
const el = ref<Nullable<HTMLElement>>(null);
const elForm = inject(elFormKey);
const elFormItem = inject(elFormItemKey);
const computedWidth = ref(0);
// Monitor width changes 
watch(computedWidth, (val, oldVal) => {
if (props.updateAll) {
elForm.registerLabelWidth(val, oldVal);
// to update form-item Medium label Width 
elFormItem.updateComputedLabelWidth(val);
}
});
const getLabelWidth = () => {
if (el.value?.firstElementChild) {
// The width of the first child node element 
const width = window.getComputedStyle(el.value.firstElementChild).width;
// Rounding up 
return Math.ceil(parseFloat(width));
} else {
return 0;
}
};
const updateLabelWidth = (action = "update") => {
nextTick(() => {
// label When it is automatic width 
if (slots.default && props.isAutoWidth) {
if (action === "update") {
// update Under the circumstances : Reread the width of the child node element 
computedWidth.value = getLabelWidth();
} else if (action === "remove") {
elForm.deregisterLabelWidth(computedWidth.value);
}
}
});
};
const updateLabelWidthFn = () => updateLabelWidth("update");
// When mounting 
onMounted(() => {
// Add... To the first child node element resize monitor , stay resize when , to update label Width 
addResizeListener(el.value.firstElementChild as ResizableElement, updateLabelWidthFn);
updateLabelWidthFn();
});
// update , Recalculate the current form-item label The width of the element 
onUpdated(updateLabelWidthFn);
// When uninstalling 
onBeforeUnmount(() => {
updateLabelWidth("remove");
removeResizeListener(el.value.firstElementChild as ResizableElement, updateLabelWidthFn);
});
// render function 
function render() {
if (!slots) return null;
if (props.isAutoWidth) {
// Get current form Of label Automatic width 
const autoLabelWidth = elForm.autoLabelWidth;
const style = {} as CSSProperties;
if (autoLabelWidth && autoLabelWidth !== "auto") {
// Distance to offset = form Of label Automatic width - With the current label-wrap Calculated width 
const marginWidth = Math.max(0, parseInt(autoLabelWidth, 10) - computedWidth.value);
const marginPosition = elForm.labelPosition === "left" ? "marginRight" : "marginLeft";
if (marginWidth) {
style[marginPosition] = marginWidth + "px";
}
}
// If label The width is automatic , Then wrap a layer of div, adopt margin Control alignment 
return h(
"div",
{
ref: el,
class: ["el-form-item__label-wrap"],
style,
},
slots.default?.()
);
} else {
// If label-width Is specified , You don't need a package div, Direct Render label
return h(Fragment, { ref: el }, slots.default?.());
}
}
return render;
},
});
 Copy code 

2.4 summary :

  1. label-wrap Pass through ResizeObserver monitor label The width of the child element changes , And register the width to el-form Medium potentialLabelWidthArr in ,el-form take potentialLabelWidthArr The maximum value in is set to label Automatic width value for ; If form-item Of label Set to auto when ,label-wrap stay label The element is wrapped with div, By setting margin-top/margin-left Conduct label Alignment of ;
  2. Use Mitt Event bus is used for event communication between components , But now that it has passed provide Inject... Into a subcomponent form The instance , Why not call the method of the instance directly , And use the bus ;
  3. rules Do not set trigger, It means to participate in the verification at any time , Set up trigger Then it will only participate in the verification under the specified trigger ;form The verification of is one by one form-item,form-item Used in the verification of async-validator check ;
  4. form-item Of prop Attributes are important , stay label Width and verification , It's all through prop Property as the path , find model Corresponding value in ;
Please bring the original link to reprint ,thank
Similar articles

2021-09-15