Skip to content

Commit

Permalink
feat(cdk:forms): add interactions trigger (#1766)
Browse files Browse the repository at this point in the history
  • Loading branch information
sallerli1 committed Jan 2, 2024
1 parent d66a646 commit 16652a8
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 44 deletions.
8 changes: 6 additions & 2 deletions packages/cdk/forms/docs/Api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function useFormControl<T>(
| `disabled` | 默认禁用当前控件 | `boolean \| (control: AbstractControl, initializing: boolean) => boolean` | - | `initializing``true` 时,表示处于初始化中,此时 `control` 的部分属性还不能访问。 |
| `name` | 控件的名称 | `string` | - | 通常用于自定义提示信息 |
| `example` | 控件的示例 | `string` | - | 通常用于自定义提示信息 |
| `trigger` | 验证器触发的时机 | `'change' \| 'blur' \| 'submit'` | `change` | - |
| `trigger` | 验证器触发的时机 | `'change' \| 'blur' \| 'submit' \| 'interactions'` | `change` | - |
| `validators` | 一个同步验证器函数或数组 | `ValidatorFn \| ValidatorFn[]` | - | - |
| `asyncValidators` | 一个异步验证器函数或数组 | `AsyncValidatorFn \| AsyncValidatorFn[]` | - | - |

Expand Down Expand Up @@ -231,6 +231,10 @@ export abstract class AbstractControl<T = any> {
* 如果用户在 UI 中尚未更改控件的值,则该控件是 `pristine`
*/
readonly pristine: ComputedRef<boolean>;
/**
* 如果控件被执行过校验,则 `validated``true`
*/
readonly validated: ComputedRef<boolean>;
/**
* 此控件的父级控制器, 如果不存在则为 `undefined`
*/
Expand All @@ -244,7 +248,7 @@ export abstract class AbstractControl<T = any> {
* 可选值: `'change'` | `'blur'` | `'submit'`
* 默认值: `'change'`
*/
get trigger(): 'change' | 'blur' | 'submit';
get trigger(): 'change' | 'blur' | 'submit' | 'interactions';
/**
* 此控件的名称,通常用于验证提示信息
*/
Expand Down
61 changes: 55 additions & 6 deletions packages/cdk/forms/src/models/abstractControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
type ValidateStatus,
type ValidatorFn,
type ValidatorOptions,
type ValidatorTrigger,
} from '../types'
import { Validators, addValidators, hasValidator, removeValidators } from '../validators'

Expand Down Expand Up @@ -126,6 +127,11 @@ export abstract class AbstractControl<T = any> {
*/
readonly pristine!: ComputedRef<boolean>

/**
* A control has been `validated`
*/
readonly validated!: ComputedRef<boolean>

/**
* The parent control.
*/
Expand All @@ -151,7 +157,7 @@ export abstract class AbstractControl<T = any> {
* Possible values: `'change'` | `'blur'` | `'submit'`
* Default value: `'change'`
*/
get trigger(): 'change' | 'blur' | 'submit' {
get trigger(): ValidatorTrigger {
return this._trigger ?? this._parent?.trigger ?? 'change'
}

Expand All @@ -168,13 +174,15 @@ export abstract class AbstractControl<T = any> {
protected _blurred = ref(false)
protected _dirty = ref(false)
protected _initializing = ref(true)
protected _validated = ref(false)

private _validators: ValidatorFn | ValidatorFn[] | undefined
private _composedValidators: ValidatorFn | undefined
private _asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | undefined
private _composedAsyncValidators: AsyncValidatorFn | undefined
private _parent: AbstractControl<T> | undefined
private _trigger?: 'change' | 'blur' | 'submit'
private _trigger?: ValidatorTrigger
private _blurMarkedCb?: () => void

constructor(
controls?: GroupControls<T> | AbstractControl<ArrayElement<T>>[],
Expand Down Expand Up @@ -284,9 +292,7 @@ export abstract class AbstractControl<T = any> {
this._blurred.value = true
}

if (this.trigger === 'blur') {
this._validate()
}
this._blurMarkedCb?.()
}

/**
Expand Down Expand Up @@ -320,6 +326,8 @@ export abstract class AbstractControl<T = any> {
} else {
this._dirty.value = false
}

this._validated.value = false
}

/**
Expand Down Expand Up @@ -524,8 +532,8 @@ export abstract class AbstractControl<T = any> {

protected async _validate(): Promise<ValidateErrors | undefined> {
let newErrors = undefined
let value = undefined
if (!this._disabled.value) {
let value = undefined
if (this._composedValidators) {
value = this.getValue()
newErrors = this._composedValidators(value, this)
Expand All @@ -538,8 +546,10 @@ export abstract class AbstractControl<T = any> {
newErrors = await this._composedAsyncValidators(value, this)
}
}
this._validated.value = true
this.setErrors(newErrors)
this._status.value = newErrors ? 'invalid' : 'valid'

return newErrors
}

Expand Down Expand Up @@ -596,6 +606,7 @@ export abstract class AbstractControl<T = any> {
current.unblurred = computed(() => !this._blurred.value)
current.dirty = computed(() => this._dirty.value)
current.pristine = computed(() => !this._dirty.value)
current.validated = computed(() => this._validated.value)

if (this._disabledFn) {
nextTick(() => {
Expand All @@ -608,6 +619,8 @@ export abstract class AbstractControl<T = any> {
})
})
}

this._watchValid()
}

private _initErrorsAndStatus() {
Expand Down Expand Up @@ -647,4 +660,40 @@ export abstract class AbstractControl<T = any> {
})
}
}

private _watchValid() {
let isChangeValidating = false
let isChanging = false

this._blurMarkedCb = () => {
if (
this.trigger === 'blur' ||
(this.trigger === 'interactions' && (!this.validated.value || !isChangeValidating))
) {
this._validate()
}

isChangeValidating = false
isChanging = false
}

watch(this._valueRef, (_, oldValue) => {
if (this.trigger === 'change') {
this._validate()
return
}

if (this.trigger === 'interactions') {
if ((!this.valid.value || !!oldValue) && !isChanging) {
isChangeValidating = true
}

if (isChangeValidating) {
this._validate()
}
}

isChanging = true
})
}
}
11 changes: 1 addition & 10 deletions packages/cdk/forms/src/models/formArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import { type ComputedRef, computed, watch, watchEffect } from 'vue'
import { type ComputedRef, computed, watchEffect } from 'vue'

import { AbstractControl } from './abstractControl'
import { type AsyncValidatorFn, type ValidateStatus, type ValidatorFn, type ValidatorOptions } from '../types'
Expand All @@ -29,7 +29,6 @@ export class FormArray<T = any> extends AbstractControl<T[]> {

this.length = computed(() => this._controls.value.length)

this._watchValid()
this._watchValue()
this._watchStatus()
this._watchBlurred()
Expand Down Expand Up @@ -127,14 +126,6 @@ export class FormArray<T = any> extends AbstractControl<T[]> {
this._controls.value = controls
}

private _watchValid() {
watch(this._valueRef, () => {
if (this.trigger === 'change') {
this._validate()
}
})
}

private _watchValue() {
watchEffect(() => {
this._valueRef.value = this.getValue()
Expand Down
9 changes: 0 additions & 9 deletions packages/cdk/forms/src/models/formControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export class FormControl<T = any> extends AbstractControl<T> {
) {
super(undefined, validatorOrOptions, asyncValidator, _initValue)

this._watchValid()
this._watchStatus()
}

Expand Down Expand Up @@ -47,14 +46,6 @@ export class FormControl<T = any> extends AbstractControl<T> {
return undefined
}

private _watchValid() {
watch(this._valueRef, () => {
if (this.trigger === 'change') {
this._validate()
}
})
}

private _watchStatus() {
watch(this._errors, errors => {
this._status.value = errors ? 'invalid' : 'valid'
Expand Down
11 changes: 1 addition & 10 deletions packages/cdk/forms/src/models/formGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import { watch, watchEffect } from 'vue'
import { watchEffect } from 'vue'

import { hasOwnProperty } from '@idux/cdk/utils'

Expand All @@ -23,7 +23,6 @@ export class FormGroup<T extends object = object> extends AbstractControl<T> {
) {
super(controls, validatorOrOptions, asyncValidator)

this._watchValid()
this._watchValue()
this._watchStatus()
this._watchBlurred()
Expand Down Expand Up @@ -106,14 +105,6 @@ export class FormGroup<T extends object = object> extends AbstractControl<T> {
this._controls.value = controls
}

private _watchValid() {
watch(this._valueRef, () => {
if (this.trigger === 'change') {
this._validate()
}
})
}

private _watchValue() {
watchEffect(() => {
this._valueRef.value = this.getValue()
Expand Down
3 changes: 2 additions & 1 deletion packages/cdk/forms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ValidateError {
}

export type ValidateErrors = Record<string, ValidateError>
export type ValidatorTrigger = 'change' | 'blur' | 'submit' | 'interactions'

export type ValidateMessageFn = (err: Omit<ValidateError, 'message'>, control: AbstractControl) => string
export type ValidateMessage = string | ValidateMessageFn | Record<string, string | ValidateMessageFn>
Expand All @@ -38,7 +39,7 @@ export interface ValidatorOptions {
disabled?: boolean | ((control: AbstractControl, initializing: boolean) => boolean)
name?: string
example?: string
trigger?: 'change' | 'blur' | 'submit'
trigger?: ValidatorTrigger
validators?: ValidatorFn | ValidatorFn[]
asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[]
}
Expand Down
14 changes: 14 additions & 0 deletions packages/components/form/demo/InteractionsTrigger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title:
zh: 配置 `trigger` 为 interactions
en: Set `trigger` as interactions
order: 0
---

## zh

内置的 `interactions` trigger具有更好的交互体验,结合了 `blur``change` 两种触发方式。

## en

Inset `interactions` trigger type provides better user experence, which combines triggers of `blur` and `change`.
66 changes: 66 additions & 0 deletions packages/components/form/demo/InteractionsTrigger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<template>
<IxForm class="demo-form" :control="formGroup">
<IxFormItem message="Please input your username!">
<IxInput control="username" prefix="user"></IxInput>
</IxFormItem>
<IxFormItem message="Please input your password, its length is 6-18!">
<IxInput control="password" prefix="lock" :type="passwordVisible ? 'text' : 'password'">
<template #suffix>
<IxIcon :name="passwordVisible ? 'eye-invisible' : 'eye'" @click="passwordVisible = !passwordVisible">
</IxIcon>
</template>
</IxInput>
</IxFormItem>
<IxFormItem messageTooltip>
<IxCheckbox control="remember">Remember me</IxCheckbox>
</IxFormItem>
<IxFormItem style="margin: 8px 0" messageTooltip>
<IxButton mode="primary" block type="submit" @click="login">Login</IxButton>
</IxFormItem>
<IxRow>
<IxCol span="12">
<a>Forgot password</a>
</IxCol>
<IxCol span="12" class="text-right">
<a>Register now!</a>
</IxCol>
</IxRow>
</IxForm>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Validators, useFormGroup } from '@idux/cdk/forms'
const { required, minLength, maxLength } = Validators
const formGroup = useFormGroup(
{
username: ['', required],
password: ['', [required, minLength(6), maxLength(18)]],
remember: [true],
},
{ trigger: 'interactions' },
)
const login = () => {
if (formGroup.valid.value) {
console.log('login', formGroup.getValue())
} else {
formGroup.markAsDirty()
}
}
const passwordVisible = ref(false)
</script>

<style lang="less" scoped>
.demo-form {
max-width: 300px;
}
.text-right {
text-align: right;
}
</style>
4 changes: 2 additions & 2 deletions packages/components/form/demo/Register.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<IxForm class="demo-form" :control="formGroup" :labelCol="labelCol" :controlCol="controlCol">
<IxFormItem label="E-mail" labelFor="email" required message="Please input a valid E-mail!">
<IxFormItem label="E-mail" labelFor="email" message="Please input a valid E-mail!">
<IxInput id="email" control="email"></IxInput>
</IxFormItem>
<IxFormItem label="Password" labelFor="password" required message="Please input your password!">
Expand Down Expand Up @@ -84,7 +84,7 @@ const noLabelControlCol = { sm: { offset: 8, span: 16 }, xs: 24 }
const { required, email } = Validators
const formGroup = useFormGroup({
email: ['', [required, email]],
email: ['', email],
password: ['', required],
confirmPassword: [
'',
Expand Down
14 changes: 10 additions & 4 deletions packages/components/form/src/composables/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,16 @@ export function useFormStatus(
if (!currControl || currControl.disabled.value) {
return undefined
}
const { trigger, dirty, blurred, status } = currControl
if ((trigger === 'change' && dirty.value) || (trigger === 'blur' && blurred.value)) {
return status.value
const { trigger, dirty, blurred, status, validated } = currControl

if (trigger === 'change') {
return dirty.value ? status.value : undefined
}

if (trigger === 'blur') {
return blurred.value ? status.value : undefined
}
return undefined

return validated.value ? status.value : undefined
})
}

0 comments on commit 16652a8

Please sign in to comment.