Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions dumi/docs/concepts/state/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface IState<V> {
error: ValidationError
/** The state's own error info, regardless of child states. */
ownError: ValidationError
/** The state's validation result, is the same as the return value of the validator, regardless of child states. */
rawError: ValidationResult
/** Append validator(s). */
withValidator(...validators: Array<Validator<V>>): this
/** Fire a validation behavior imperatively. */
Expand Down Expand Up @@ -67,11 +69,11 @@ Validation is the process of validating user input values.
Validation is important for cases like:

* When user inputs, we display error tips if validation not passed, so users see that and correct the input
* Before form submiiting, we check if all value is valid, so invalid requests to the server can be avoided
* Before form submitting, we check if all value is valid, so invalid requests to the server can be avoided

That's why validation should provide such features:

* It should run automatically, when users changed the value, or when some other data change influcend the value validity
* It should run automatically, when users changed the value, or when some other data change influenced the value validity
* It should produce details such as a meaningful message, so users can get friendly hint

With formstate-x, we define validators and append them to states with `withValidator`. formstate-x will do validation for us. Through `validateStatus` & `error`, we can access the validate status and result.
Expand All @@ -97,6 +99,10 @@ States will not be auto-validated until it becomes **activated**. And they will

`ownError` & `hasOwnError` are special fields especially for composed states. You can check details about them in issue [#71](https://github.com/qiniu/formstate-x/issues/71).

### Raw Error

The state's validation result, is the same as the return value of the `validator`, regardless of child states. The difference compared to `ownError` is that it contains the type of `ValidationErrorObject`. You can check details about them in issue [#82](https://github.com/qiniu/formstate-x/issues/82).
Copy link
Collaborator

@nighca nighca May 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 后边有时间的时候,最好是在文档 Guide/Advanced 里补一下对应的 case(可以不在这个 PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK..


### Disable State

You may find that we defined method `disableWhen` to configure when a state should be disabled. It is useful in some specific cases. You can check details in section [Disable State](/guide/advanced#disable-state).
4 changes: 4 additions & 0 deletions dumi/docs/concepts/validator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export type ValidationResult =
| null
| undefined
| false
| ValidationErrorObject

/** Object type validation result. */
export type ValidationErrorObject = { message: string }

/** Return value of validator. */
export type ValidatorReturned =
Expand Down
1 change: 1 addition & 0 deletions src/adapter/v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ describe('toV2', () => {
touched = false
ownError = undefined
error = undefined
rawError = undefined
activated = false
validateStatus = v3.ValidateStatus.NotValidated
async validate() {
Expand Down
22 changes: 20 additions & 2 deletions src/adapter/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as v2 from 'formstate-x-v2'
import { BaseState } from '../state'
import * as v3 from '..'
import Disposable from '../disposable'
import { isPromiseLike, normalizeError } from '../utils'

interface IV3StateFromV2<T extends v2.ComposibleValidatable<unknown, V>, V> extends v3.IState<V> {
/** The original ([email protected]) state */
Expand All @@ -27,6 +28,7 @@ class Upgrader<T extends v2.ComposibleValidatable<unknown, V>, V> extends BaseSt
@computed get ownError() {
return getV3OwnError(this.stateV2)
}
@computed get rawError() { return this.ownError }
@computed get error() { return this.stateV2.error }
@computed get activated() { return this.stateV2._activated }
@computed get validateStatus() {
Expand All @@ -47,7 +49,7 @@ class Upgrader<T extends v2.ComposibleValidatable<unknown, V>, V> extends BaseSt
isV2FieldState(this.stateV2)
|| isV2FormState(this.stateV2)
) {
this.stateV2.validators(...validators)
this.stateV2.validators(...portV2Validators(...validators))
return this
}
throwNotSupported()
Expand All @@ -64,7 +66,23 @@ class Upgrader<T extends v2.ComposibleValidatable<unknown, V>, V> extends BaseSt
}
}

/** Convets [email protected] state to [email protected] state */
function portV2Validators<V>(...validators: Array<v3.Validator<V>>): Array<v2.Validator<V>> {
const normalizeRet = (v: any) => (
normalizeError(v)
)
return validators.map(validator => {
return (value: V) => {
const returned = validator(value)
if (isPromiseLike(returned)) {
return returned.then(normalizeRet)
} else {
return normalizeRet(returned)
}
}
})
}

/** Converts [email protected] state to [email protected] state */
export function fromV2<T extends v2.ComposibleValidatable<unknown, unknown>>(stateV2: T): IV3StateFromV2<T, T['value']> {
return new Upgrader(stateV2)
}
Expand Down
28 changes: 28 additions & 0 deletions src/debouncedState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,20 @@ describe('DebouncedState validation', () => {
expect(state.error).toBe('empty')
expect(state.ownError).toBe('empty')
})

it('should work well with resolved error object', async () => {
const fooState = new FieldState('')
const formState = new FormState({ foo: fooState })
const state = new DebouncedState(formState, defaultDelay).withValidator(
() => ({ message: 'mock msg' })
)

await state.validate()
expect(state.hasError).toBe(true)
expect(state.ownError).toBe('mock msg')
expect(state.error).toBe('mock msg')
expect(state.rawError).toEqual({ message: 'mock msg' })
})
})

function createFieldState<T>(initialValue: T, delay = defaultDelay) {
Expand Down Expand Up @@ -607,4 +621,18 @@ describe('DebouncedFieldState validation', () => {
expect(validator).toBeCalled()
expect(state.validateStatus).toBe(ValidateStatus.Validated)
})

it('should work well with resolved error object', async () => {
const state = createFieldState(0).withValidator(
_ => ({ message: 'empty' })
)

state.validate()

await delay()
expect(state.hasError).toBe(true)
expect(state.error).toBe('empty')
expect(state.ownError).toBe('empty')
expect(state.rawError).toEqual({ message: 'empty' })
})
})
14 changes: 10 additions & 4 deletions src/debouncedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { action, computed, makeObservable, observable, override, reaction } from
import { FieldState } from './fieldState'
import { ValidatableState } from './state'
import { IState, ValidateStatus, ValueOf } from './types'
import { debounce } from './utils'
import { debounce, isPassed, normalizeError } from './utils'

const defaultDelay = 200 // ms

/** Infomation synced from original state */
type OriginalInfo<V> = Pick<IState<V>, 'activated' | 'touched' | 'error' | 'ownError'>
type OriginalInfo<V> = Pick<IState<V>, 'activated' | 'touched' | 'error' | 'ownError' | 'hasError'>

/**
* The state for debounce purpose.
Expand Down Expand Up @@ -42,7 +42,8 @@ export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends Validat
activated: this.$.activated,
touched: this.$.touched,
error: this.$.error,
ownError: this.$.ownError
ownError: this.$.ownError,
hasError: this.$.hasError
}
}

Expand All @@ -52,7 +53,7 @@ export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends Validat

@override override get ownError() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在实现 DebouncedState 的时候,也应该去通过 override rawError 来实现,而不是 override ownError

#87 (comment) 这里所述,ownError = normalizeError(rawError) 这个逻辑对于 DebouncedState 也应该是成立的?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

没注意这里有个 override,我看怎么改下。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

031f8a1
done

if (this.disabled) return undefined
if (this._error) return this._error
if (this.rawError) return normalizeError(this.rawError)
return this.original.ownError
}

Expand All @@ -62,6 +63,11 @@ export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends Validat
return this.original.error
}

@override override get hasError() {
if (this.disabled) return false
return !isPassed(this.rawError) || this.original.hasError
}

@override override get validateStatus() {
if (this.disabled) return ValidateStatus.WontValidate
if (
Expand Down
56 changes: 56 additions & 0 deletions src/fieldState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,35 @@ describe('FieldState', () => {
expect(state.value).toBe(initialValue)
expect(state.touched).toBe(false)
})

it('should setError well', async () => {
const initialValue = ''
const state = new FieldState(initialValue)

state.setError('')
expect(state.hasError).toBe(false)
expect(state.error).toBe(undefined)
expect(state.ownError).toBe(undefined)
expect(state.rawError).toBe('')

state.setError('123')
expect(state.hasError).toBe(true)
expect(state.error).toBe('123')
expect(state.ownError).toBe('123')
expect(state.rawError).toBe('123')

state.setError({ message: 'mewo3' })
expect(state.hasError).toBe(true)
expect(state.error).toBe('mewo3')
expect(state.ownError).toBe('mewo3')
expect(state.rawError).toEqual({ message: 'mewo3' })

state.reset()
expect(state.hasError).toBe(false)
expect(state.error).toBe(undefined)
expect(state.ownError).toBe(undefined)
expect(state.rawError).toBe(undefined)
})
})

describe('FieldState validation', () => {
Expand Down Expand Up @@ -373,4 +402,31 @@ describe('FieldState validation', () => {
assertType<string>(res.value)
}
})

describe('should work well with resolved error object', () => {
it('should work well with sync resolved', async () => {
const state = new FieldState('').withValidator(
_ => ({ message: 'error-object-msg' })
)

const res = await state.validate()
expect(state.hasError).toBe(true)
expect(state.error).toBe('error-object-msg')
expect(state.rawError).toEqual({ message: 'error-object-msg' })
expect(res).toEqual({ hasError: true, error: 'error-object-msg' })
})

it('should work well with async resolved', async () => {
const state = new FieldState('').withValidator(
_ => null,
_ => delayValue({ message: 'error-object-msg' }, 100)
)

const res = await state.validate()
expect(state.hasError).toBe(true)
expect(state.error).toBe('error-object-msg')
expect(state.rawError).toEqual({ message: 'error-object-msg' })
expect(res).toEqual({ hasError: true, error: 'error-object-msg' })
})
})
})
Loading