From 9748edaacd60c420c898ab14aab8652644f8ccde Mon Sep 17 00:00:00 2001 From: Hanxing Yang Date: Wed, 10 Jun 2020 10:57:55 +0800 Subject: [PATCH] Provide error info along with hasError in return value of method validate (#25) * add error in validate result * use strict instead of strictNullCheck * upgrade deps --- package-lock.json | 56 ++++++++++++++---------------------------- package.json | 2 +- src/fieldState.spec.ts | 19 +++++++++++++- src/fieldState.ts | 14 +++++------ src/formState.spec.ts | 23 ++++++++++++++++- src/formState.ts | 8 +++--- src/types.ts | 10 ++++++-- tsconfig.json | 2 +- 8 files changed, 80 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ccd19f..2ff3ded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1186,9 +1186,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, @@ -1816,15 +1816,16 @@ "optional": true }, "handlebars": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", - "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", "dev": true, "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" } }, "har-schema": { @@ -3066,9 +3067,9 @@ "dev": true }, "mobx": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.14.0.tgz", - "integrity": "sha512-GhDSZV9rGlCeVBpFPVYaYSn7UjgkD3158Njailp4IJhKqbT5iiEtiRyr76b7gPj3wpUSl+NHREQqBXzc1I8jpQ==", + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.4.tgz", + "integrity": "sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==", "dev": true }, "ms": { @@ -3253,24 +3254,6 @@ "mimic-fn": "^2.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -4454,14 +4437,13 @@ "dev": true }, "uglify-js": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", - "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.4.tgz", + "integrity": "sha512-8RZBJq5smLOa7KslsNsVcSH+KOXf1uDU8yqLeNuVKwmT0T3FA0ZoXlinQfRad7SDcbZZRZE4ov+2v71EnxNyCA==", "dev": true, "optional": true, "requires": { - "commander": "2.20.0", - "source-map": "~0.6.1" + "commander": "~2.20.3" } }, "underscore": { @@ -4668,9 +4650,9 @@ "dev": true }, "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index 7871555..4b3475e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/fieldState.spec.ts b/src/fieldState.spec.ts index 4dec224..57597d0 100644 --- a/src/fieldState.spec.ts +++ b/src/fieldState.spec.ts @@ -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] @@ -177,7 +178,7 @@ 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) @@ -185,6 +186,22 @@ describe('FieldState validation', () => { 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).value).toBe('sth') + state.dispose() }) diff --git a/src/fieldState.ts b/src/fieldState.ts index f603c90..be0f6bb 100644 --- a/src/fieldState.ts +++ b/src/fieldState.ts @@ -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' @@ -25,18 +25,18 @@ export default class FieldState 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. @@ -53,7 +53,7 @@ export default class FieldState extends Disposable implements Composible /** * The original error info of validation. */ - @observable _error?: string + @observable _error: Error /** * The error info of validation. @@ -151,7 +151,7 @@ export default class FieldState extends Disposable implements Composible /** * Fire a validation behavior. */ - async validate() { + async validate(): Promise> { const validation = this.validation runInAction('activate-and-sync-_value-when-validate', () => { @@ -174,7 +174,7 @@ export default class FieldState 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 ) } diff --git a/src/formState.spec.ts b/src/formState.spec.ts index c83ec26..a7f3241 100644 --- a/src/formState.spec.ts +++ b/src/formState.spec.ts @@ -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] @@ -132,7 +133,7 @@ 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) @@ -140,6 +141,26 @@ describe('FormState (mode: object) validation', () => { 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).value).toEqual({ + foo: '123', + bar: '456' + }) + state.dispose() }) diff --git a/src/formState.ts b/src/formState.ts index a9fdfb5..745a1dd 100644 --- a/src/formState.ts +++ b/src/formState.ts @@ -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' @@ -94,7 +94,7 @@ export default class FormState> { runInAction('activate-when-validate', () => { this._activated = true }) @@ -219,7 +219,7 @@ export default class FormState { (value: TValue): ValidatorResponse } +export type Error = string | undefined + +export type ValidateResultWithError = { hasError: true, error: Error } +export type ValidateResultWithValue = { hasError: false, value: T } +export type ValidateResult = ValidateResultWithError | ValidateResultWithValue + /** Validatable object. */ export interface Validatable { $: 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> // To see if there are requirements: enableAutoValidation, disableAutoValidation // enableAutoValidation: () => void diff --git a/tsconfig.json b/tsconfig.json index dac404b..4224e95 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "sourceMap": true, "esModuleInterop": true, "moduleResolution": "node", - "strictNullChecks": true, + "strict": true, "experimentalDecorators": true, "outDir": "./lib", "lib": ["es2015"]