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:
三咲智子 2022-03-06 22:20:56 +08:00 committed by GitHub
parent 174f328e54
commit c72679e4e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1324 additions and 1967 deletions

View File

@ -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',

View File

@ -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>

View File

@ -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) => {

View File

@ -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[]

View File

@ -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({

View File

@ -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)
}
})
}

View File

@ -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"]

View File

@ -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))
},
})

View File

@ -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 () => {

View File

@ -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))
}
)

View File

@ -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

View 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)
})
})

View File

@ -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'

View 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>

View File

@ -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>

View File

@ -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
},
})

View 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>

View File

@ -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[]>
>

View File

@ -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>

View 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>

View 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
}

View File

@ -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(

View File

@ -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))
}
}
)

View File

@ -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 {

View File

@ -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')

View File

@ -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))
}
},
{

View File

@ -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))
}
},
{

View File

@ -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
}
}

View File

@ -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)

View File

@ -167,7 +167,7 @@ export default defineComponent({
}
if (props.validateEvent) {
formItem?.validate?.('change')
formItem?.validate?.('change').catch((err) => debugWarn(err))
}
})

View File

@ -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)
}

View File

@ -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) => {

View File

@ -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))
}
)

View File

@ -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)

View File

@ -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
)

View File

@ -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,

View File

@ -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')

View 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)
})
})

View File

@ -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)
}
}

View File

@ -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)
},
}
}