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 ElInfiniteScroll from '@element-plus/infinite-scroll'
|
||||
import ElDrawer from '@element-plus/drawer'
|
||||
import ElForm from '@element-plus/form'
|
||||
import ElUpload from '@element-plus/upload'
|
||||
import ElTree from '@element-plus/tree'
|
||||
|
||||
@ -78,6 +79,7 @@ export {
|
||||
ElCalendar,
|
||||
ElInfiniteScroll,
|
||||
ElDrawer,
|
||||
ElForm,
|
||||
ElUpload,
|
||||
ElTree,
|
||||
}
|
||||
|
@ -38,6 +38,7 @@
|
||||
"@element-plus/collapse": "^0.0.0",
|
||||
"@element-plus/time-picker": "^0.0.0",
|
||||
"@element-plus/tabs": "^0.0.0",
|
||||
"@element-plus/form": "^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 type { AnyFunction } from './types'
|
||||
import type { Ref } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
export type PartialCSSStyleDeclaration = Partial<
|
||||
Pick<CSSStyleDeclaration, 'transform' | 'transition' | 'animation'>
|
||||
@ -171,3 +172,11 @@ export function isUndefined(val: any) {
|
||||
}
|
||||
|
||||
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 {
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -3250,6 +3250,11 @@ async-settle@^1.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
|
Loading…
Reference in New Issue
Block a user