Skip to content

Commit

Permalink
Provide error info along with hasError in return value of method vali…
Browse files Browse the repository at this point in the history
…date (#25)

* add error in validate result

* use strict instead of strictNullCheck

* upgrade deps
  • Loading branch information
nighca authored Jun 10, 2020
1 parent a529e29 commit 9748eda
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 54 deletions.
56 changes: 19 additions & 37 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@types/jest": "^25.2.1",
"@types/node": "^12.7.12",
"jest": "^25.2.7",
"mobx": "^5.14.0",
"mobx": "^5.15.4",
"ts-jest": "^25.3.1",
"typedoc": "^0.15.0",
"typedoc-twilio-theme": "^1.0.0",
Expand Down
19 changes: 18 additions & 1 deletion src/fieldState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { when, observable, runInAction } from 'mobx'
import FieldState from './fieldState'
import { ValidateResultWithError, ValidateResultWithValue } from './types'

const defaultDelay = 10
const stableDelay = defaultDelay * 3 // [onChange debounce] + [async validate] + [buffer]
Expand Down Expand Up @@ -177,14 +178,30 @@ describe('FieldState validation', () => {

it('should work well with validate()', async () => {
const state = createFieldState('').validators(val => !val && 'empty')
state.validate()
const validateRet1 = state.validate()

await delay()
expect(state.validating).toBe(false)
expect(state.validated).toBe(true)
expect(state.hasError).toBe(true)
expect(state.error).toBe('empty')

const validateResult1 = await validateRet1
expect(validateResult1.hasError).toBe(true)
expect((validateResult1 as ValidateResultWithError).error).toBe('empty')

state.onChange('sth')
const validateRet2 = state.validate()
await delay()
expect(state.validating).toBe(false)
expect(state.validated).toBe(true)
expect(state.hasError).toBe(false)
expect(state.error).toBeUndefined()

const validateResult2 = await validateRet2
expect(validateResult2.hasError).toBe(false)
expect((validateResult2 as ValidateResultWithValue<string>).value).toBe('sth')

state.dispose()
})

Expand Down
14 changes: 7 additions & 7 deletions src/fieldState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { observable, computed, action, reaction, autorun, runInAction, when } from 'mobx'
import { ComposibleValidatable, Validator, Validated, ValidationResponse, ValidateStatus } from './types'
import { ComposibleValidatable, Validator, Validated, ValidationResponse, ValidateStatus, Error, ValidateResult } from './types'
import { applyValidators, debounce, isPromiseLike } from './utils'
import Disposable from './disposable'

Expand All @@ -25,18 +25,18 @@ export default class FieldState<TValue> extends Disposable implements Composible
* Value that reacts to `onChange` immediately.
* You should only use it to bind with UI input componnet.
*/
@observable.ref _value: TValue
@observable.ref _value!: TValue

/**
* Value that can be consumed by your code.
* It's synced from `_value` with debounce of 200ms.
*/
@observable.ref value: TValue
@observable.ref value!: TValue

/**
* Value that has bean validated with no error, AKA "safe".
*/
@observable.ref $: TValue
@observable.ref $!: TValue

/**
* The validate status.
Expand All @@ -53,7 +53,7 @@ export default class FieldState<TValue> extends Disposable implements Composible
/**
* The original error info of validation.
*/
@observable _error?: string
@observable _error: Error

/**
* The error info of validation.
Expand Down Expand Up @@ -151,7 +151,7 @@ export default class FieldState<TValue> extends Disposable implements Composible
/**
* Fire a validation behavior.
*/
async validate() {
async validate(): Promise<ValidateResult<TValue>> {
const validation = this.validation

runInAction('activate-and-sync-_value-when-validate', () => {
Expand All @@ -174,7 +174,7 @@ export default class FieldState<TValue> extends Disposable implements Composible

return (
this.hasError
? { hasError: true } as const
? { hasError: true, error: this.error } as const
: { hasError: false, value: this.value } as const
)
}
Expand Down
23 changes: 22 additions & 1 deletion src/formState.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { observable, runInAction, isObservable } from 'mobx'
import FieldState from './fieldState'
import FormState from './formState'
import { ValidateResultWithError, ValidateResultWithValue } from './types'

const defaultDelay = 10
const stableDelay = defaultDelay * 3 // [onChange debounce] + [async validate] + [buffer]
Expand Down Expand Up @@ -132,14 +133,34 @@ describe('FormState (mode: object) validation', () => {
bar: createFieldState(initialValue.bar)
}).validators(({ foo, bar }) => foo === bar && 'same')

state.validate()
const validateRet1 = state.validate()

await delay()
expect(state.validating).toBe(false)
expect(state.validated).toBe(true)
expect(state.hasError).toBe(true)
expect(state.error).toBe('same')

const validateResult1 = await validateRet1
expect(validateResult1.hasError).toBe(true)
expect((validateResult1 as ValidateResultWithError).error).toBe('same')

state.$.bar.onChange('456')
const validateRet2 = state.validate()

await delay()
expect(state.validating).toBe(false)
expect(state.validated).toBe(true)
expect(state.hasError).toBe(false)
expect(state.error).toBeUndefined()

const validateResult2 = await validateRet2
expect(validateResult2.hasError).toBe(false)
expect((validateResult2 as ValidateResultWithValue<typeof initialValue>).value).toEqual({
foo: '123',
bar: '456'
})

state.dispose()
})

Expand Down
8 changes: 4 additions & 4 deletions src/formState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { observable, computed, isArrayLike, isObservable, action, autorun, runInAction, when, reaction } from 'mobx'
import { ComposibleValidatable, ValueOfFields, ValidationResponse, Validator, Validated, ValidateStatus } from './types'
import { ComposibleValidatable, ValueOfFields, ValidationResponse, Validator, Validated, ValidateStatus, Error, ValidateResult } from './types'
import { applyValidators, isPromiseLike } from './utils'
import Disposable from './disposable'

Expand Down Expand Up @@ -94,7 +94,7 @@ export default class FormState<TFields extends ValidatableFields, TValue = Value
/**
* The error info of form validation.
*/
@observable private _error?: string
@observable private _error: Error

/**
* The error info of validation (including fields' error info).
Expand Down Expand Up @@ -201,7 +201,7 @@ export default class FormState<TFields extends ValidatableFields, TValue = Value
/**
* Fire a validation behavior.
*/
async validate() {
async validate(): Promise<ValidateResult<TValue>> {
runInAction('activate-when-validate', () => {
this._activated = true
})
Expand All @@ -219,7 +219,7 @@ export default class FormState<TFields extends ValidatableFields, TValue = Value

return (
this.hasError
? { hasError: true } as const
? { hasError: true, error: this.error } as const
: { hasError: false, value: this.value } as const
)
}
Expand Down
10 changes: 8 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@ export interface Validator<TValue> {
(value: TValue): ValidatorResponse
}

export type Error = string | undefined

export type ValidateResultWithError = { hasError: true, error: Error }
export type ValidateResultWithValue<T> = { hasError: false, value: T }
export type ValidateResult<T> = ValidateResultWithError | ValidateResultWithValue<T>

/** Validatable object. */
export interface Validatable<T, TValue = T> {
$: T
value: TValue
hasError: boolean
error?: string | null | undefined
error: Error
validating: boolean
validated: boolean
validationDisabled: boolean
validate(): Promise<{ hasError: true } | { hasError: false, value: TValue }>
validate(): Promise<ValidateResult<TValue>>

// To see if there are requirements: enableAutoValidation, disableAutoValidation
// enableAutoValidation: () => void
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"sourceMap": true,
"esModuleInterop": true,
"moduleResolution": "node",
"strictNullChecks": true,
"strict": true,
"experimentalDecorators": true,
"outDir": "./lib",
"lib": ["es2015"]
Expand Down

0 comments on commit 9748eda

Please sign in to comment.