qiniu / formstate-x

Manage state of form with ease.
https://qiniu.github.io/formstate-x
MIT License
34 stars 10 forks source link

value transform inside of state #49

Closed nighca closed 2 years ago

nighca commented 3 years ago

Background

In many cases, the business value of an input is different from the view value. For example:

type Value = number // this is type of business concept `uid`
type State = FieldState<string> // this is type of form state of component `UidInput`

// we construct form state for `UidInput` from `Value`-typed input
export function createState(uid?: Value) {
  return new FieldState(uid != null ? (uid + '') : '')
}

// we get `Value`-typed output from form state for outside usage
export function getValue(state: State): Value | undefined {
  const parsed = parseInt(state.value, 10)
  return Number.isNaN(parsed) ? undefined : uid
}

The above code shows a typical practice pattern we follow: export createState, getValue & the input component (UidInput) together. Then the usage code may look like this:

import UidInput, * as uidInput from 'UidInput'

function createFormState(initialValue) {
  return new FormState({
    uid: uidInput.createState(initialValue.uid)
  })
}

function handleSubmit(formState) {
  const value = {
    uid: uidInput.getValue(formState.$.uid)
  }
}

It works well in most cases. However there are a few drawbacks:

  1. It is contagious: if UidInput provides getValue, inputs based on UidInput have to provide getValue to get correct business value
  2. It's not possible for parent inputs to set value without knowing details about the view value
  3. It's not convenient for validators
  4. It introduces complex API for input component (the less the better)

Proposal

Consider to integrate getValue (and the opposite) logic in form state (FieldState / FormState), so that we can get business value directly from state.value:

Check the updated proposal here.

nighca commented 2 years ago

Updated Proposal:

By introducing a new state class ProxyState, we wrap original state (FieldState or FormState) with transform logic, and get a new state instance. For example:

// type of business value
type Uid = number | undefined

// type of view value
type UidInput = string

// transform from view value to business value
function parseUid(uidStr: UidInput): Uid {
  const num = parseInt(uidStr, 10)
  return Number.isNaN(num) ? undefined : num
}

// transform from business value to view value
function stringifyUid(uid: Uid): UidInput {
  return uid == null ? '' : (uid + '')
}

export function createUidState() {
  const rawState = new FieldState('') // the `rawState` is for view-binding, we will access it with `state.$`
  const state = new ProxyState(rawState, parseUid, stringifyUid) // the `state` is for business usage (parent input, the form...)
  return state
}

function createFormState() {
  return new FormState({
    uid: createUidState()
  })
}

type FormState = ReturnType<typeof createFormState>

function handleSubmit(formState: FormState) {
  const formValue = formState.value // { uid: Uid }
}

Additionally, we can do this based on FormState instance, too.

nighca commented 2 years ago

考虑 https://github.com/qbox/www/blob/43970e15ee6e64565fbecaa5131e1667014f63ce/front/2020/components/pages/activity/detail/Banner/Modal/PhoneInfoInput.tsx 这样的场景,给外边的 value 只是部分信息,转回来的时候可能导致信息丢失

nighca commented 2 years ago

closed by #56

nighca commented 2 years ago

reopen for https://github.com/qiniu/formstate-x/issues/49#issuecomment-979100366

nighca commented 2 years ago

考虑 https://github.com/qbox/www/blob/43970e15ee6e64565fbecaa5131e1667014f63ce/front/2020/components/pages/activity/detail/Banner/Modal/PhoneInfoInput.tsx 这样的场景,给外边的 value 只是部分信息,转回来的时候可能导致信息丢失

For information which exists in view value, while does not exist in business value. To avoid losing it, we can read it from the view state when transforming from business value to view value:

export function createFullNameState() {
  const state = new FormState({
    first: new FieldState('').validators(required),
    mid: new FieldState(''),
    last: new FieldState('').validators(required)
  })
  return new ProxyState(
    state,
    ({ first, last }) => `${first} ${last}`, // `mid` is not included in the final full name
    fullName => {
      const mid = state.$.mid.value // so we read from the state `mid` to keep it
      const [first, last] = fullName.split(' ')
      return { first, mid, last }
    }
  )
}
nighca commented 2 years ago

Related solutions: