mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-17 11:49:41 +08:00
Feat/form (#342)
* feat(form): add form component fix #125 * test(form): add test code * docs(form): add form doc * feat: add uitls merge * fix(form): fix style * test(form): add form test code * refactor(form): review changes * test(form): use idiomatic vue-test-util methods * feat(core): bump vue version * feat(form): rewrite label wrap * feat(form): fix tons of bugs * fix(form): reuse ts extension * refactor(form): move out label width computation * fix(form): fix tons of bugs * fix(form): test Co-authored-by: 286506460 <286506460@qq.com>
This commit is contained in:
parent
128436214e
commit
62f1135768
@ -37,6 +37,7 @@ import ElDialog from '@element-plus/dialog'
|
|||||||
import ElCalendar from '@element-plus/calendar'
|
import ElCalendar from '@element-plus/calendar'
|
||||||
import ElInfiniteScroll from '@element-plus/infinite-scroll'
|
import ElInfiniteScroll from '@element-plus/infinite-scroll'
|
||||||
import ElDrawer from '@element-plus/drawer'
|
import ElDrawer from '@element-plus/drawer'
|
||||||
|
import ElForm from '@element-plus/form'
|
||||||
import ElUpload from '@element-plus/upload'
|
import ElUpload from '@element-plus/upload'
|
||||||
import ElTree from '@element-plus/tree'
|
import ElTree from '@element-plus/tree'
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ export {
|
|||||||
ElCalendar,
|
ElCalendar,
|
||||||
ElInfiniteScroll,
|
ElInfiniteScroll,
|
||||||
ElDrawer,
|
ElDrawer,
|
||||||
|
ElForm,
|
||||||
ElUpload,
|
ElUpload,
|
||||||
ElTree,
|
ElTree,
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"@element-plus/collapse": "^0.0.0",
|
"@element-plus/collapse": "^0.0.0",
|
||||||
"@element-plus/time-picker": "^0.0.0",
|
"@element-plus/time-picker": "^0.0.0",
|
||||||
"@element-plus/tabs": "^0.0.0",
|
"@element-plus/tabs": "^0.0.0",
|
||||||
|
"@element-plus/form": "^0.0.0",
|
||||||
"@element-plus/tree": "^0.0.0"
|
"@element-plus/tree": "^0.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1014
packages/form/__tests__/form.spec.ts
Normal file
1014
packages/form/__tests__/form.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/form/index.ts
Normal file
10
packages/form/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { App } from 'vue'
|
||||||
|
import Form from './src/form.vue'
|
||||||
|
import FormItem from './src/form-item.vue'
|
||||||
|
import LabelWrap from './src/label-wrap.vue'
|
||||||
|
|
||||||
|
export default (app: App): void => {
|
||||||
|
app.component(Form.name, Form)
|
||||||
|
app.component(FormItem.name, FormItem)
|
||||||
|
app.component(LabelWrap.name, LabelWrap)
|
||||||
|
}
|
15
packages/form/package.json
Normal file
15
packages/form/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@element-plus/form",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0-rc.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/test-utils": "^2.0.0-beta.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"async-validator": "^3.4.0"
|
||||||
|
}
|
||||||
|
}
|
374
packages/form/src/form-item.vue
Normal file
374
packages/form/src/form-item.vue
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="el-form-item"
|
||||||
|
:class="formItemClass"
|
||||||
|
>
|
||||||
|
<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 + elForm.labelSuffix }}</slot>
|
||||||
|
</label>
|
||||||
|
</LabelWrap>
|
||||||
|
<div class="el-form-item__content" :style="contentStyle">
|
||||||
|
<slot></slot>
|
||||||
|
<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 {
|
||||||
|
defineComponent,
|
||||||
|
provide,
|
||||||
|
inject,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
getCurrentInstance,
|
||||||
|
toRefs,
|
||||||
|
reactive,
|
||||||
|
PropType,
|
||||||
|
} from 'vue'
|
||||||
|
import AsyncValidator from 'async-validator'
|
||||||
|
import { RuleItem } from 'async-validator'
|
||||||
|
import LabelWrap from './label-wrap'
|
||||||
|
import { getPropByPath, useGlobalConfig } from '@element-plus/utils/util'
|
||||||
|
import mitt from 'mitt'
|
||||||
|
import { elFormKey, elFormItemKey, ValidateFieldCallback } from './token'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ElFormItem',
|
||||||
|
componentName: 'ElFormItem',
|
||||||
|
components: {
|
||||||
|
LabelWrap,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
label: String,
|
||||||
|
labelWidth: String,
|
||||||
|
prop: String,
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
rules: [Object, Array] as PropType<RuleItem | RuleItem[]>,
|
||||||
|
error: String,
|
||||||
|
validateStatus: String,
|
||||||
|
for: String,
|
||||||
|
inlineMessage: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
showMessage: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
size: String,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const formItemMitt = mitt()
|
||||||
|
const $ELEMENT = useGlobalConfig()
|
||||||
|
|
||||||
|
const elForm = inject(elFormKey)
|
||||||
|
const validateState = ref('')
|
||||||
|
const validateMessage = ref('')
|
||||||
|
const validateDisabled = ref(false)
|
||||||
|
|
||||||
|
const computedLabelWidth = ref('')
|
||||||
|
|
||||||
|
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)
|
||||||
|
const labelStyle = computed(() => {
|
||||||
|
if (elForm.labelPosition === 'top') return {}
|
||||||
|
const labelWidth = props.labelWidth || elForm.labelWidth
|
||||||
|
if (labelWidth) {
|
||||||
|
return {
|
||||||
|
width: labelWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
const contentStyle = computed(() => {
|
||||||
|
if (elForm.labelPosition === 'top' || elForm.inline) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
if (!props.label && !props.labelWidth && isNested.value) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const labelWidth = props.labelWidth || elForm.labelWidth
|
||||||
|
const ret: Partial<CSSStyleDeclaration> = {}
|
||||||
|
if (labelWidth === 'auto') {
|
||||||
|
if (props.labelWidth === 'auto') {
|
||||||
|
ret.marginLeft = computedLabelWidth.value
|
||||||
|
} else if (elForm.labelWidth === 'auto') {
|
||||||
|
ret.marginLeft = elForm.autoLabelWidth
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret.marginLeft = labelWidth
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
const fieldValue = computed(() => {
|
||||||
|
const model = elForm.model
|
||||||
|
if (!model || !props.prop) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = props.prop
|
||||||
|
if (path.indexOf(':') !== -1) {
|
||||||
|
path = path.replace(/:/, '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPropByPath(model, path, true).v
|
||||||
|
})
|
||||||
|
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(() => {
|
||||||
|
return elFormItemSize.value || $ELEMENT.size
|
||||||
|
})
|
||||||
|
|
||||||
|
const validate = (trigger: string, callback?: ValidateFieldCallback) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
const resetField = () => {
|
||||||
|
validateState.value = ''
|
||||||
|
validateMessage.value = ''
|
||||||
|
let model = elForm.model
|
||||||
|
let value = fieldValue.value
|
||||||
|
let path = props.prop
|
||||||
|
if (path.indexOf(':') !== -1) {
|
||||||
|
path = path.replace(/:/, '.')
|
||||||
|
}
|
||||||
|
let prop = getPropByPath(model, path, true)
|
||||||
|
validateDisabled.value = true
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRules = () => {
|
||||||
|
const formRules = elForm.rules
|
||||||
|
const selfRules = props.rules
|
||||||
|
const requiredRule =
|
||||||
|
props.required !== undefined ? { required: !!props.required } : []
|
||||||
|
|
||||||
|
const prop = getPropByPath(formRules, props.prop || '', true)
|
||||||
|
const normalizedRule = formRules
|
||||||
|
? (prop.o[props.prop || ''] || prop.v)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return [].concat(selfRules || normalizedRule || []).concat(requiredRule)
|
||||||
|
}
|
||||||
|
const getFilteredRule = trigger => {
|
||||||
|
const rules = getRules()
|
||||||
|
|
||||||
|
return rules
|
||||||
|
.filter(rule => {
|
||||||
|
if (!rule.trigger || trigger === '') return true
|
||||||
|
if (Array.isArray(rule.trigger)) {
|
||||||
|
return rule.trigger.indexOf(trigger) > -1
|
||||||
|
} else {
|
||||||
|
return rule.trigger === trigger
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(rule => ({ ...rule }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldBlur = () => {
|
||||||
|
validate('blur')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldChange = () => {
|
||||||
|
if (validateDisabled.value) {
|
||||||
|
validateDisabled.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate('change')
|
||||||
|
}
|
||||||
|
const updateComputedLabelWidth = width => {
|
||||||
|
computedLabelWidth.value = width ? `${width}px` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const addValidateEvents = () => {
|
||||||
|
const rules = getRules()
|
||||||
|
|
||||||
|
if (rules.length || props.required !== undefined) {
|
||||||
|
formItemMitt.on('el.form.blur', onFieldBlur)
|
||||||
|
formItemMitt.on('el.form.change', onFieldChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const removeValidateEvents = () => {
|
||||||
|
formItemMitt.off('el.form.blur', onFieldBlur)
|
||||||
|
formItemMitt.off('el.form.change', onFieldChange)
|
||||||
|
}
|
||||||
|
const elFormItem = reactive({
|
||||||
|
...toRefs(props),
|
||||||
|
removeValidateEvents,
|
||||||
|
addValidateEvents,
|
||||||
|
resetField,
|
||||||
|
clearValidate,
|
||||||
|
validate,
|
||||||
|
formItemMitt,
|
||||||
|
updateComputedLabelWidth,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.prop) {
|
||||||
|
elForm.formMitt.emit('el.form.addField', elFormItem)
|
||||||
|
|
||||||
|
let value = fieldValue.value
|
||||||
|
initialValue = Array.isArray(value)
|
||||||
|
? [...value] : value
|
||||||
|
|
||||||
|
addValidateEvents()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
elForm.formMitt.emit('el.form.removeField', elFormItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
provide(elFormItemKey, elFormItem)
|
||||||
|
|
||||||
|
const formItemClass = computed(() => [
|
||||||
|
{
|
||||||
|
'el-form-item--feedback': elForm && elForm.statusIcon,
|
||||||
|
'is-error': validateState.value === 'error',
|
||||||
|
'is-validating': validateState.value === 'validating',
|
||||||
|
'is-success': validateState.value === 'success',
|
||||||
|
'is-required': isRequired.value || props.required,
|
||||||
|
'is-no-asterisk': elForm && elForm.hideRequiredAsterisk,
|
||||||
|
},
|
||||||
|
sizeClass.value ? 'el-form-item--' + sizeClass.value : '',
|
||||||
|
])
|
||||||
|
|
||||||
|
const shouldShowError = computed(() => {
|
||||||
|
return validateState.value === 'error' && props.showMessage && elForm.showMessage
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
formItemClass,
|
||||||
|
shouldShowError,
|
||||||
|
elForm,
|
||||||
|
labelStyle,
|
||||||
|
contentStyle,
|
||||||
|
validateMessage,
|
||||||
|
labelFor,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
224
packages/form/src/form.vue
Normal file
224
packages/form/src/form.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="el-form"
|
||||||
|
:class="[
|
||||||
|
labelPosition ? 'el-form--label-' + labelPosition : '',
|
||||||
|
{ 'el-form--inline': inline }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent, provide, watch, ref,
|
||||||
|
computed, reactive, toRefs,
|
||||||
|
} from 'vue'
|
||||||
|
import mitt from 'mitt'
|
||||||
|
import {
|
||||||
|
elFormKey, ElFormItemContext as FormItemCtx,
|
||||||
|
elFormEvents, ValidateFieldCallback,
|
||||||
|
} from './token'
|
||||||
|
import { FieldErrorList } from 'async-validator'
|
||||||
|
|
||||||
|
function useFormLabelWidth() {
|
||||||
|
const potentialLabelWidthArr = ref([])
|
||||||
|
const autoLabelWidth = computed(() => {
|
||||||
|
if (!potentialLabelWidthArr.value.length) return '0'
|
||||||
|
const max = Math.max(...potentialLabelWidthArr.value)
|
||||||
|
return max ? `${max}px` : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function getLabelWidthIndex(width: number) {
|
||||||
|
const index = potentialLabelWidthArr.value.indexOf(width)
|
||||||
|
// it's impossible
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error('[ElementForm]unpected width ' + width)
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerLabelWidth(val: number, oldVal: number) {
|
||||||
|
if (val && oldVal) {
|
||||||
|
const index = getLabelWidthIndex(oldVal)
|
||||||
|
potentialLabelWidthArr.value.splice(index, 1, val)
|
||||||
|
} else if (val) {
|
||||||
|
potentialLabelWidthArr.value.push(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deregisterLabelWidth(val: number) {
|
||||||
|
const index = getLabelWidthIndex(val)
|
||||||
|
potentialLabelWidthArr.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
autoLabelWidth,
|
||||||
|
registerLabelWidth,
|
||||||
|
deregisterLabelWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
(isValid?: boolean, invalidFields?: FieldErrorList): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ElForm',
|
||||||
|
props: {
|
||||||
|
model: Object,
|
||||||
|
rules: Object,
|
||||||
|
labelPosition: String,
|
||||||
|
labelWidth: String,
|
||||||
|
labelSuffix: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
inline: Boolean,
|
||||||
|
inlineMessage: Boolean,
|
||||||
|
statusIcon: Boolean,
|
||||||
|
showMessage: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
size: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
validateOnRuleChange: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
hideRequiredAsterisk: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const formMitt = mitt()
|
||||||
|
|
||||||
|
const fields: FormItemCtx[] = []
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.rules,
|
||||||
|
() => {
|
||||||
|
fields.forEach(field => {
|
||||||
|
field.removeValidateEvents()
|
||||||
|
field.addValidateEvents()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (props.validateOnRuleChange) {
|
||||||
|
validate(() => ({}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
formMitt.on<FormItemCtx>(elFormEvents.addField, field => {
|
||||||
|
if (field) {
|
||||||
|
fields.push(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
formMitt.on<FormItemCtx>(elFormEvents.removeField, field => {
|
||||||
|
if (field.prop) {
|
||||||
|
fields.splice(fields.indexOf(field), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetFields = () => {
|
||||||
|
if (!props.model) {
|
||||||
|
console.warn(
|
||||||
|
'[Element Warn][Form]model is required for resetFields to work.',
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fields.forEach(field => {
|
||||||
|
field.resetField()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
fds.forEach(field => {
|
||||||
|
field.clearValidate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 no callback, return promise
|
||||||
|
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 = {}
|
||||||
|
for (const field of fields) {
|
||||||
|
field.validate('', (message, field) => {
|
||||||
|
if (message) {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
invalidFields = { ...invalidFields, ...field }
|
||||||
|
if (++count === fields.length) {
|
||||||
|
callback(valid, invalidFields)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateField = (props: string|string[], cb: ValidateFieldCallback) => {
|
||||||
|
props = [].concat(props)
|
||||||
|
const fds = fields.filter(field => props.indexOf(field.prop) !== -1)
|
||||||
|
if (!fields.length) {
|
||||||
|
console.warn('[Element Warn]please pass correct props!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fds.forEach(field => {
|
||||||
|
field.validate('', cb)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const elForm = reactive({
|
||||||
|
formMitt,
|
||||||
|
...toRefs(props),
|
||||||
|
resetFields,
|
||||||
|
clearValidate,
|
||||||
|
validateField,
|
||||||
|
emit,
|
||||||
|
...useFormLabelWidth(),
|
||||||
|
})
|
||||||
|
|
||||||
|
provide(elFormKey, elForm)
|
||||||
|
|
||||||
|
return {
|
||||||
|
validate, // export
|
||||||
|
resetFields,
|
||||||
|
clearValidate,
|
||||||
|
validateField,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
88
packages/form/src/label-wrap.ts
Normal file
88
packages/form/src/label-wrap.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
h,
|
||||||
|
inject,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
onUpdated,
|
||||||
|
onBeforeUnmount,
|
||||||
|
nextTick,
|
||||||
|
} from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
elFormKey, elFormItemKey,
|
||||||
|
} from './token'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
isAutoWidth: Boolean,
|
||||||
|
updateAll: Boolean,
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
const el = ref<Nullable<HTMLElement>>(null)
|
||||||
|
const elForm = inject(elFormKey)
|
||||||
|
const elFormItem = inject(elFormItemKey)
|
||||||
|
|
||||||
|
const computedWidth = ref(0)
|
||||||
|
watch(computedWidth, (val, oldVal) => {
|
||||||
|
if (props.updateAll) {
|
||||||
|
elForm.registerLabelWidth(val, oldVal)
|
||||||
|
elFormItem.updateComputedLabelWidth(val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getLabelWidth = () => {
|
||||||
|
if (el.value?.firstElementChild) {
|
||||||
|
const width = window.getComputedStyle(el.value.firstElementChild)
|
||||||
|
.width
|
||||||
|
return Math.ceil(parseFloat(width))
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateLabelWidth = (action = 'update') => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (slots.default && props.isAutoWidth) {
|
||||||
|
if (action === 'update') {
|
||||||
|
computedWidth.value = getLabelWidth()
|
||||||
|
} else if (action === 'remove') {
|
||||||
|
elForm.deregisterLabelWidth(computedWidth.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => updateLabelWidth('update'))
|
||||||
|
|
||||||
|
onUpdated(() => updateLabelWidth('update'))
|
||||||
|
|
||||||
|
onBeforeUnmount(() => updateLabelWidth('remove'))
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!slots) return null
|
||||||
|
if (props.isAutoWidth) {
|
||||||
|
const autoLabelWidth = elForm.autoLabelWidth
|
||||||
|
const style = {} as CSSStyleDeclaration
|
||||||
|
if (autoLabelWidth && autoLabelWidth !== 'auto') {
|
||||||
|
const marginLeft = parseInt(autoLabelWidth, 10) - computedWidth.value
|
||||||
|
if (marginLeft) {
|
||||||
|
style.marginLeft = marginLeft + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
ref: el,
|
||||||
|
class: ['el-form-item__label-wrap'],
|
||||||
|
style,
|
||||||
|
},
|
||||||
|
slots.default?.(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return h('div', { ref: el }, slots.default?.())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return render
|
||||||
|
},
|
||||||
|
})
|
48
packages/form/src/token.ts
Normal file
48
packages/form/src/token.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { InjectionKey } from 'vue'
|
||||||
|
import type { Emitter } from 'mitt'
|
||||||
|
import type {
|
||||||
|
FieldErrorList,
|
||||||
|
} from 'async-validator'
|
||||||
|
|
||||||
|
export interface ElFormContext {
|
||||||
|
registerLabelWidth(width: number, oldWidth: number): void
|
||||||
|
deregisterLabelWidth(width: number): void
|
||||||
|
autoLabelWidth: string | undefined
|
||||||
|
formMitt: Emitter
|
||||||
|
emit: (evt: string, ...args: any[]) => void
|
||||||
|
|
||||||
|
labelSuffix: string
|
||||||
|
inline?: boolean
|
||||||
|
model?: Record<string, unknown>
|
||||||
|
size?: string
|
||||||
|
showMessage?: boolean
|
||||||
|
labelPosition?: string
|
||||||
|
labelWidth?: string
|
||||||
|
rules?: Record<string, unknown>
|
||||||
|
statusIcon?: boolean
|
||||||
|
hideRequiredAsterisk?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidateFieldCallback {
|
||||||
|
(message?: string, invalidFields?: FieldErrorList): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElFormItemContext {
|
||||||
|
prop?: string
|
||||||
|
validate(trigger?: string, callback?: ValidateFieldCallback): void
|
||||||
|
updateComputedLabelWidth(width: number): void
|
||||||
|
addValidateEvents(): void
|
||||||
|
removeValidateEvents(): void
|
||||||
|
resetField(): void
|
||||||
|
clearValidate(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change it to symbol
|
||||||
|
export const elFormKey: InjectionKey<ElFormContext> = 'elForm' as any
|
||||||
|
|
||||||
|
export const elFormItemKey: InjectionKey<ElFormItemContext> = 'elFormItem' as any
|
||||||
|
|
||||||
|
export const elFormEvents = {
|
||||||
|
addField: 'el.form.addField',
|
||||||
|
removeField: 'el.form.removeField',
|
||||||
|
} as const
|
13
packages/test-utils/style-plugin.ts
Normal file
13
packages/test-utils/style-plugin.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { config } from '@vue/test-utils'
|
||||||
|
|
||||||
|
const stylePlugin = (wrapper: any) => {
|
||||||
|
return {
|
||||||
|
style: wrapper.element.style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function install() {
|
||||||
|
config.plugins.DOMWrapper.install(stylePlugin)
|
||||||
|
config.plugins.VueWrapper.install(stylePlugin)
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
|||||||
import isServer from './isServer'
|
import isServer from './isServer'
|
||||||
import type { AnyFunction } from './types'
|
import type { AnyFunction } from './types'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
export type PartialCSSStyleDeclaration = Partial<
|
export type PartialCSSStyleDeclaration = Partial<
|
||||||
Pick<CSSStyleDeclaration, 'transform' | 'transition' | 'animation'>
|
Pick<CSSStyleDeclaration, 'transform' | 'transition' | 'animation'>
|
||||||
@ -171,3 +172,11 @@ export function isUndefined(val: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { isVNode } from 'vue'
|
export { isVNode } from 'vue'
|
||||||
|
|
||||||
|
export function useGlobalConfig() {
|
||||||
|
const vm: any = getCurrentInstance()
|
||||||
|
if ('$ELEMENT' in vm.proxy) {
|
||||||
|
return vm.proxy.$ELEMENT
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
11
typings/vue-test-utils.d.ts
vendored
Normal file
11
typings/vue-test-utils.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ComponentPublicInstance } from '@vue/test-utils'
|
||||||
|
|
||||||
|
declare module '@vue/test-utils' {
|
||||||
|
interface DOMWrapper<ElementType> {
|
||||||
|
style: CSSStyleDeclaration
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VueWrapper<T extends ComponentPublicInstance> {
|
||||||
|
style: CSSStyleDeclaration
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,6 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3250,6 +3250,11 @@ async-settle@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
async-done "^1.2.2"
|
async-done "^1.2.2"
|
||||||
|
|
||||||
|
async-validator@^3.4.0:
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-3.4.0.tgz#871b3e594124bf4c4eb7bcd1a9e78b44f3b09cae"
|
||||||
|
integrity sha512-VrFk4eYiJAWKskEz115iiuCf9O0ftnMMPXrOFMqyzGH2KxO7YwncKyn/FgOOP+0MDHMfXL7gLExagCutaZGigA==
|
||||||
|
|
||||||
async@^2.6.2:
|
async@^2.6.2:
|
||||||
version "2.6.3"
|
version "2.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||||
|
Loading…
Reference in New Issue
Block a user