diff --git a/src/debouncedState.ts b/src/debouncedState.ts index d33fee9..4fe6287 100644 --- a/src/debouncedState.ts +++ b/src/debouncedState.ts @@ -13,7 +13,7 @@ type OriginalInfo = Pick, 'activated' | 'touched' | 'error' | 'ownE * The state for debounce purpose. * Changes from the original state (`$`) will be debounced. */ -export class DebouncedState, V = ValueOf> extends ValidatableState implements IState { +export class DebouncedState, V = ValueOf, SV extends V = V> extends ValidatableState implements IState { /** * The original state. @@ -136,7 +136,7 @@ export class DebouncedState, V = ValueOf> extends Validat * The field state with debounce. * Value changes from `onChange` will be debounced. */ -export class DebouncedFieldState extends DebouncedState, V> { +export class DebouncedFieldState extends DebouncedState, V, SV> { constructor(initialValue: V, delay = defaultDelay) { super(new FieldState(initialValue), delay) } diff --git a/src/fieldState.ts b/src/fieldState.ts index 5e46a00..9683a0c 100644 --- a/src/fieldState.ts +++ b/src/fieldState.ts @@ -5,7 +5,7 @@ import { ValidatableState } from './state' /** * The state for a field. */ -export class FieldState extends ValidatableState implements IState { +export class FieldState extends ValidatableState implements IState { @observable.ref value!: V diff --git a/src/formState.ts b/src/formState.ts index 08e8ecf..a72398c 100644 --- a/src/formState.ts +++ b/src/formState.ts @@ -1,8 +1,8 @@ import { observable, computed, isObservable, action, reaction, makeObservable, override } from 'mobx' -import { IState, ValidateStatus, ValidateResult, ValueOfStatesObject } from './types' +import { IState, ValidateStatus, ValidateResult, ValueOfStatesObject, SafeValueOfStatesObject } from './types' import { ValidatableState } from './state' -abstract class AbstractFormState extends ValidatableState implements IState { +abstract class AbstractFormState extends ValidatableState implements IState { /** Reference of child states. */ abstract readonly $: T @@ -66,7 +66,7 @@ abstract class AbstractFormState extends ValidatableState implements IS this.resetChildStates() } - override async validate(): Promise> { + override async validate(): Promise> { if (this.disabled) { return this.validateResult } @@ -113,7 +113,7 @@ export type StatesObject = { [key: string]: IState } export class FormState< TStates extends StatesObject > extends AbstractFormState< - TStates, ValueOfStatesObject + TStates, ValueOfStatesObject, SafeValueOfStatesObject > { @observable.ref readonly $: Readonly @@ -171,9 +171,9 @@ export class FormState< * The state for a array form (list of child states). */ export class ArrayFormState< - V, T extends IState = IState + V, SV extends V, T extends IState = IState > extends AbstractFormState< - readonly T[], V[] + readonly T[], V[], SV[] > { @observable.ref protected childStates: T[] diff --git a/src/index.spec.ts b/src/index.spec.ts index d638f3a..fed0175 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -126,3 +126,41 @@ describe('Composition', () => { expect(hostState.error).toBe('empty hostname') }) }) + +interface HostInput { + hostname: string | null + port: number | null +} + +function parseHost(input: string): HostInput { + const [hostname, portStr] = input.split(':') + const port = parseInt(portStr, 10) + return { hostname, port } +} + +function stringifyHost(host: HostInput) { + const suffix = (host.port == null || Number.isNaN(host.port)) ? '' : `:${host.port}` + return host.hostname + suffix +} + +function createDebouncedHostState(hostStr: string) { + const host = parseHost(hostStr) + const hostnameState = new FieldState(host.hostname).withValidator( + v => !v && 'empty hostname' + ) + const portState = new FieldState(host.port) + const rawState = new FormState({ + hostname: hostnameState, + port: portState + }) + const state = new DebouncedState( + new TransformedState(rawState, stringifyHost, parseHost), + defaultDelay + ).withValidator( + v => !v && 'empty' + ) + + state.safeValue + + return state +} \ No newline at end of file diff --git a/src/state.ts b/src/state.ts index c104caf..54fba6d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,5 +1,5 @@ import { action, autorun, computed, makeObservable, observable, when } from 'mobx' -import { ValidationError, IState, Validation, ValidateResult, ValidateStatus, Validator } from './types' +import { ValidationError, IState, Validation, ValidateResult, ValidateStatus, Validator, WithSafeValue } from './types' import Disposable from './disposable' import { applyValidators, isValid, isPromiseLike } from './utils' @@ -37,13 +37,18 @@ export abstract class BaseState extends Disposable implements Pick< } /** Extraction for State's validating logic */ -export abstract class ValidatableState extends BaseState implements IState { +export abstract class ValidatableState extends BaseState implements IState { abstract value: V abstract touched: boolean abstract onChange(value: V): void abstract set(value: V): void + @computed get safeValue(): SV { + if (!this.validated || this.hasError) throw new Error('TODO') + return this.value as SV + } + /** The original validate status (regardless of `validationDisabled`) */ @observable protected _validateStatus: ValidateStatus = ValidateStatus.NotValidated @@ -76,9 +81,9 @@ export abstract class ValidatableState extends BaseState implements IState /** List of validator functions. */ @observable.shallow private validatorList: Validator[] = [] - @action withValidator(...validators: Validator[]) { + @action withValidator(...validators: Validator[]): WithSafeValue { this.validatorList.push(...validators) - return this + return this as WithSafeValue } /** Current validation info. */ @@ -108,15 +113,15 @@ export abstract class ValidatableState extends BaseState implements IState })() } - @computed protected get validateResult(): ValidateResult { + @computed protected get validateResult(): ValidateResult { return ( this.error - ? { hasError: true, error: this.error } as const - : { hasError: false, value: this.value } as const + ? { hasError: true, error: this.error } + : { hasError: false, value: this.value as SV } ) } - async validate(): Promise> { + async validate(): Promise> { if (this.disabled) { return this.validateResult } diff --git a/src/transformedState.ts b/src/transformedState.ts index 4528687..60f870e 100644 --- a/src/transformedState.ts +++ b/src/transformedState.ts @@ -1,8 +1,8 @@ import { computed } from 'mobx' import { BaseState } from './state' -import { IState, Validator, ValueOf } from './types' +import { IState, Validator, ValueOf, WithSafeValue } from './types' -export class TransformedState, V, $V = ValueOf> extends BaseState implements IState { +export class TransformedState, V, $V = ValueOf, SV extends V = V> extends BaseState implements IState { /** The original state, whose value will be transformed. */ public $: S @@ -23,6 +23,11 @@ export class TransformedState, V, $V = ValueOf> extends return this.parseOriginalValue(this.$.value) } + @computed get safeValue() { + if (!this.validated || this.hasError) throw new Error('TODO') + return this.value as SV + } + @computed get ownError() { return this.$.ownError } @@ -46,7 +51,7 @@ export class TransformedState, V, $V = ValueOf> extends async validate() { const result = await this.$.validate() if (result.hasError) return result - return { ...result, value: this.value } + return { ...result, value: this.value as SV } } set(value: V) { @@ -61,12 +66,12 @@ export class TransformedState, V, $V = ValueOf> extends this.$.reset() } - withValidator(...validators: Array>) { + withValidator(...validators: Array>): WithSafeValue { const rawValidators = validators.map(validator => ( (rawValue: $V) => validator(this.parseOriginalValue(rawValue)) )) this.$.withValidator(...rawValidators) - return this + return this as WithSafeValue } disableWhen(predict: () => boolean) { diff --git a/src/types.ts b/src/types.ts index e8530ce..2883124 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,9 +25,10 @@ export type ValidateResultWithValue = { hasError: false, value: T } export type ValidateResult = ValidateResultWithError | ValidateResultWithValue /** interface for State */ -export interface IState { +export interface IState { /** Value in the state. */ - value: V + value: Value + safeValue: SafeValue /** If value has been touched. */ touched: boolean /** The error info of validation. */ @@ -50,15 +51,15 @@ export interface IState { */ validated: boolean /** Fire a validation behavior. */ - validate(): Promise> + validate(): Promise> /** Set `value` on change event. */ - onChange(value: V): void + onChange(value: Value): void /** Set `value` imperatively. */ - set(value: V): void + set(value: Value): void /** Reset to initial status. */ reset(): void /** Append validator(s). */ - withValidator(...validators: Array>): this + withValidator(...validators: Array>): WithSafeValue /** * Configure when state should be disabled, which means: * - corresponding UI is invisible or disabled @@ -71,6 +72,13 @@ export interface IState { dispose(): void } +/** Safe Value of `IState` */ +export type SafeValueOf = S extends IState ? S['safeValue'] : never + +export type WithSafeValue = S & { + safeValue: SV +} + /** Function to do dispose. */ export interface Disposer { (): void @@ -81,6 +89,11 @@ export type ValueOfStatesObject = { [K in keyof StatesObject]: ValueOf } +/** Safe Value of states object. */ +export type SafeValueOfStatesObject = { + [K in keyof StatesObject]: SafeValueOf +} + /** Value of `IState` */ export type ValueOf = S extends IState ? V : never