Skip to content

Commit

Permalink
sv
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Apr 5, 2022
1 parent 1c1fe31 commit 78c70be
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 28 deletions.
4 changes: 2 additions & 2 deletions src/debouncedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type OriginalInfo<V> = Pick<IState<V>, 'activated' | 'touched' | 'error' | 'ownE
* The state for debounce purpose.
* Changes from the original state (`$`) will be debounced.
*/
export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends ValidatableState<V> implements IState<V> {
export class DebouncedState<S extends IState<V>, V = ValueOf<S>, SV extends V = V> extends ValidatableState<V, SV> implements IState<V, SV> {

/**
* The original state.
Expand Down Expand Up @@ -136,7 +136,7 @@ export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends Validat
* The field state with debounce.
* Value changes from `onChange` will be debounced.
*/
export class DebouncedFieldState<V> extends DebouncedState<FieldState<V>, V> {
export class DebouncedFieldState<V, SV extends V = V> extends DebouncedState<FieldState<V, SV>, V, SV> {
constructor(initialValue: V, delay = defaultDelay) {
super(new FieldState(initialValue), delay)
}
Expand Down
2 changes: 1 addition & 1 deletion src/fieldState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ValidatableState } from './state'
/**
* The state for a field.
*/
export class FieldState<V> extends ValidatableState<V> implements IState<V> {
export class FieldState<V, SV extends V = V> extends ValidatableState<V, SV> implements IState<V, SV> {

@observable.ref value!: V

Expand Down
12 changes: 6 additions & 6 deletions src/formState.ts
Original file line number Diff line number Diff line change
@@ -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<T, V> extends ValidatableState<V> implements IState<V> {
abstract class AbstractFormState<T, V, SV extends V = V> extends ValidatableState<V, SV> implements IState<V, SV> {

/** Reference of child states. */
abstract readonly $: T
Expand Down Expand Up @@ -66,7 +66,7 @@ abstract class AbstractFormState<T, V> extends ValidatableState<V> implements IS
this.resetChildStates()
}

override async validate(): Promise<ValidateResult<V>> {
override async validate(): Promise<ValidateResult<SV>> {
if (this.disabled) {
return this.validateResult
}
Expand Down Expand Up @@ -113,7 +113,7 @@ export type StatesObject = { [key: string]: IState }
export class FormState<
TStates extends StatesObject
> extends AbstractFormState<
TStates, ValueOfStatesObject<TStates>
TStates, ValueOfStatesObject<TStates>, SafeValueOfStatesObject<TStates>
> {

@observable.ref readonly $: Readonly<TStates>
Expand Down Expand Up @@ -171,9 +171,9 @@ export class FormState<
* The state for a array form (list of child states).
*/
export class ArrayFormState<
V, T extends IState<V> = IState<V>
V, SV extends V, T extends IState<V, SV> = IState<V, SV>
> extends AbstractFormState<
readonly T[], V[]
readonly T[], V[], SV[]
> {

@observable.ref protected childStates: T[]
Expand Down
38 changes: 38 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(host.hostname).withValidator<string>(
v => !v && 'empty hostname'
)
const portState = new FieldState<number | null, number>(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
}
21 changes: 13 additions & 8 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -37,13 +37,18 @@ export abstract class BaseState extends Disposable implements Pick<
}

/** Extraction for State's validating logic */
export abstract class ValidatableState<V> extends BaseState implements IState<V> {
export abstract class ValidatableState<V, SV extends V> extends BaseState implements IState<V, SV> {

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

Expand Down Expand Up @@ -76,9 +81,9 @@ export abstract class ValidatableState<V> extends BaseState implements IState<V>
/** List of validator functions. */
@observable.shallow private validatorList: Validator<V>[] = []

@action withValidator(...validators: Validator<V>[]) {
@action withValidator<NSV = SV>(...validators: Validator<V>[]): WithSafeValue<this, NSV> {
this.validatorList.push(...validators)
return this
return this as WithSafeValue<this, NSV>
}

/** Current validation info. */
Expand Down Expand Up @@ -108,15 +113,15 @@ export abstract class ValidatableState<V> extends BaseState implements IState<V>
})()
}

@computed protected get validateResult(): ValidateResult<V> {
@computed protected get validateResult(): ValidateResult<SV> {
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<ValidateResult<V>> {
async validate(): Promise<ValidateResult<SV>> {
if (this.disabled) {
return this.validateResult
}
Expand Down
15 changes: 10 additions & 5 deletions src/transformedState.ts
Original file line number Diff line number Diff line change
@@ -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<S extends IState<$V>, V, $V = ValueOf<S>> extends BaseState implements IState<V> {
export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>, SV extends V = V> extends BaseState implements IState<V, SV> {

/** The original state, whose value will be transformed. */
public $: S
Expand All @@ -23,6 +23,11 @@ export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> 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
}
Expand All @@ -46,7 +51,7 @@ export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> 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) {
Expand All @@ -61,12 +66,12 @@ export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> extends
this.$.reset()
}

withValidator(...validators: Array<Validator<V>>) {
withValidator<NSV = SV>(...validators: Array<Validator<V>>): WithSafeValue<this, NSV> {
const rawValidators = validators.map(validator => (
(rawValue: $V) => validator(this.parseOriginalValue(rawValue))
))
this.$.withValidator(...rawValidators)
return this
return this as WithSafeValue<this, NSV>
}

disableWhen(predict: () => boolean) {
Expand Down
25 changes: 19 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export type ValidateResultWithValue<T> = { hasError: false, value: T }
export type ValidateResult<T> = ValidateResultWithError | ValidateResultWithValue<T>

/** interface for State */
export interface IState<V = unknown> {
export interface IState<Value = unknown, SafeValue extends Value = Value> {
/** Value in the state. */
value: V
value: Value
safeValue: SafeValue
/** If value has been touched. */
touched: boolean
/** The error info of validation. */
Expand All @@ -50,15 +51,15 @@ export interface IState<V = unknown> {
*/
validated: boolean
/** Fire a validation behavior. */
validate(): Promise<ValidateResult<V>>
validate(): Promise<ValidateResult<this['safeValue']>>
/** 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<Validator<V>>): this
withValidator<NSV = SafeValue>(...validators: Array<Validator<Value>>): WithSafeValue<this, NSV>
/**
* Configure when state should be disabled, which means:
* - corresponding UI is invisible or disabled
Expand All @@ -71,6 +72,13 @@ export interface IState<V = unknown> {
dispose(): void
}

/** Safe Value of `IState` */
export type SafeValueOf<S> = S extends IState ? S['safeValue'] : never

export type WithSafeValue<S extends IState, SV> = S & {
safeValue: SV
}

/** Function to do dispose. */
export interface Disposer {
(): void
Expand All @@ -81,6 +89,11 @@ export type ValueOfStatesObject<StatesObject> = {
[K in keyof StatesObject]: ValueOf<StatesObject[K]>
}

/** Safe Value of states object. */
export type SafeValueOfStatesObject<StatesObject> = {
[K in keyof StatesObject]: SafeValueOf<StatesObject[K]>
}

/** Value of `IState` */
export type ValueOf<S> = S extends IState<infer V> ? V : never

Expand Down

0 comments on commit 78c70be

Please sign in to comment.