mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-29 17:58:08 +08:00
refactor(components)!: refactor form (#5401)
* refactor(components): refactor form * refactor: resolve PR comments * refactor(components): refactor isNested * refactor: resolve PR comments
This commit is contained in:
parent
174f328e54
commit
c72679e4e9
@ -63,6 +63,9 @@ module.exports = defineConfig({
|
||||
'block-scoped-var': 'error',
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="form" label-width="120px">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Activity name">
|
||||
<el-input v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
|
@ -35,9 +35,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import type { ElForm } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
|
||||
const checkAge = (rule: any, value: any, callback: any) => {
|
||||
|
@ -49,9 +49,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { ElForm } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const formRef = ref<FormInstance>()
|
||||
const dynamicValidateForm = reactive<{
|
||||
domains: DomainItem[]
|
||||
|
@ -28,9 +28,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { ElForm } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const numberValidateForm = reactive({
|
||||
|
@ -71,14 +71,12 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { ElForm } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
|
||||
const formSize = ref('')
|
||||
const formSize = ref('default')
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
const ruleForm = reactive({
|
||||
name: '',
|
||||
name: 'Hello',
|
||||
region: '',
|
||||
date1: '',
|
||||
date2: '',
|
||||
@ -90,17 +88,8 @@ const ruleForm = reactive({
|
||||
|
||||
const rules = reactive({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input Activity name',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
min: 3,
|
||||
max: 5,
|
||||
message: 'Length should be 3 to 5',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
|
||||
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
|
||||
],
|
||||
region: [
|
||||
{
|
||||
@ -141,22 +130,17 @@ const rules = reactive({
|
||||
},
|
||||
],
|
||||
desc: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input activity form',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{ required: true, message: 'Please input activity form', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
console.log('submit!')
|
||||
} else {
|
||||
console.log('error submit!')
|
||||
return false
|
||||
console.log('error submit!', fields)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -9,7 +9,10 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"element-plus": ["../packages/element-plus"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*", ".vitepress/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
@ -180,7 +180,7 @@ import ElScrollbar from '@element-plus/components/scrollbar'
|
||||
import ElTag from '@element-plus/components/tag'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
|
||||
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
||||
import { ClickOutside as Clickoutside } from '@element-plus/directives'
|
||||
import { useLocale, useSize } from '@element-plus/hooks'
|
||||
|
||||
@ -191,6 +191,7 @@ import {
|
||||
removeResizeListener,
|
||||
isValidComponentSize,
|
||||
isKorean,
|
||||
debugWarn,
|
||||
} from '@element-plus/utils'
|
||||
import {
|
||||
EVENT_CODE,
|
||||
@ -201,7 +202,7 @@ import { CircleClose, Check, ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
import type { Options } from '@element-plus/components/popper'
|
||||
import type { ComputedRef, PropType, Ref } from 'vue'
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormContext, FormItemContext } from '@element-plus/tokens'
|
||||
import type {
|
||||
CascaderValue,
|
||||
CascaderNode,
|
||||
@ -323,8 +324,8 @@ export default defineComponent({
|
||||
'popperAppendToBody'
|
||||
)
|
||||
const { t } = useLocale()
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
|
||||
const tooltipRef: Ref<tooltipType | null> = ref(null)
|
||||
const input: Ref<inputType | null> = ref(null)
|
||||
@ -384,7 +385,7 @@ export default defineComponent({
|
||||
set(val) {
|
||||
emit(UPDATE_MODEL_EVENT, val)
|
||||
emit(CHANGE_EVENT, val)
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
renderSlot,
|
||||
} from 'vue'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { isValidComponentSize } from '@element-plus/utils'
|
||||
import { debugWarn, isValidComponentSize } from '@element-plus/utils'
|
||||
import { useSize, useNamespace } from '@element-plus/hooks'
|
||||
import { useCheckboxGroup } from './useCheckbox'
|
||||
|
||||
@ -86,7 +86,7 @@ export default defineComponent({
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
)
|
||||
return () => {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ref, computed, inject, getCurrentInstance, watch } from 'vue'
|
||||
import { toTypeString } from '@vue/shared'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
|
||||
|
||||
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
||||
import { useSize } from '@element-plus/hooks'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormContext, FormItemContext } from '@element-plus/tokens'
|
||||
import type { ICheckboxGroupInstance } from './checkbox.type'
|
||||
|
||||
export const useCheckboxProps = {
|
||||
@ -38,8 +38,8 @@ export const useCheckboxProps = {
|
||||
export type IUseCheckboxProps = ExtractPropTypes<typeof useCheckboxProps>
|
||||
|
||||
export const useCheckboxGroup = () => {
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
const checkboxGroup = inject<ICheckboxGroupInstance>('CheckboxGroup', {})
|
||||
const isGroup = computed(
|
||||
() => checkboxGroup && checkboxGroup?.name === 'ElCheckboxGroup'
|
||||
@ -186,7 +186,7 @@ const useEvent = (
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -108,12 +108,12 @@ import { debounce } from 'lodash-unified'
|
||||
import ElButton from '@element-plus/components/button'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import { ClickOutside } from '@element-plus/directives'
|
||||
import { elFormItemKey, elFormKey } from '@element-plus/tokens'
|
||||
import { formItemContextKey, formContextKey } from '@element-plus/tokens'
|
||||
import { useLocale, useSize, useNamespace } from '@element-plus/hooks'
|
||||
import ElTooltip from '@element-plus/components/tooltip'
|
||||
import ElInput from '@element-plus/components/input'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { isValidComponentSize } from '@element-plus/utils'
|
||||
import { debugWarn, isValidComponentSize } from '@element-plus/utils'
|
||||
import { Close, ArrowDown } from '@element-plus/icons-vue'
|
||||
import AlphaSlider from './components/alpha-slider.vue'
|
||||
import HueSlider from './components/hue-slider.vue'
|
||||
@ -121,9 +121,8 @@ import Predefine from './components/predefine.vue'
|
||||
import SvPanel from './components/sv-panel.vue'
|
||||
import Color from './color'
|
||||
import { OPTIONS_KEY } from './useOption'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormContext, FormItemContext } from '@element-plus/tokens'
|
||||
import type { ComponentSize } from '@element-plus/constants'
|
||||
import type { IUseOptions } from './useOption'
|
||||
|
||||
@ -160,8 +159,8 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('color')
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
|
||||
const hue = ref(null)
|
||||
const svPanel = ref(null)
|
||||
@ -267,7 +266,7 @@ export default defineComponent({
|
||||
const value = color.value
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
emit('change', value)
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
debounceSetShowPicker(false)
|
||||
// check if modelValue change, if not change, then reset color.
|
||||
nextTick(() => {
|
||||
@ -287,7 +286,7 @@ export default defineComponent({
|
||||
emit(UPDATE_MODEL_EVENT, null)
|
||||
emit('change', null)
|
||||
if (props.modelValue !== null) {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
resetColor()
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
478
packages/components/form/__tests__/form.spec.tsx
Normal file
478
packages/components/form/__tests__/form.spec.tsx
Normal file
@ -0,0 +1,478 @@
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import installStyle from '@element-plus/test-utils/style-plugin'
|
||||
import Checkbox from '@element-plus/components/checkbox/src/checkbox.vue'
|
||||
import CheckboxGroup from '@element-plus/components/checkbox/src/checkbox-group.vue'
|
||||
import Input from '@element-plus/components/input'
|
||||
import Form from '../src/form.vue'
|
||||
import FormItem from '../src/form-item.vue'
|
||||
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import type { FormInstance } from '../src/form'
|
||||
import type { FormRules } from '../src/types'
|
||||
import type { FormItemInstance } from '../src/form-item'
|
||||
|
||||
const findStyle = (wrapper: VueWrapper<any>, selector: string) =>
|
||||
wrapper.find<HTMLElement>(selector).element.style
|
||||
|
||||
;(globalThis as any).ASYNC_VALIDATOR_NO_WARNING = 1
|
||||
|
||||
describe('Form', () => {
|
||||
beforeAll(() => {
|
||||
installStyle()
|
||||
})
|
||||
|
||||
test('label width', async () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
})
|
||||
return () => (
|
||||
<Form ref="form" model={form} labelWidth="80px">
|
||||
<FormItem label="Activity Name">
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
expect(findStyle(wrapper, '.el-form-item__label').width).toBe('80px')
|
||||
})
|
||||
|
||||
test('auto label width', async () => {
|
||||
const labelPosition = ref('right')
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
intro: '',
|
||||
})
|
||||
return () => (
|
||||
<Form
|
||||
ref="form"
|
||||
model={form}
|
||||
labelWidth="auto"
|
||||
labelPosition={labelPosition.value}
|
||||
>
|
||||
<FormItem label="Name">
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem label="Intro">
|
||||
<Input v-model={form.intro} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const formItems = wrapper.findAll<HTMLElement>('.el-form-item__content')
|
||||
const marginLeft = parseInt(formItems[0].element.style.marginLeft, 10)
|
||||
const marginLeft1 = parseInt(formItems[1].element.style.marginLeft, 10)
|
||||
expect(marginLeft).toEqual(marginLeft1)
|
||||
|
||||
labelPosition.value = 'left'
|
||||
await nextTick()
|
||||
|
||||
const formItems1 = wrapper.findAll<HTMLElement>('.el-form-item__content')
|
||||
const marginRight = parseInt(formItems1[0].element.style.marginRight, 10)
|
||||
const marginRight1 = parseInt(formItems1[1].element.style.marginRight, 10)
|
||||
expect(marginRight).toEqual(marginRight1)
|
||||
})
|
||||
|
||||
test('form-item auto label width', async () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
region: '',
|
||||
type: '',
|
||||
})
|
||||
return () => (
|
||||
<Form
|
||||
ref="form"
|
||||
labelPosition="right"
|
||||
labelWidth="150px"
|
||||
model={form}
|
||||
>
|
||||
<FormItem label="名称">
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem label="活动区域" label-width="auto">
|
||||
<Input v-model={form.region} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="活动形式(我是一个很长很长很长很长的label)"
|
||||
label-width="auto"
|
||||
>
|
||||
<Input v-model={form.type} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const formItemLabels = wrapper.findAll<HTMLElement>('.el-form-item__label')
|
||||
const formItemLabelWraps = wrapper.findAll<HTMLElement>(
|
||||
'.el-form-item__label-wrap'
|
||||
)
|
||||
|
||||
const labelWrapMarginLeft1 = formItemLabelWraps[0].element.style.marginLeft
|
||||
const labelWrapMarginLeft2 = formItemLabelWraps[1].element.style.marginLeft
|
||||
expect(labelWrapMarginLeft1).toEqual(labelWrapMarginLeft2)
|
||||
expect(labelWrapMarginLeft2).toEqual('')
|
||||
|
||||
const labelWidth0 = parseInt(formItemLabels[0].element.style.width, 10)
|
||||
expect(labelWidth0).toEqual(150)
|
||||
const labelWidth1 = formItemLabels[1].element.style.width
|
||||
const labelWidth2 = formItemLabels[2].element.style.width
|
||||
expect(labelWidth1).toEqual(labelWidth2)
|
||||
expect(labelWidth2).toEqual('auto')
|
||||
})
|
||||
|
||||
test('inline form', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
address: '',
|
||||
})
|
||||
return () => (
|
||||
<Form ref="form" model={form} inline>
|
||||
<FormItem>
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Input v-model={form.address} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
expect(wrapper.classes()).toContain('el-form--inline')
|
||||
})
|
||||
|
||||
test('label position', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
address: '',
|
||||
})
|
||||
return () => (
|
||||
<div>
|
||||
<Form model={form} labelPosition="top" ref="labelTop">
|
||||
<FormItem>
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Input v-model={form.address} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<Form model={form} labelPosition="left" ref="labelLeft">
|
||||
<FormItem>
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Input v-model={form.address} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
expect(wrapper.findComponent({ ref: 'labelTop' }).classes()).toContain(
|
||||
'el-form--label-top'
|
||||
)
|
||||
expect(wrapper.findComponent({ ref: 'labelLeft' }).classes()).toContain(
|
||||
'el-form--label-left'
|
||||
)
|
||||
})
|
||||
|
||||
test('label size', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
})
|
||||
return () => (
|
||||
<div>
|
||||
<div>
|
||||
<Form model={form} size="small" ref="labelSmall">
|
||||
<FormItem>
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
expect(wrapper.findComponent(FormItem).classes()).toContain(
|
||||
'el-form-item--small'
|
||||
)
|
||||
})
|
||||
|
||||
test('show message', (done) => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
})
|
||||
return () => (
|
||||
<Form model={form} ref="form">
|
||||
<FormItem
|
||||
label="Name"
|
||||
prop="name"
|
||||
showMessage={false}
|
||||
rules={{
|
||||
required: true,
|
||||
message: 'Please input name',
|
||||
trigger: 'change',
|
||||
min: 3,
|
||||
max: 6,
|
||||
}}
|
||||
>
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
const form = wrapper.findComponent(Form).vm as FormInstance
|
||||
form.validate(async (valid) => {
|
||||
expect(valid).toBe(false)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.el-form-item__error').exists()).toBe(false)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test('reset field', async () => {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
address: '',
|
||||
type: Array<string>(),
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: 'Please input name', trigger: 'blur' },
|
||||
],
|
||||
address: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input address',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
type: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: 'Please input type',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
}
|
||||
return () => (
|
||||
<Form ref="form" model={form} rules={rules}>
|
||||
<FormItem label="name" prop="name">
|
||||
<Input v-model={form.name} ref="fieldName" />
|
||||
</FormItem>
|
||||
<FormItem label="address" prop="address">
|
||||
<Input v-model={form.address} ref="fieldAddr" />
|
||||
</FormItem>
|
||||
<FormItem label="type" prop="type">
|
||||
<CheckboxGroup v-model={form.type}>
|
||||
<Checkbox label="type1" name="type" />
|
||||
<Checkbox label="type2" name="type" />
|
||||
<Checkbox label="type3" name="type" />
|
||||
<Checkbox label="type4" name="type" />
|
||||
</CheckboxGroup>
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
form.name = 'jack'
|
||||
form.address = 'aaaa'
|
||||
form.type.push('type1')
|
||||
|
||||
const formRef = wrapper.findComponent({ ref: 'form' }).vm as FormInstance
|
||||
formRef.resetFields()
|
||||
await nextTick()
|
||||
expect(form.name).toBe('')
|
||||
expect(form.address).toBe('')
|
||||
expect(form.type.length).toBe(0)
|
||||
})
|
||||
|
||||
test('clear validate', async () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const form = reactive({
|
||||
name: '',
|
||||
address: '',
|
||||
type: [],
|
||||
})
|
||||
|
||||
const rules: FormRules = reactive({
|
||||
name: [
|
||||
{ required: true, message: 'Please input name', trigger: 'blur' },
|
||||
],
|
||||
address: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input address',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
type: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: 'Please input type',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return () => (
|
||||
<Form ref="form" model={form} rules={rules}>
|
||||
<FormItem label="name" prop="name" ref="name">
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem label="address" prop="address" ref="address">
|
||||
<Input v-model={form.address} />
|
||||
</FormItem>
|
||||
<FormItem label="type" prop="type">
|
||||
<CheckboxGroup v-model={form.type}>
|
||||
<Checkbox label="type1" name="type" />
|
||||
<Checkbox label="type2" name="type" />
|
||||
<Checkbox label="type3" name="type" />
|
||||
<Checkbox label="type4" name="type" />
|
||||
</CheckboxGroup>
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const form = wrapper.findComponent({ ref: 'form' }).vm as FormInstance
|
||||
const nameField = wrapper.findComponent({ ref: 'name' })
|
||||
.vm as FormItemInstance
|
||||
const addressField = wrapper.findComponent({ ref: 'address' })
|
||||
.vm as FormItemInstance
|
||||
await form.validate().catch(() => undefined)
|
||||
await nextTick()
|
||||
expect(nameField.validateMessage).toBe('Please input name')
|
||||
expect(addressField.validateMessage).toBe('Please input address')
|
||||
form.clearValidate(['name'])
|
||||
await nextTick()
|
||||
expect(nameField.validateMessage).toBe('')
|
||||
expect(addressField.validateMessage).toBe('Please input address')
|
||||
form.clearValidate()
|
||||
await nextTick()
|
||||
expect(addressField.validateMessage).toBe('')
|
||||
})
|
||||
|
||||
test('scroll to field', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => (
|
||||
<div>
|
||||
<Form ref="form">
|
||||
<FormItem prop="name" ref="formItem">
|
||||
<Input />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const oldScrollIntoView = window.HTMLElement.prototype.scrollIntoView
|
||||
|
||||
const scrollIntoViewMock = jest.fn()
|
||||
window.HTMLElement.prototype.scrollIntoView = function () {
|
||||
scrollIntoViewMock(this)
|
||||
}
|
||||
|
||||
const form = wrapper.findComponent({ ref: 'form' }).vm as FormInstance
|
||||
form.scrollToField('name')
|
||||
expect(scrollIntoViewMock).toHaveBeenCalledWith(
|
||||
wrapper.findComponent({ ref: 'formItem' }).element
|
||||
)
|
||||
|
||||
window.HTMLElement.prototype.scrollIntoView = oldScrollIntoView
|
||||
})
|
||||
|
||||
test('validate return parameters', async () => {
|
||||
const form = reactive({
|
||||
name: 'test',
|
||||
age: '',
|
||||
})
|
||||
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
const rules = reactive({
|
||||
name: [
|
||||
{ required: true, message: 'Please input name', trigger: 'blur' },
|
||||
],
|
||||
age: [
|
||||
{ required: true, message: 'Please input age', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
return () => (
|
||||
<Form
|
||||
ref="formRef"
|
||||
model={form}
|
||||
rules={rules}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
onSubmit="return false"
|
||||
>
|
||||
<FormItem prop="name" label="name">
|
||||
<Input v-model={form.name} />
|
||||
</FormItem>
|
||||
<FormItem prop="age" label="age">
|
||||
<Input v-model={form.age} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
|
||||
function validate() {
|
||||
return (vm.$refs.formRef as FormInstance)
|
||||
.validate()
|
||||
.then(() => ({ valid: true, fields: undefined }))
|
||||
.catch((fields) => ({ valid: false, fields }))
|
||||
}
|
||||
|
||||
let res = await validate()
|
||||
expect(res.valid).toBe(false)
|
||||
expect(Object.keys(res.fields).length).toBe(1)
|
||||
form.name = ''
|
||||
await nextTick()
|
||||
|
||||
res = await validate()
|
||||
expect(res.valid).toBe(false)
|
||||
expect(Object.keys(res.fields).length).toBe(2)
|
||||
|
||||
form.name = 'test'
|
||||
form.age = 'age'
|
||||
await nextTick()
|
||||
res = await validate()
|
||||
expect(res.valid).toBe(true)
|
||||
expect(res.fields).toBe(undefined)
|
||||
})
|
||||
})
|
@ -5,7 +5,9 @@ import FormItem from './src/form-item.vue'
|
||||
export const ElForm = withInstall(Form, {
|
||||
FormItem,
|
||||
})
|
||||
|
||||
export default ElForm
|
||||
|
||||
export const ElFormItem = withNoopInstall(FormItem)
|
||||
|
||||
export * from './src/form'
|
||||
export * from './src/form-item'
|
||||
export * from './src/types'
|
||||
|
54
packages/components/form/src/form-item.ts
Normal file
54
packages/components/form/src/form-item.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { componentSizes } from '@element-plus/constants'
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
import type FormItem from './form-item.vue'
|
||||
import type { FormItemRule } from './types'
|
||||
|
||||
export const formItemValidateStates = [
|
||||
'',
|
||||
'error',
|
||||
'validating',
|
||||
'success',
|
||||
] as const
|
||||
export type FormItemValidateState = typeof formItemValidateStates[number]
|
||||
|
||||
export const formItemProps = buildProps({
|
||||
label: String,
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
prop: {
|
||||
type: definePropType<Arrayable<string>>([String, Array]),
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
rules: {
|
||||
type: definePropType<Arrayable<FormItemRule>>([Object, Array]),
|
||||
},
|
||||
error: String,
|
||||
validateStatus: {
|
||||
type: String,
|
||||
values: formItemValidateStates,
|
||||
},
|
||||
for: String,
|
||||
inlineMessage: {
|
||||
type: [String, Boolean],
|
||||
default: '',
|
||||
},
|
||||
showMessage: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
values: componentSizes,
|
||||
},
|
||||
} as const)
|
||||
export type FormItemProps = ExtractPropTypes<typeof formItemProps>
|
||||
|
||||
export type FormItemInstance = InstanceType<typeof FormItem>
|
@ -1,33 +1,26 @@
|
||||
<template>
|
||||
<div ref="formItemRef" class="el-form-item" :class="formItemClass">
|
||||
<LabelWrap
|
||||
<div ref="formItemRef" :class="formItemClasses">
|
||||
<form-label-wrap
|
||||
:is-auto-width="labelStyle.width === 'auto'"
|
||||
:update-all="elForm.labelWidth === 'auto'"
|
||||
:update-all="formContext.labelWidth === 'auto'"
|
||||
>
|
||||
<label
|
||||
v-if="label || $slots.label"
|
||||
:for="labelFor"
|
||||
class="el-form-item__label"
|
||||
:class="ns.e('label')"
|
||||
:style="labelStyle"
|
||||
>
|
||||
<slot name="label" :label="currentLabel">
|
||||
{{ currentLabel }}
|
||||
</slot>
|
||||
</label>
|
||||
</LabelWrap>
|
||||
<div class="el-form-item__content" :style="contentStyle">
|
||||
</form-label-wrap>
|
||||
|
||||
<div :class="ns.e('content')" :style="contentStyle">
|
||||
<slot></slot>
|
||||
<transition name="el-zoom-in-top">
|
||||
<transition :name="`${ns.namespace.value}-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,
|
||||
}"
|
||||
>
|
||||
<div :class="validateClasses">
|
||||
{{ validateMessage }}
|
||||
</div>
|
||||
</slot>
|
||||
@ -36,11 +29,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
@ -50,313 +41,284 @@ import {
|
||||
toRefs,
|
||||
watch,
|
||||
nextTick,
|
||||
useSlots,
|
||||
} from 'vue'
|
||||
import { NOOP } from '@vue/shared'
|
||||
import AsyncValidator from 'async-validator'
|
||||
import { clone } from 'lodash-unified'
|
||||
import {
|
||||
addUnit,
|
||||
isValidComponentSize,
|
||||
getPropByPath,
|
||||
ensureArray,
|
||||
getProp,
|
||||
isString,
|
||||
isBoolean,
|
||||
throwError,
|
||||
} from '@element-plus/utils'
|
||||
import { elFormItemKey, elFormKey } from '@element-plus/tokens'
|
||||
import { useSize } from '@element-plus/hooks'
|
||||
import LabelWrap from './label-wrap'
|
||||
import { formItemContextKey, formContextKey } from '@element-plus/tokens'
|
||||
import { useSize, useNamespace } from '@element-plus/hooks'
|
||||
import { formItemProps } from './form-item'
|
||||
import FormLabelWrap from './form-label-wrap'
|
||||
|
||||
import type { PropType, CSSProperties } from 'vue'
|
||||
import type { ComponentSize } from '@element-plus/constants'
|
||||
import type { ElFormContext, ValidateFieldCallback } from '@element-plus/tokens'
|
||||
import type { FormItemRule } from './form.type'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type {
|
||||
RuleItem,
|
||||
ValidateError,
|
||||
ValidateFieldsError,
|
||||
} from 'async-validator'
|
||||
import type { FormItemContext } from '@element-plus/tokens'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
import type { FormItemValidateState } from './form-item'
|
||||
import type { FormItemRule } from './types'
|
||||
|
||||
export default defineComponent({
|
||||
const COMPONENT_NAME = 'ElFormItem'
|
||||
defineOptions({
|
||||
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 elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const validateState = ref('')
|
||||
const validateMessage = ref('')
|
||||
const isValidationEnabled = ref(false)
|
||||
})
|
||||
const props = defineProps(formItemProps)
|
||||
const slots = useSlots()
|
||||
|
||||
const computedLabelWidth = ref('')
|
||||
const formContext = inject(formContextKey)
|
||||
if (!formContext)
|
||||
throwError(COMPONENT_NAME, 'usage: <el-form><el-form-item /></el-form>')
|
||||
const parentFormItemContext = inject(formItemContextKey, undefined)
|
||||
|
||||
const formItemRef = ref<HTMLDivElement>()
|
||||
const _size = useSize(undefined, { formItem: false })
|
||||
const ns = useNamespace('form-item')
|
||||
|
||||
const vm = getCurrentInstance()
|
||||
const isNested = computed(() => {
|
||||
let parent = vm.parent
|
||||
while (parent && parent.type.name !== 'ElForm') {
|
||||
if (parent.type.name === 'ElFormItem') {
|
||||
return true
|
||||
const validateState = ref<FormItemValidateState>('')
|
||||
const validateMessage = ref('')
|
||||
const formItemRef = ref<HTMLDivElement>()
|
||||
let initialValue: any = undefined
|
||||
|
||||
const labelStyle = computed<CSSProperties>(() => {
|
||||
if (formContext.labelPosition === 'top') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const labelWidth = addUnit(props.labelWidth || formContext.labelWidth || '')
|
||||
if (labelWidth) return { width: labelWidth }
|
||||
return {}
|
||||
})
|
||||
|
||||
const contentStyle = computed<CSSProperties>(() => {
|
||||
if (formContext.labelPosition === 'top' || formContext.inline) {
|
||||
return {}
|
||||
}
|
||||
if (!props.label && !props.labelWidth && isNested) {
|
||||
return {}
|
||||
}
|
||||
const labelWidth = addUnit(props.labelWidth || formContext.labelWidth || '')
|
||||
if (!props.label && !slots.label) {
|
||||
return { marginLeft: labelWidth }
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const formItemClasses = computed(() => [
|
||||
ns.b(),
|
||||
ns.m(_size.value),
|
||||
ns.is('error', validateState.value === 'error'),
|
||||
ns.is('validating', validateState.value === 'validating'),
|
||||
ns.is('success', validateState.value === 'success'),
|
||||
ns.is('required', isRequired.value || props.required),
|
||||
ns.is('no-asterisk', formContext.hideRequiredAsterisk),
|
||||
{ [ns.m('feedback')]: formContext.statusIcon },
|
||||
])
|
||||
|
||||
const _inlineMessage = computed(() =>
|
||||
isBoolean(props.inlineMessage)
|
||||
? props.inlineMessage
|
||||
: formContext.inlineMessage || false
|
||||
)
|
||||
|
||||
const validateClasses = computed(() => [
|
||||
ns.e('error'),
|
||||
{ [ns.em('error', 'inline')]: _inlineMessage.value },
|
||||
])
|
||||
|
||||
const propString = computed(() => {
|
||||
if (!props.prop) return ''
|
||||
return isString(props.prop) ? props.prop : props.prop.join('.')
|
||||
})
|
||||
|
||||
const labelFor = computed(() => props.for || propString.value)
|
||||
|
||||
const isNested = !!parentFormItemContext
|
||||
|
||||
const fieldValue = computed(() => {
|
||||
const model = formContext.model
|
||||
if (!model || !props.prop) {
|
||||
return
|
||||
}
|
||||
return getProp(model, props.prop).value
|
||||
})
|
||||
|
||||
const _rules = computed(() => {
|
||||
const rules: FormItemRule[] = props.rules ? ensureArray(props.rules) : []
|
||||
|
||||
const formRules = formContext.rules
|
||||
if (formRules && props.prop) {
|
||||
const _rules = getProp<Arrayable<FormItemRule> | undefined>(
|
||||
formRules,
|
||||
props.prop
|
||||
).value
|
||||
if (_rules) {
|
||||
rules.push(...ensureArray(_rules))
|
||||
}
|
||||
}
|
||||
|
||||
if (props.required !== undefined) {
|
||||
rules.push({ required: !!props.required })
|
||||
}
|
||||
|
||||
return rules
|
||||
})
|
||||
|
||||
const validateEnabled = computed(() => _rules.value.length > 0)
|
||||
|
||||
const getFilteredRule = (trigger: string) => {
|
||||
const rules = _rules.value
|
||||
return (
|
||||
rules
|
||||
.filter((rule) => {
|
||||
if (!rule.trigger || !trigger) return true
|
||||
if (Array.isArray(rule.trigger)) {
|
||||
return rule.trigger.includes(trigger)
|
||||
} else {
|
||||
return rule.trigger === trigger
|
||||
}
|
||||
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(() => {
|
||||
const ret: CSSProperties = {}
|
||||
if (elForm.labelPosition === 'top') return ret
|
||||
const labelWidth = addUnit(props.labelWidth || elForm.labelWidth)
|
||||
if (labelWidth) {
|
||||
ret.width = labelWidth
|
||||
}
|
||||
return ret
|
||||
})
|
||||
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 || elForm.labelWidth)
|
||||
if (!props.label && !slots.label) {
|
||||
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(() => {
|
||||
const rules = getRules()
|
||||
let required = false
|
||||
|
||||
if (rules && rules.length) {
|
||||
rules.every((rule) => {
|
||||
if (rule.required) {
|
||||
required = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return required
|
||||
})
|
||||
const sizeClass = useSize(undefined, { formItem: false })
|
||||
|
||||
const validate = (
|
||||
trigger: string,
|
||||
callback: ValidateFieldCallback = NOOP
|
||||
) => {
|
||||
if (!isValidationEnabled.value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
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, fields) => {
|
||||
validateState.value = !errors ? 'success' : 'error'
|
||||
validateMessage.value = errors
|
||||
? errors[0].message || `${props.prop} is required`
|
||||
: ''
|
||||
// fix: #3860 after version 3.5.2, async-validator also return fields if validation fails
|
||||
callback(validateMessage.value, errors ? fields : {})
|
||||
elForm.emit?.(
|
||||
'validate',
|
||||
props.prop,
|
||||
!errors,
|
||||
validateMessage.value || null
|
||||
)
|
||||
})
|
||||
// exclude trigger
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.map(({ trigger, ...rule }): RuleItem => rule)
|
||||
)
|
||||
}
|
||||
|
||||
const isRequired = computed(() =>
|
||||
_rules.value.some((rule) => rule.required === true)
|
||||
)
|
||||
|
||||
const shouldShowError = computed(
|
||||
() =>
|
||||
validateState.value === 'error' &&
|
||||
props.showMessage &&
|
||||
formContext.showMessage
|
||||
)
|
||||
|
||||
const currentLabel = computed(
|
||||
() => `${props.label || ''}${formContext.labelSuffix || ''}`
|
||||
)
|
||||
|
||||
const validate: FormItemContext['validate'] = async (trigger, callback) => {
|
||||
if (callback) {
|
||||
try {
|
||||
validate(trigger)
|
||||
callback(true)
|
||||
} catch (err) {
|
||||
callback(false, err as ValidateFieldsError)
|
||||
}
|
||||
|
||||
const clearValidate = () => {
|
||||
validateState.value = ''
|
||||
validateMessage.value = ''
|
||||
}
|
||||
const resetField = () => {
|
||||
const model = elForm.model
|
||||
const value = fieldValue.value
|
||||
let path = props.prop
|
||||
if (path.indexOf(':') !== -1) {
|
||||
path = path.replace(/:/, '.')
|
||||
}
|
||||
const prop = getPropByPath(model, path, true)
|
||||
if (Array.isArray(value)) {
|
||||
prop.o[prop.k] = [].concat(initialValue)
|
||||
} else {
|
||||
prop.o[prop.k] = initialValue
|
||||
}
|
||||
nextTick(() => {
|
||||
clearValidate()
|
||||
})
|
||||
}
|
||||
validate(trigger)
|
||||
.then(() => callback(true))
|
||||
.catch((fields: ValidateFieldsError) => callback(false, fields))
|
||||
return
|
||||
}
|
||||
|
||||
const getRules = () => {
|
||||
const formRules = elForm.rules
|
||||
const selfRules = props.rules
|
||||
const requiredRule =
|
||||
props.required !== undefined ? { required: !!props.required } : []
|
||||
if (!validateEnabled.value) {
|
||||
return
|
||||
}
|
||||
const rules = getFilteredRule(trigger)
|
||||
if (rules.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const prop = getPropByPath(formRules, props.prop || '', false)
|
||||
const normalizedRule = formRules ? prop.o[props.prop || ''] || prop.v : []
|
||||
validateState.value = 'validating'
|
||||
|
||||
return [].concat(selfRules || normalizedRule || []).concat(requiredRule)
|
||||
}
|
||||
const getFilteredRule = (trigger) => {
|
||||
const rules = getRules()
|
||||
const descriptor = {
|
||||
[propString.value]: rules,
|
||||
}
|
||||
const validator = new AsyncValidator(descriptor)
|
||||
const model = {
|
||||
[propString.value]: fieldValue.value,
|
||||
}
|
||||
|
||||
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 }))
|
||||
}
|
||||
interface ValidateFailure {
|
||||
errors: ValidateError[] | null
|
||||
fields: ValidateFieldsError
|
||||
}
|
||||
|
||||
const evaluateValidationEnabled = () => {
|
||||
isValidationEnabled.value = !!getRules()?.length
|
||||
}
|
||||
return validator
|
||||
.validate(model, { firstFields: true })
|
||||
.then(() => undefined)
|
||||
.catch((err: ValidateFailure) => {
|
||||
const { errors, fields } = err
|
||||
if (!errors || !fields) console.error(err)
|
||||
|
||||
const updateComputedLabelWidth = (width: string | number) => {
|
||||
computedLabelWidth.value = width ? `${width}px` : ''
|
||||
}
|
||||
|
||||
const elFormItem = reactive({
|
||||
...toRefs(props),
|
||||
size: sizeClass,
|
||||
validateState,
|
||||
$el: formItemRef,
|
||||
evaluateValidationEnabled,
|
||||
resetField,
|
||||
clearValidate,
|
||||
validate,
|
||||
updateComputedLabelWidth,
|
||||
validateState.value = 'error'
|
||||
validateMessage.value = errors
|
||||
? errors[0].message || `${props.prop} is required`
|
||||
: ''
|
||||
formContext.emit('validate', props.prop, !errors, validateMessage.value)
|
||||
return Promise.reject(fields)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.prop) {
|
||||
elForm?.addField(elFormItem)
|
||||
const clearValidate: FormItemContext['clearValidate'] = () => {
|
||||
validateState.value = ''
|
||||
validateMessage.value = ''
|
||||
}
|
||||
|
||||
const value = fieldValue.value
|
||||
initialValue = Array.isArray(value) ? [...value] : value
|
||||
const resetField: FormItemContext['resetField'] = () => {
|
||||
const model = formContext.model
|
||||
if (!model || !props.prop) return
|
||||
|
||||
evaluateValidationEnabled()
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
elForm?.removeField(elFormItem)
|
||||
})
|
||||
getProp(model, props.prop).value = initialValue
|
||||
nextTick(() => clearValidate())
|
||||
}
|
||||
|
||||
provide(elFormItemKey, elFormItem)
|
||||
|
||||
const formItemClass = computed(() => [
|
||||
{
|
||||
'el-form-item--feedback': 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.hideRequiredAsterisk,
|
||||
},
|
||||
sizeClass.value ? `el-form-item--${sizeClass.value}` : '',
|
||||
])
|
||||
|
||||
const shouldShowError = computed(() => {
|
||||
return (
|
||||
validateState.value === 'error' &&
|
||||
props.showMessage &&
|
||||
elForm.showMessage
|
||||
)
|
||||
})
|
||||
|
||||
const currentLabel = computed(
|
||||
() => (props.label || '') + (elForm.labelSuffix || '')
|
||||
)
|
||||
|
||||
return {
|
||||
formItemRef,
|
||||
formItemClass,
|
||||
shouldShowError,
|
||||
elForm,
|
||||
labelStyle,
|
||||
contentStyle,
|
||||
validateMessage,
|
||||
labelFor,
|
||||
resetField,
|
||||
clearValidate,
|
||||
currentLabel,
|
||||
}
|
||||
watch(
|
||||
() => props.error,
|
||||
(val) => {
|
||||
validateMessage.value = val || ''
|
||||
validateState.value = val ? 'error' : ''
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(
|
||||
() => props.validateStatus,
|
||||
(val) => (validateState.value = val || '')
|
||||
)
|
||||
|
||||
const context: FormItemContext = reactive({
|
||||
...toRefs(props),
|
||||
$el: formItemRef,
|
||||
size: _size,
|
||||
validateState,
|
||||
resetField,
|
||||
clearValidate,
|
||||
validate,
|
||||
})
|
||||
provide(formItemContextKey, context)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.prop) {
|
||||
formContext.addField(context)
|
||||
initialValue = clone(fieldValue.value)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
formContext.removeField(context)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** @description form item size */
|
||||
size: _size,
|
||||
/** @description validation message */
|
||||
validateMessage,
|
||||
/** @description validate form item */
|
||||
validate,
|
||||
/** @description clear validation status */
|
||||
clearValidate,
|
||||
/** @description reset field value */
|
||||
resetField,
|
||||
})
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
Fragment,
|
||||
h,
|
||||
inject,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
@ -10,30 +10,34 @@ import {
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { addResizeListener, removeResizeListener } from '@element-plus/utils'
|
||||
import { elFormItemKey, elFormKey } from '@element-plus/tokens'
|
||||
import type { ResizableElement, Nullable } from '@element-plus/utils'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { throwError } from '@element-plus/utils'
|
||||
import { formItemContextKey, formContextKey } from '@element-plus/tokens'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
const COMPONENT_NAME = 'ElLabelWrap'
|
||||
export default defineComponent({
|
||||
name: 'ElLabelWrap',
|
||||
name: COMPONENT_NAME,
|
||||
props: {
|
||||
isAutoWidth: Boolean,
|
||||
updateAll: Boolean,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const el = ref<Nullable<HTMLElement>>(null)
|
||||
const elForm = inject(elFormKey)
|
||||
const elFormItem = inject(elFormItemKey)
|
||||
|
||||
setup(props, { slots }) {
|
||||
const formContext = inject(formContextKey)
|
||||
const formItemContext = inject(formItemContextKey)
|
||||
if (!formContext || !formItemContext)
|
||||
throwError(
|
||||
COMPONENT_NAME,
|
||||
'usage: <el-form><el-form-item><label-wrap /></el-form-item></el-form>'
|
||||
)
|
||||
|
||||
const ns = useNamespace('form')
|
||||
|
||||
const el = ref<HTMLElement>()
|
||||
const computedWidth = ref(0)
|
||||
watch(computedWidth, (val, oldVal) => {
|
||||
if (props.updateAll) {
|
||||
elForm.registerLabelWidth(val, oldVal)
|
||||
elFormItem.updateComputedLabelWidth(val)
|
||||
}
|
||||
})
|
||||
|
||||
const getLabelWidth = () => {
|
||||
if (el.value?.firstElementChild) {
|
||||
@ -43,13 +47,14 @@ export default defineComponent({
|
||||
return 0
|
||||
}
|
||||
}
|
||||
const updateLabelWidth = (action = 'update') => {
|
||||
|
||||
const updateLabelWidth = (action: 'update' | 'remove' = 'update') => {
|
||||
nextTick(() => {
|
||||
if (slots.default && props.isAutoWidth) {
|
||||
if (action === 'update') {
|
||||
computedWidth.value = getLabelWidth()
|
||||
} else if (action === 'remove') {
|
||||
elForm.deregisterLabelWidth(computedWidth.value)
|
||||
formContext.deregisterLabelWidth(computedWidth.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -57,53 +62,52 @@ export default defineComponent({
|
||||
const updateLabelWidthFn = () => updateLabelWidth('update')
|
||||
|
||||
onMounted(() => {
|
||||
addResizeListener(
|
||||
el.value.firstElementChild as ResizableElement,
|
||||
updateLabelWidthFn
|
||||
)
|
||||
updateLabelWidthFn()
|
||||
})
|
||||
|
||||
onUpdated(updateLabelWidthFn)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
updateLabelWidth('remove')
|
||||
removeResizeListener(
|
||||
el.value?.firstElementChild as ResizableElement,
|
||||
updateLabelWidthFn
|
||||
)
|
||||
})
|
||||
onUpdated(() => updateLabelWidthFn())
|
||||
|
||||
watch(computedWidth, (val, oldVal) => {
|
||||
if (props.updateAll) {
|
||||
formContext.registerLabelWidth(val, oldVal)
|
||||
}
|
||||
})
|
||||
|
||||
function render() {
|
||||
useResizeObserver(
|
||||
computed(
|
||||
() => (el.value?.firstElementChild ?? null) as HTMLElement | null
|
||||
),
|
||||
updateLabelWidthFn
|
||||
)
|
||||
|
||||
return () => {
|
||||
if (!slots) return null
|
||||
if (props.isAutoWidth) {
|
||||
const autoLabelWidth = elForm.autoLabelWidth
|
||||
const style = {} as CSSProperties
|
||||
|
||||
const { isAutoWidth } = props
|
||||
if (isAutoWidth) {
|
||||
const autoLabelWidth = formContext.autoLabelWidth
|
||||
const style: CSSProperties = {}
|
||||
if (autoLabelWidth && autoLabelWidth !== 'auto') {
|
||||
const marginWidth = Math.max(
|
||||
0,
|
||||
parseInt(autoLabelWidth, 10) - computedWidth.value
|
||||
)
|
||||
const marginPosition =
|
||||
elForm.labelPosition === 'left' ? 'marginRight' : 'marginLeft'
|
||||
formContext.labelPosition === 'left' ? 'marginRight' : 'marginLeft'
|
||||
if (marginWidth) {
|
||||
style[marginPosition] = `${marginWidth}px`
|
||||
}
|
||||
}
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
ref: el,
|
||||
class: ['el-form-item__label-wrap'],
|
||||
style,
|
||||
},
|
||||
slots.default?.()
|
||||
return (
|
||||
<div ref={el} class={[ns.be('item', 'label-wrap')]} style={style}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return h(Fragment, { ref: el }, slots.default?.())
|
||||
return <Fragment ref={el}>{slots.default?.()}</Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
return render
|
||||
},
|
||||
})
|
61
packages/components/form/src/form.ts
Normal file
61
packages/components/form/src/form.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { componentSizes } from '@element-plus/constants'
|
||||
import {
|
||||
buildProps,
|
||||
definePropType,
|
||||
isArray,
|
||||
isString,
|
||||
isBoolean,
|
||||
} from '@element-plus/utils'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { FormItemProps } from './form-item'
|
||||
import type { FormRules } from './types'
|
||||
import type Form from './form.vue'
|
||||
|
||||
export const formProps = buildProps({
|
||||
model: Object,
|
||||
rules: {
|
||||
type: definePropType<FormRules>(Object),
|
||||
},
|
||||
labelPosition: String,
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
labelSuffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inline: Boolean,
|
||||
inlineMessage: Boolean,
|
||||
statusIcon: Boolean,
|
||||
showMessage: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
values: componentSizes,
|
||||
},
|
||||
disabled: Boolean,
|
||||
validateOnRuleChange: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hideRequiredAsterisk: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scrollToError: Boolean,
|
||||
} as const)
|
||||
export type FormProps = ExtractPropTypes<typeof formProps>
|
||||
|
||||
export const formEmits = {
|
||||
validate: (prop: FormItemProps['prop'], isValid: boolean, message: string) =>
|
||||
(isArray(prop) || isString(prop)) &&
|
||||
isBoolean(isValid) &&
|
||||
isString(message),
|
||||
}
|
||||
export type FormEmits = typeof formEmits
|
||||
|
||||
export type FormInstance = InstanceType<typeof Form>
|
@ -1,9 +0,0 @@
|
||||
import type { RuleItem } from 'async-validator'
|
||||
|
||||
export interface FormItemRule extends RuleItem {
|
||||
trigger?: string | string[]
|
||||
}
|
||||
|
||||
export type FormRulesMap<T extends string = string> = Partial<
|
||||
Record<T, FormItemRule | FormItemRule[]>
|
||||
>
|
@ -1,258 +1,159 @@
|
||||
<template>
|
||||
<form :class="formKls">
|
||||
<slot></slot>
|
||||
<form :class="formClasses">
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
provide,
|
||||
reactive,
|
||||
ref,
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { elFormKey } from '@element-plus/tokens'
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, reactive, toRefs, watch } from 'vue'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { useSize } from '@element-plus/hooks'
|
||||
import { formContextKey } from '@element-plus/tokens'
|
||||
import { useNamespace, useSize } from '@element-plus/hooks'
|
||||
import { formProps, formEmits } from './form'
|
||||
import { useFormLabelWidth, filterFields } from './utils'
|
||||
import type { ValidateFieldsError } from 'async-validator'
|
||||
import type { FormItemContext, FormContext } from '@element-plus/tokens'
|
||||
import type { FormValidateCallback } from './types'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { ComponentSize } from '@element-plus/constants'
|
||||
import type { FormRulesMap } from './form.type'
|
||||
import type {
|
||||
ElFormItemContext as FormItemCtx,
|
||||
ValidateFieldCallback,
|
||||
} from '@element-plus/tokens'
|
||||
|
||||
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)
|
||||
if (index === -1) {
|
||||
debugWarn('Form', `unexpected 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)
|
||||
index > -1 && potentialLabelWidthArr.value.splice(index, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
autoLabelWidth,
|
||||
registerLabelWidth,
|
||||
deregisterLabelWidth,
|
||||
}
|
||||
}
|
||||
|
||||
export interface Callback {
|
||||
(isValid?: boolean, invalidFields?: ValidateFieldsError): void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
const COMPONENT_NAME = 'ElForm'
|
||||
defineOptions({
|
||||
name: 'ElForm',
|
||||
props: {
|
||||
model: Object,
|
||||
rules: Object as PropType<FormRulesMap>,
|
||||
labelPosition: String,
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
})
|
||||
const props = defineProps(formProps)
|
||||
const emit = defineEmits(formEmits)
|
||||
|
||||
const fields: FormItemContext[] = []
|
||||
|
||||
const formSize = useSize()
|
||||
const ns = useNamespace('form')
|
||||
const formClasses = computed(() => {
|
||||
const { labelPosition, inline } = props
|
||||
return [
|
||||
ns.b(),
|
||||
ns.m(formSize.value),
|
||||
{
|
||||
[ns.m(`label-${labelPosition}`)]: labelPosition,
|
||||
[ns.m('inline')]: inline,
|
||||
},
|
||||
labelSuffix: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inline: Boolean,
|
||||
inlineMessage: Boolean,
|
||||
statusIcon: Boolean,
|
||||
showMessage: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: String as PropType<ComponentSize>,
|
||||
disabled: Boolean,
|
||||
validateOnRuleChange: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hideRequiredAsterisk: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scrollToError: Boolean,
|
||||
]
|
||||
})
|
||||
|
||||
const addField: FormContext['addField'] = (field) => {
|
||||
fields.push(field)
|
||||
}
|
||||
|
||||
const removeField: FormContext['removeField'] = (field) => {
|
||||
if (!field.prop) {
|
||||
fields.splice(fields.indexOf(field), 1)
|
||||
}
|
||||
}
|
||||
|
||||
const resetFields: FormContext['resetFields'] = (properties = []) => {
|
||||
if (!props.model) {
|
||||
debugWarn(COMPONENT_NAME, 'model is required for resetFields to work.')
|
||||
return
|
||||
}
|
||||
filterFields(fields, properties).forEach((field) => field.resetField())
|
||||
}
|
||||
|
||||
const clearValidate: FormContext['clearValidate'] = (props = []) => {
|
||||
filterFields(fields, props).forEach((field) => field.clearValidate())
|
||||
}
|
||||
|
||||
const validate = async (callback?: FormValidateCallback): Promise<void> =>
|
||||
validateField(undefined, callback)
|
||||
|
||||
const validateField: FormContext['validateField'] = async (
|
||||
properties = [],
|
||||
callback
|
||||
) => {
|
||||
if (callback) {
|
||||
validate()
|
||||
.then(() => callback(true))
|
||||
.catch((fields: ValidateFieldsError) => callback(false, fields))
|
||||
return
|
||||
}
|
||||
|
||||
const { model, scrollToError } = props
|
||||
|
||||
if (!model) {
|
||||
debugWarn(COMPONENT_NAME, 'model is required for form validation!')
|
||||
return
|
||||
}
|
||||
if (fields.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const filteredFields = filterFields(fields, properties)
|
||||
if (!filteredFields.length) {
|
||||
debugWarn(COMPONENT_NAME, 'please pass correct props!')
|
||||
return
|
||||
}
|
||||
|
||||
let valid = true
|
||||
let invalidFields: ValidateFieldsError = {}
|
||||
let firstInvalidFields: ValidateFieldsError | undefined
|
||||
|
||||
for (const field of filteredFields) {
|
||||
const fieldsError = await field
|
||||
.validate('')
|
||||
.catch((fields: ValidateFieldsError) => fields)
|
||||
|
||||
if (fieldsError) {
|
||||
valid = false
|
||||
if (!firstInvalidFields) firstInvalidFields = fieldsError
|
||||
}
|
||||
|
||||
invalidFields = { ...invalidFields, ...fieldsError }
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
if (scrollToError) scrollToField(Object.keys(firstInvalidFields!)[0])
|
||||
return Promise.reject(invalidFields)
|
||||
}
|
||||
}
|
||||
|
||||
const scrollToField = (prop: string) => {
|
||||
const field = filterFields(fields, prop)[0]
|
||||
if (field) {
|
||||
field.$el?.scrollIntoView()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.rules,
|
||||
() => {
|
||||
if (props.validateOnRuleChange) validate()
|
||||
},
|
||||
emits: ['validate'],
|
||||
setup(props, { emit }) {
|
||||
const fields: FormItemCtx[] = []
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.rules,
|
||||
() => {
|
||||
fields.forEach((field) => {
|
||||
field.evaluateValidationEnabled()
|
||||
})
|
||||
provide(
|
||||
formContextKey,
|
||||
reactive({
|
||||
...toRefs(props),
|
||||
emit,
|
||||
|
||||
if (props.validateOnRuleChange) {
|
||||
validate(() => ({}))
|
||||
}
|
||||
}
|
||||
)
|
||||
resetFields,
|
||||
clearValidate,
|
||||
validateField,
|
||||
addField,
|
||||
removeField,
|
||||
|
||||
const formSize = useSize()
|
||||
const prefix = 'el-form'
|
||||
const formKls = computed(() => {
|
||||
const { labelPosition, inline } = props
|
||||
return [
|
||||
prefix,
|
||||
`${prefix}--${formSize.value}`,
|
||||
labelPosition ? `${prefix}--label-${labelPosition}` : '',
|
||||
inline ? `${prefix}--inline` : '',
|
||||
]
|
||||
})
|
||||
...useFormLabelWidth(),
|
||||
})
|
||||
)
|
||||
|
||||
const addField = (field: FormItemCtx) => {
|
||||
if (field) {
|
||||
fields.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
const removeField = (field: FormItemCtx) => {
|
||||
if (field.prop) {
|
||||
fields.splice(fields.indexOf(field), 1)
|
||||
}
|
||||
}
|
||||
|
||||
const resetFields = () => {
|
||||
if (!props.model) {
|
||||
debugWarn('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) {
|
||||
debugWarn('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 = {}
|
||||
let firstInvalidFields
|
||||
for (const field of fields) {
|
||||
field.validate('', (message, field) => {
|
||||
if (message) {
|
||||
valid = false
|
||||
firstInvalidFields || (firstInvalidFields = field)
|
||||
}
|
||||
invalidFields = { ...invalidFields, ...field }
|
||||
if (++count === fields.length) {
|
||||
callback(valid, invalidFields)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!valid && props.scrollToError) {
|
||||
scrollToField(Object.keys(firstInvalidFields)[0])
|
||||
}
|
||||
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) {
|
||||
debugWarn('Form', 'please pass correct props!')
|
||||
return
|
||||
}
|
||||
|
||||
fds.forEach((field) => {
|
||||
field.validate('', cb)
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToField = (prop: string) => {
|
||||
fields.forEach((item) => {
|
||||
if (item.prop === prop) {
|
||||
item.$el.scrollIntoView?.()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const elForm = reactive({
|
||||
...toRefs(props),
|
||||
resetFields,
|
||||
clearValidate,
|
||||
validateField,
|
||||
emit,
|
||||
addField,
|
||||
removeField,
|
||||
...useFormLabelWidth(),
|
||||
})
|
||||
|
||||
provide(elFormKey, elForm)
|
||||
|
||||
return {
|
||||
formKls,
|
||||
validate, // export
|
||||
resetFields,
|
||||
clearValidate,
|
||||
validateField,
|
||||
scrollToField,
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
/** @description validate form */
|
||||
validate,
|
||||
/** @description validate form field */
|
||||
validateField,
|
||||
/** @description reset fields */
|
||||
resetFields,
|
||||
/** @description clear validation status */
|
||||
clearValidate,
|
||||
/** @description scroll to field */
|
||||
scrollToField,
|
||||
})
|
||||
</script>
|
||||
|
15
packages/components/form/src/types.ts
Normal file
15
packages/components/form/src/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { RuleItem, ValidateFieldsError } from 'async-validator'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
import type { useFormLabelWidth } from './utils'
|
||||
|
||||
export interface FormItemRule extends RuleItem {
|
||||
trigger?: Arrayable<string>
|
||||
}
|
||||
export type FormRules = Partial<Record<string, Arrayable<FormItemRule>>>
|
||||
|
||||
export type FormValidateCallback = (
|
||||
isValid: boolean,
|
||||
invalidFields?: ValidateFieldsError
|
||||
) => void
|
||||
|
||||
export type FormLabelWidthContext = ReturnType<typeof useFormLabelWidth>
|
57
packages/components/form/src/utils.ts
Normal file
57
packages/components/form/src/utils.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { debugWarn, ensureArray } from '@element-plus/utils'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
import type { FormItemContext } from '@element-plus/tokens'
|
||||
import type { FormItemProps } from './form-item'
|
||||
|
||||
const SCOPE = 'ElForm'
|
||||
|
||||
export function useFormLabelWidth() {
|
||||
const potentialLabelWidthArr = ref<number[]>([])
|
||||
|
||||
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)
|
||||
if (index === -1) {
|
||||
debugWarn(SCOPE, `unexpected 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)
|
||||
if (index > -1) {
|
||||
potentialLabelWidthArr.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
autoLabelWidth,
|
||||
registerLabelWidth,
|
||||
deregisterLabelWidth,
|
||||
}
|
||||
}
|
||||
|
||||
export const filterFields = (
|
||||
fields: FormItemContext[],
|
||||
props: Arrayable<FormItemProps['prop']>
|
||||
) => {
|
||||
const normalized = ensureArray(props)
|
||||
return normalized.length > 0
|
||||
? fields.filter((field) => field.prop && normalized.includes(field.prop))
|
||||
: fields
|
||||
}
|
@ -208,7 +208,7 @@ export default defineComponent({
|
||||
emit('update:modelValue', newVal)
|
||||
emit('input', newVal)
|
||||
emit('change', newVal, oldVal)
|
||||
formItem?.validate?.('change')
|
||||
formItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
data.currentValue = newVal
|
||||
}
|
||||
const handleInput = (value: string) => {
|
||||
@ -236,7 +236,7 @@ export default defineComponent({
|
||||
|
||||
const handleBlur = (event: MouseEvent) => {
|
||||
emit('blur', event)
|
||||
formItem?.validate?.('blur')
|
||||
formItem?.validate?.('blur').catch((err) => debugWarn(err))
|
||||
}
|
||||
|
||||
watch(
|
||||
|
@ -148,7 +148,12 @@ import {
|
||||
import { isClient } from '@vueuse/core'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { CircleClose, View as IconView } from '@element-plus/icons-vue'
|
||||
import { ValidateComponentsMap, isObject, isKorean } from '@element-plus/utils'
|
||||
import {
|
||||
ValidateComponentsMap,
|
||||
isObject,
|
||||
isKorean,
|
||||
debugWarn,
|
||||
} from '@element-plus/utils'
|
||||
import {
|
||||
useAttrs,
|
||||
useDisabled,
|
||||
@ -340,7 +345,7 @@ export default defineComponent({
|
||||
focused.value = false
|
||||
emit('blur', event)
|
||||
if (props.validateEvent) {
|
||||
formItem?.validate?.('blur')
|
||||
formItem?.validate?.('blur').catch((err) => debugWarn(err))
|
||||
}
|
||||
}
|
||||
|
||||
@ -395,7 +400,7 @@ export default defineComponent({
|
||||
() => {
|
||||
nextTick(resizeTextarea)
|
||||
if (props.validateEvent) {
|
||||
formItem?.validate?.('change')
|
||||
formItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
import { EVENT_CODE, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { radioGroupKey } from '@element-plus/tokens'
|
||||
import { useFormItem, useNamespace } from '@element-plus/hooks'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { radioGroupEmits, radioGroupProps } from './radio-group'
|
||||
import type { RadioGroupProps } from '..'
|
||||
|
||||
@ -96,7 +97,7 @@ export default defineComponent({
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => formItem?.validate('change')
|
||||
() => formItem?.validate('change').catch((err) => debugWarn(err))
|
||||
)
|
||||
|
||||
return {
|
||||
|
@ -47,14 +47,15 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, computed, ref, watch } from 'vue'
|
||||
import { isObject, isArray } from '@vue/shared'
|
||||
import { elFormKey } from '@element-plus/tokens'
|
||||
import { formContextKey } from '@element-plus/tokens'
|
||||
import { hasClass } from '@element-plus/utils'
|
||||
import { EVENT_CODE, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { StarFilled, Star } from '@element-plus/icons-vue'
|
||||
import { useNamespace, useSize } from '@element-plus/hooks'
|
||||
import { rateProps, rateEmits } from './rate'
|
||||
import type { ElFormContext } from '@element-plus/tokens'
|
||||
|
||||
import type { FormContext } from '@element-plus/tokens'
|
||||
|
||||
function getValueFromMap<T>(
|
||||
value: number,
|
||||
@ -87,7 +88,8 @@ export default defineComponent({
|
||||
emits: rateEmits,
|
||||
|
||||
setup(props, { emit }) {
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
|
||||
const rateSize = useSize()
|
||||
const ns = useNamespace('rate')
|
||||
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
ValidateComponentsMap,
|
||||
addResizeListener,
|
||||
removeResizeListener,
|
||||
debugWarn,
|
||||
} from '@element-plus/utils'
|
||||
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
|
||||
|
||||
@ -723,7 +724,7 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
|
||||
initStates()
|
||||
}
|
||||
if (!isEqual(val, oldVal)) {
|
||||
elFormItem?.validate?.('change')
|
||||
elFormItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -16,13 +16,13 @@ import {
|
||||
CHANGE_EVENT,
|
||||
EVENT_CODE,
|
||||
} from '@element-plus/constants'
|
||||
import { isKorean, scrollIntoView } from '@element-plus/utils'
|
||||
import { debugWarn, isKorean, scrollIntoView } from '@element-plus/utils'
|
||||
import { useLocale, useNamespace, useSize } from '@element-plus/hooks'
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
|
||||
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
||||
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import type ElTooltip from '@element-plus/components/tooltip'
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormContext, FormItemContext } from '@element-plus/tokens'
|
||||
import type { QueryChangeCtx, SelectOptionProxy } from './token'
|
||||
|
||||
export function useSelectStates(props) {
|
||||
@ -79,8 +79,8 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
const groupQueryChange = shallowRef('')
|
||||
|
||||
// inject
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
|
||||
const readonly = computed(
|
||||
() => !props.filterable || props.multiple || !states.visible
|
||||
@ -206,7 +206,7 @@ export const useSelect = (props, states: States, ctx) => {
|
||||
states.inputLength = 20
|
||||
}
|
||||
if (!isEqual(val, oldVal)) {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -97,7 +97,13 @@ import {
|
||||
CHANGE_EVENT,
|
||||
INPUT_EVENT,
|
||||
} from '@element-plus/constants'
|
||||
import { off, on, throwError, isValidComponentSize } from '@element-plus/utils'
|
||||
import {
|
||||
off,
|
||||
on,
|
||||
throwError,
|
||||
isValidComponentSize,
|
||||
debugWarn,
|
||||
} from '@element-plus/utils'
|
||||
import { useNamespace, useSize } from '@element-plus/hooks'
|
||||
import SliderButton from './button.vue'
|
||||
import SliderMarker from './marker.vue'
|
||||
@ -339,7 +345,7 @@ const useWatch = (props, initData, minValue, maxValue, emit, elFormItem) => {
|
||||
initData.firstValue = val[0]
|
||||
initData.secondValue = val[1]
|
||||
if (valueChanged()) {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
initData.oldValue = val.slice()
|
||||
}
|
||||
}
|
||||
@ -351,7 +357,7 @@ const useWatch = (props, initData, minValue, maxValue, emit, elFormItem) => {
|
||||
} else {
|
||||
initData.firstValue = val
|
||||
if (valueChanged()) {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
initData.oldValue = val
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,10 @@ import {
|
||||
UPDATE_MODEL_EVENT,
|
||||
INPUT_EVENT,
|
||||
} from '@element-plus/constants'
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
|
||||
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { ButtonRefs, ISliderInitData, ISliderProps } from './slider.type'
|
||||
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormContext, FormItemContext } from '@element-plus/tokens'
|
||||
import type { Nullable } from '@element-plus/utils'
|
||||
|
||||
export const useSlide = (
|
||||
@ -16,8 +15,8 @@ export const useSlide = (
|
||||
initData: ISliderInitData,
|
||||
emit
|
||||
) => {
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
|
||||
const slider = shallowRef<Nullable<HTMLElement>>(null)
|
||||
|
||||
|
@ -167,7 +167,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (props.validateEvent) {
|
||||
formItem?.validate?.('change')
|
||||
formItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { h } from 'vue'
|
||||
import ElCheckbox from '@element-plus/components/checkbox'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { ArrowRight, Loading } from '@element-plus/icons-vue'
|
||||
import { getPropByPath } from '@element-plus/utils'
|
||||
import { getProp } from '@element-plus/utils'
|
||||
|
||||
import type { VNode } from 'vue'
|
||||
import type { TableColumnCtx } from './table-column/defaults'
|
||||
@ -156,7 +156,7 @@ export function defaultRenderCell<T>({
|
||||
$index: number
|
||||
}) {
|
||||
const property = column.property
|
||||
const value = property && getPropByPath(row, property, false).v
|
||||
const value = property && getProp(row, property).value
|
||||
if (column && column.formatter) {
|
||||
return column.formatter(row, column, value, $index)
|
||||
}
|
||||
|
@ -159,18 +159,18 @@ import dayjs from 'dayjs'
|
||||
import { isEqual } from 'lodash-unified'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { useLocale, useSize } from '@element-plus/hooks'
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
|
||||
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
||||
import ElInput from '@element-plus/components/input'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import ElTooltip from '@element-plus/components/tooltip'
|
||||
import { isEmpty } from '@element-plus/utils'
|
||||
import { debugWarn, isEmpty } from '@element-plus/utils'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
import { Clock, Calendar } from '@element-plus/icons-vue'
|
||||
import { timePickerDefaultProps } from './props'
|
||||
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormContext, FormItemContext } from '@element-plus/tokens'
|
||||
import type { Options } from '@popperjs/core'
|
||||
|
||||
interface PickerOptions {
|
||||
@ -254,8 +254,8 @@ export default defineComponent({
|
||||
setup(props, ctx) {
|
||||
const { lang } = useLocale()
|
||||
|
||||
const elForm = inject(elFormKey, {} as ElFormContext)
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elForm = inject(formContextKey, {} as FormContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
const elPopperOptions = inject('ElPopperOptions', {} as Options)
|
||||
|
||||
const refPopper = ref<InstanceType<typeof ElTooltip>>()
|
||||
@ -272,7 +272,8 @@ export default defineComponent({
|
||||
})
|
||||
ctx.emit('blur')
|
||||
blurInput()
|
||||
props.validateEvent && elFormItem.validate?.('blur')
|
||||
props.validateEvent &&
|
||||
elFormItem.validate?.('blur').catch((err) => debugWarn(err))
|
||||
} else {
|
||||
valueOnOpen.value = props.modelValue
|
||||
}
|
||||
@ -281,7 +282,8 @@ export default defineComponent({
|
||||
// determine user real change only
|
||||
if (isClear || !valueEquals(val, valueOnOpen.value)) {
|
||||
ctx.emit('change', val)
|
||||
props.validateEvent && elFormItem.validate?.('change')
|
||||
props.validateEvent &&
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
}
|
||||
const emitInput = (val) => {
|
||||
|
@ -66,10 +66,11 @@ import {
|
||||
} from 'vue'
|
||||
import ElButton from '@element-plus/components/button'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import { elFormItemKey } from '@element-plus/tokens'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { formItemContextKey } from '@element-plus/tokens'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import TransferPanel from './transfer-panel.vue'
|
||||
import { useComputedData } from './useComputedData'
|
||||
import {
|
||||
@ -81,7 +82,7 @@ import { useMove } from './useMove'
|
||||
import { CHANGE_EVENT } from './transfer'
|
||||
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import type { ElFormItemContext } from '@element-plus/tokens'
|
||||
import type { FormItemContext } from '@element-plus/tokens'
|
||||
import type { DataItem, Format, Key, Props, TargetOrder } from './transfer'
|
||||
|
||||
type TransferType = InstanceType<typeof TransferPanel>
|
||||
@ -165,7 +166,7 @@ export default defineComponent({
|
||||
setup(props, { emit, slots }) {
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('transfer')
|
||||
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
|
||||
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
|
||||
|
||||
const checkedState = reactive({
|
||||
leftChecked: [],
|
||||
@ -217,7 +218,7 @@ export default defineComponent({
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
elFormItem.validate?.('change')
|
||||
elFormItem.validate?.('change').catch((err) => debugWarn(err))
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -4,14 +4,14 @@ import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { ElButton } from '@element-plus/components'
|
||||
import {
|
||||
elFormKey,
|
||||
elFormItemKey,
|
||||
formContextKey,
|
||||
formItemContextKey,
|
||||
buttonGroupContextKey,
|
||||
} from '@element-plus/tokens'
|
||||
|
||||
import type {
|
||||
ElFormContext,
|
||||
ElFormItemContext,
|
||||
FormContext,
|
||||
FormItemContext,
|
||||
ButtonGroupContext,
|
||||
} from '@element-plus/tokens'
|
||||
|
||||
@ -38,7 +38,7 @@ describe('use-form-item', () => {
|
||||
const propSize = 'small'
|
||||
const wrapper = mountComponent(
|
||||
() => {
|
||||
provide(elFormItemKey, { size: 'large' } as ElFormItemContext)
|
||||
provide(formItemContextKey, { size: 'large' })
|
||||
},
|
||||
{
|
||||
props: { size: propSize },
|
||||
@ -55,9 +55,9 @@ describe('use-form-item', () => {
|
||||
size: fallbackSize,
|
||||
} as ButtonGroupContext)
|
||||
|
||||
provide(elFormItemKey, {
|
||||
provide(formItemContextKey, {
|
||||
size: 'large',
|
||||
} as ElFormItemContext)
|
||||
} as FormItemContext)
|
||||
})
|
||||
|
||||
expect(wrapper.find(`.el-button--${fallbackSize}`).exists()).toBe(true)
|
||||
@ -66,13 +66,13 @@ describe('use-form-item', () => {
|
||||
it('should return formItem.size instead form.size', () => {
|
||||
const itemSize = 'small'
|
||||
const wrapper = mountComponent(() => {
|
||||
provide(elFormItemKey, {
|
||||
provide(formItemContextKey, {
|
||||
size: itemSize,
|
||||
} as ElFormItemContext)
|
||||
} as FormItemContext)
|
||||
|
||||
provide(elFormKey, {
|
||||
provide(formContextKey, {
|
||||
size: 'large',
|
||||
} as ElFormContext)
|
||||
} as FormContext)
|
||||
})
|
||||
|
||||
expect(wrapper.find(`.el-button--${itemSize}`).exists()).toBe(true)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ref, unref, inject, computed } from 'vue'
|
||||
import { elFormItemKey, elFormKey } from '@element-plus/tokens'
|
||||
import { formItemContextKey, formContextKey } from '@element-plus/tokens'
|
||||
import { buildProp } from '@element-plus/utils'
|
||||
import { componentSizes } from '@element-plus/constants'
|
||||
import { useProp } from '../use-prop'
|
||||
@ -21,10 +21,12 @@ export const useSize = (
|
||||
|
||||
const size = ignore.prop ? emptyRef : useProp<ComponentSize>('size')
|
||||
const globalConfig = ignore.global ? emptyRef : useGlobalConfig('size')
|
||||
const form = ignore.form ? { size: undefined } : inject(elFormKey, undefined)
|
||||
const form = ignore.form
|
||||
? { size: undefined }
|
||||
: inject(formContextKey, undefined)
|
||||
const formItem = ignore.formItem
|
||||
? { size: undefined }
|
||||
: inject(elFormItemKey, undefined)
|
||||
: inject(formItemContextKey, undefined)
|
||||
|
||||
return computed(
|
||||
(): ComponentSize =>
|
||||
@ -39,7 +41,7 @@ export const useSize = (
|
||||
|
||||
export const useDisabled = (fallback?: MaybeRef<boolean | undefined>) => {
|
||||
const disabled = useProp<boolean>('disabled')
|
||||
const form = inject(elFormKey, undefined)
|
||||
const form = inject(formContextKey, undefined)
|
||||
return computed(
|
||||
() => disabled.value || unref(fallback) || form?.disabled || false
|
||||
)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { inject } from 'vue'
|
||||
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
|
||||
import { formContextKey, formItemContextKey } from '@element-plus/tokens'
|
||||
|
||||
export const useFormItem = () => {
|
||||
const form = inject(elFormKey, undefined)
|
||||
const formItem = inject(elFormItemKey, undefined)
|
||||
const form = inject(formContextKey, undefined)
|
||||
const formItem = inject(formItemContextKey, undefined)
|
||||
return {
|
||||
form,
|
||||
formItem,
|
||||
|
@ -1,47 +1,39 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { ValidateFieldsError } from 'async-validator'
|
||||
import type { InjectionKey, SetupContext, UnwrapRef } from 'vue'
|
||||
import type { ComponentSize } from '@element-plus/constants'
|
||||
import type {
|
||||
FormProps,
|
||||
FormEmits,
|
||||
FormItemProps,
|
||||
FormValidateCallback,
|
||||
FormLabelWidthContext,
|
||||
} from '@element-plus/components/form'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
|
||||
export interface ElFormContext {
|
||||
registerLabelWidth(width: number, oldWidth: number): void
|
||||
deregisterLabelWidth(width: number): void
|
||||
autoLabelWidth: string | undefined
|
||||
emit: (evt: string, ...args: any[]) => void
|
||||
addField: (field: ElFormItemContext) => void
|
||||
removeField: (field: ElFormItemContext) => void
|
||||
resetFields: () => void
|
||||
clearValidate: (props: string | string[]) => void
|
||||
validateField: (props: string | string[], cb: ValidateFieldCallback) => void
|
||||
labelSuffix: string
|
||||
inline?: boolean
|
||||
inlineMessage?: boolean
|
||||
model?: Record<string, unknown>
|
||||
size?: ComponentSize
|
||||
showMessage?: boolean
|
||||
labelPosition?: string
|
||||
labelWidth?: string | number
|
||||
rules?: Record<string, unknown>
|
||||
statusIcon?: boolean
|
||||
hideRequiredAsterisk?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
export type FormContext = FormProps &
|
||||
UnwrapRef<FormLabelWidthContext> & {
|
||||
emit: SetupContext<FormEmits>['emit']
|
||||
|
||||
export interface ValidateFieldCallback {
|
||||
(isValid?: string, invalidFields?: ValidateFieldsError): void
|
||||
}
|
||||
// expose
|
||||
addField: (field: FormItemContext) => void
|
||||
removeField: (field: FormItemContext) => void
|
||||
resetFields: (props?: Arrayable<string>) => void
|
||||
clearValidate: (props?: Arrayable<string>) => void
|
||||
validateField: (
|
||||
props?: Arrayable<string>,
|
||||
callback?: FormValidateCallback
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export interface ElFormItemContext {
|
||||
prop?: string
|
||||
size?: ComponentSize
|
||||
export interface FormItemContext extends FormItemProps {
|
||||
$el: HTMLDivElement | undefined
|
||||
size: ComponentSize
|
||||
validateState: string
|
||||
$el: HTMLDivElement
|
||||
validate(trigger: string, callback?: ValidateFieldCallback): void
|
||||
updateComputedLabelWidth(width: number): void
|
||||
evaluateValidationEnabled(): void
|
||||
validate: (trigger: string, callback?: FormValidateCallback) => Promise<void>
|
||||
resetField(): void
|
||||
clearValidate(): void
|
||||
}
|
||||
|
||||
export const elFormKey: InjectionKey<ElFormContext> = Symbol('elForm')
|
||||
export const elFormItemKey: InjectionKey<ElFormItemContext> =
|
||||
Symbol('elFormItem')
|
||||
export const formContextKey: InjectionKey<FormContext> =
|
||||
Symbol('formContextKey')
|
||||
export const formItemContextKey: InjectionKey<FormItemContext> =
|
||||
Symbol('formItemContextKey')
|
||||
|
34
packages/utils/__tests__/objects.vitest.ts
Normal file
34
packages/utils/__tests__/objects.vitest.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getProp } from '..'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
|
||||
describe('objects', () => {
|
||||
it('getProp should work', () => {
|
||||
const obj = {
|
||||
a: {
|
||||
b: {
|
||||
c: 'd',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
['@@::']: 'hello',
|
||||
'abc.': 'cde',
|
||||
},
|
||||
key: 'value',
|
||||
}
|
||||
|
||||
// get
|
||||
expect(getProp(obj, 'a.b.c').value).toBe('d')
|
||||
expect(getProp(obj, 'key').value).toBe('value')
|
||||
expect(getProp(obj, 'foo.@@::').value).toBe('hello')
|
||||
expect(getProp(obj, ['foo', 'abc.']).value).toBe('cde')
|
||||
|
||||
// set
|
||||
getProp(obj, ['foo', 'abc.']).value = AXIOM
|
||||
expect(obj.foo['abc.']).toBe(AXIOM)
|
||||
|
||||
getProp(obj, 'a.b.c').value = AXIOM
|
||||
expect(obj.a.b.c).toBe(AXIOM)
|
||||
})
|
||||
})
|
@ -1,3 +1,5 @@
|
||||
import { isString } from './types'
|
||||
|
||||
class ElementPlusError extends Error {
|
||||
constructor(m: string) {
|
||||
super(m)
|
||||
@ -9,9 +11,14 @@ export function throwError(scope: string, m: string): never {
|
||||
throw new ElementPlusError(`[${scope}] ${m}`)
|
||||
}
|
||||
|
||||
export function debugWarn(scope: string, message: string): void {
|
||||
export function debugWarn(err: Error): void
|
||||
export function debugWarn(scope: string, message: string): void
|
||||
export function debugWarn(scope: string | Error, message?: string): void {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const error: Error = isString(scope)
|
||||
? new ElementPlusError(`[${scope}] ${message}`)
|
||||
: scope
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(new ElementPlusError(`[${scope}] ${message}`))
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +1,22 @@
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { throwError } from './error'
|
||||
import { get, set } from 'lodash-unified'
|
||||
import type { Entries } from 'type-fest'
|
||||
|
||||
const SCOPE = 'UtilV2/objects'
|
||||
import type { Arrayable } from '.'
|
||||
|
||||
export const keysOf = <T>(arr: T) => Object.keys(arr) as Array<keyof T>
|
||||
export const entriesOf = <T>(arr: T) => Object.entries(arr) as Entries<T>
|
||||
export { hasOwn } from '@vue/shared'
|
||||
|
||||
/** @deprecated TODO: improve it, use lodash */
|
||||
export function getPropByPath(
|
||||
obj: any,
|
||||
path: string,
|
||||
strict: boolean
|
||||
): {
|
||||
o: any
|
||||
k: string
|
||||
v: any
|
||||
} {
|
||||
let tempObj = obj
|
||||
let key, value
|
||||
|
||||
if (obj && hasOwn(obj, path)) {
|
||||
key = path
|
||||
value = tempObj?.[path]
|
||||
} else {
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1')
|
||||
path = path.replace(/^\./, '')
|
||||
|
||||
const keyArr = path.split('.')
|
||||
let i = 0
|
||||
for (i; i < keyArr.length - 1; i++) {
|
||||
if (!tempObj && !strict) break
|
||||
const key = keyArr[i]
|
||||
|
||||
if (key in tempObj) {
|
||||
tempObj = tempObj[key]
|
||||
} else {
|
||||
if (strict) {
|
||||
throwError(SCOPE, 'Please transfer a valid prop path to form item!')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
key = keyArr[i]
|
||||
value = tempObj?.[keyArr[i]]
|
||||
}
|
||||
export const getProp = <T = any>(
|
||||
obj: Record<string, any>,
|
||||
path: Arrayable<string>,
|
||||
defaultValue?: any
|
||||
): { value: T } => {
|
||||
return {
|
||||
o: tempObj,
|
||||
k: key,
|
||||
v: value,
|
||||
get value() {
|
||||
return get(obj, path, defaultValue)
|
||||
},
|
||||
set value(val: any) {
|
||||
set(obj, path, val)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user