qiniu / formstate-x

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

support `formState.set` #19

Closed lzfee0227 closed 3 years ago

lzfee0227 commented 4 years ago

demo

createXxxFields(list) {
  return list.map(value => new FieldState(value))
}

createFields({ a, b }) { // 叫 fields 不太对,叫 states 又怪怪的… 先不纠结
  return {
    a: new FieldState(a),
    b: new FormState(b, createXxxFields)
  }
}

createState(initValue) {
  return new FormState(initValue, createFields)
}

state = createState({ a: 1, b: [2, 3] })

+ formstate.reset(value) & fieldstate.reset(value)

@nighca

nighca commented 4 years ago

几个问题可以考虑下:

  1. 每个 form state 的构造都用 new FormState(initValue, createFields) 的写法吗,是的话比较繁琐;否的话,看起来是数组类型的 form state 才需要?它有传染性吗(传染到自己的父 form state)

  2. 对于一个 form state,原本里边装的值是 [0, 1],做了一次 set([1, 2, 3]) 的操作之后,再执行 reset(),它应该是 reset 到 [0, 1],还是 [1, 2, 3],还是 [0, 1, 3]

  3. 对于输入组件进行组合的情况,各自的 createState 是怎么去写的?这边我主要担心父组件在使用子组件的时候,向其构造的 state 后边追加 validator 的情况,这种情况下,set 产生的新 state 是不是会丢失这些外部追加的 validator

lzfee0227 commented 4 years ago

我趁机稍微整理了一下,包括之前的一些想法,记录一下,免得后续又忘了或重新回忆……

  1. 每个 form state 的构造都用 new FormState(initValue, createFields) 的写法吗,是的话比较繁琐;否的话,看起来是数组类型的 form state 才需要?它有传染性吗(传染到自己的父 form state)

1、createFields 是可选的,如果不填,用 formstate 的 set 可以考虑报错或有个默认行为什么的; 如果认为 initValue 有歧义,也可以换一下接口形式, 其实我一开始想的是 FormState.create 之类的接口,直接不复用现在的 new 了, 这里只是个 demo 而已,不是最终 api,应该影响不大的

2、跟是否数组类型无关,各种类型的 formstate(即非 fieldstate,fieldstate 比较简单先不考虑) 都一样的, 我只是举了个数组的 demo 而已(只因数组更不常见), demo 里的 createFields 就是一个非数组的 case 啊,只不过恰好在最外层而已

3、也跟传染性无关, 每个 formstate 有没有自己的 createFields 是它自己的事(有点像你目前实现的 disableValidationWhen 上下文不干扰), 这里只是简单的嵌套组合使用而已; 当然如果要在父的 formstate 上调用 set, 那么子的 formstate 也需要定义自己的 createFields,这个同样可以运行时检测 这算是“对子表单的传染性”吗?如果算,我觉得问题不大 在不调用父表单的 set 之前,不会有任何限制, 两种形式之间也很容易改造,以后全部都用这种形式我觉得也未尝不可

4、只稍微繁琐了一点点,在我看来其实几乎只有形式上的差异了,代码量也没差; 相比之下,我会设想如果 formstate 有了 set 的能力, 也许我以后就只需要声明 IValue (现 getValue 的返回值类型) 而不需要声明 formstate 的类型了…… 这个才是真·繁琐 因为我觉得现在 createState 里的实现内容已经是接近于声明性质的了,又往往是某个局部使用的,重复声明 interface 再去实现它我觉得没什么好处…… 其次我还是那个观点,IValue 跟 formstate 的结构一一对应, 只存在在外部的 **Value 跟 IValue 之间的转换,而不是在 createState 内部转换

2. 对于一个 form state,原本里边装的值是 [0, 1],做了一次 set([1, 2, 3]) 的操作之后,再执行 reset(),它应该是 reset 到 [0, 1],还是 [1, 2, 3],还是 [0, 1, 3]

按照目前的设想,应该是 [0, 1],用自个儿的 createFields 直接重新生成, 这会导致什么问题吗?

不过这跟另外一个事情有关,就是为什么我问你 set 是否会重置成未激活, 就 formstate (不是 formstate-x) 使用 reset(value) 的经验而言,我也许更希望的 set 是重置成未激活, 举个例子,我也许需要 set 成空值,但暂时不触发校验,直到用户重新经过那个表单 这时候在 reset 支持传值之前,我就只能整个 formstate 重新 create? 而如果默认行为是重置成未激活,需要激活我调一下 validate 方法就好了, 说白了就是把决定权归还给使用 set 的一方(本来使用 set 就是相对特殊的场景了), 并且 set 的时候不激活本身也没什么坏处, 而维持激活状态这个 feature 在以前的 formstate 需求里确实没遇到过

具体到这里,我也会认为, 如果 set 是一个 replace 的行为, 并且传入的是数据值、数据结构前后就是预期可能有比较大的差异的 (而不是一个小小的直接比较引用的 fieldstate 了), 那么维持原来的状态是没必要的(主要就是校验状态了) 设想一下从 [0, 1] 通过 set([0, 2]) 变成 [0, 2] 虽然只有后面的 1 变成了 2, 但是我们无法断定前面的 0 是否还是上次那个 0 了, 可能只是恰好值相等而已,外面的数组应该认为是整个新数组(而跟上次的数据无关), (还可以假设 0 是个更复杂的数据结构并且引用都跟上次相等,性质还是不变的), 就算校验结果都是 不能为 0,也不太合适再次展示了; 或者说 如果是局部变化而不是整个变化, 那我就不会使用 formstate 上的 set 接口了,而可能是 formstate.$.splice(1)formstate.$[1].set 了, 这时候上次的 0 就真的是上次的 0

从这一点上看我还是觉得 set 跟 reset 的区别应该只有 initValue 而已… 另外如果 fieldstate 的值原本是 0 然后我调用了 fieldstate.set(0) 虽然值 / 引用相等,但是校验状态被重置了,我觉得这也是很合理的 也跟我期望的 formstate 的 set 行为保持一致了

3. 对于输入组件进行组合的情况,各自的 createState 是怎么去写的?这边我主要担心父组件在使用子组件的时候,向其构造的 state 后边追加 validator 的情况,这种情况下,set 产生的新 state 是不是会丢失这些外部追加的 validator

1、我个人觉得追加的行为也应该在 createState 完成时完成追加操作, 而不是随意的在后续外部追加(现在遇到的需求也都是),所以应该还好, 就我还是倾向于那个观点,createState 里更像是声明,应该声明所有相关的东西, 而不是把部分 validator 散落在太远的地方(目前也没有这个需求), 容易出问题(如我就见过有人重复追加) 另外其实就算没有追加这功能,我个人也觉得还好…

2、虽然我还没细想,但是就之前遇到的复杂情况镜像回源随手写个 demo 吧,后续想清楚了再补充一个完整的 shape 的 demo @nighca

export function createLineState(initLine, getLinesState, getHostState) {
  return new FormState(initLine, ({ host }) => ({
    host: (
      new FieldState(host)

        .validators(/* current line vs other lines */)
        .validators(/* current line vs host */)
    )
  }))
}

export function createState(initValue?) {
  return new FormState(initValue, ({ host, lines }) => {
    // 套用解决循环依赖的声明套路,同时在 formstate tree 相对顶层的地方声明了相互依赖的 part,类型也是对的
    // 只是不能自由地追加 validators 了,但是如果追加的代码反正都是要在这附近实现的,那么我觉得现在这样也没差,真正复杂的地方并没有因此变得更加复杂
    // 追加 validators 太 “过程式” 了,我还是喜欢现在这样更 “声明式” 
    // 如果把这两个 get 方法实现得再精确一点、不直接返回 state 而只返回需要的 value 就更好了
    // 相当于是只把收集依赖 + 提取公共信息放在这个公共顶层(从而不依赖 or 耦合于数据源的内部结构 or 实现,甚至可以不是隔壁 fields 而是某个 global***Store 也说不定呢)
    // 而把具体部分(lines)相关的提取数据+校验逻辑分别实现在各自(如 createLineState)内部、更内聚
    // createLineState 就真的可以独立使用并且不依存于所属的 formstate 结构了,而不是纯粹的代码拆分的手段
    const getHostState = () => hostState
    const getLinesState = () => linesState

    const hostState = new FieldState(host).validators(host => getLinesState().$.forEach(/***/))

    const linesState = new FormState(
      lines,
      lines => lines.map(line => createLineState(line, getLinesState, getHostState))
    ).validators(/***/) // TODO: formstate 上的这种 validators 好说,先不展开

    return { host: hostState, lines: linesState }
  })
}

class {
  formState = createState()

  addLineState(initLine?) {
    const main = this.formState.$
    main.lines.$.push(
      createLineState(initLine, () => main.lines, () => main.host)
    )
  }
}

至于更换 formState.$.lines 和 formState.$.host 现在就不讨论了(我觉得也可以记个 TODO) 之前也不支持这种……

此外 目前如果需要重置 reset formstate 初始 initValue 的时候 就是可能要重新 create 整个 formstate 的话 那么在这个场景也是要考虑重新构筑所有 validators 的 (甚至可能还需要保留上次的某些值、某些校验状态和某些已经追加进去的校验函数,细节先不考究) (上面的 addLineState 其实也是类似情形,但是要简单些,只靠拆代码的手段就绕过去了) 所以几乎可以认为现在的 createState 套路对具体实现者而言 就是有可能面临要肩负 一次把所有包括 validators 在内的东西重新 create 出来的职责的… 如果都是会收拢到 createState 里、足够内聚的话,那么按照使用 formstate 的历史经验, 是否需要通过追加 validators 来实现某些功能我觉得已经无所谓了… 而 set 由于跟这个 reset 的场景有点像,所以个人感觉不算是增加了新的问题 只是让原本已有的问题更容易被看出来了而已…

nighca commented 4 years ago

太长了,有时间再看..

lzfee0227 commented 4 years ago

不急,怕忘了,赶紧记下来而已……

lzfee0227 commented 4 years ago

@Luncher 有空来看这个啊,你那边表单不是也賊复杂吗

nighca commented 4 years ago

当面讨论了下,这边记录下。

  1. 个 form state 的构造都用 new FormState(initValue, createFields) 的写法吗,是的话比较繁琐;否的话,看起来是数组类型的 form state 才需要?它有传染性吗(传染到自己的父 form state)

目前看起来是,凡 FormState 都需要 createFields,因为是把 new FormState(fields) 变成了 new FormState(value, value2Fields)

  1. 对于一个 form state,原本里边装的值是 [0, 1],做了一次 set([1, 2, 3]) 的操作之后,再执行 reset(),它应该是 reset 到 [0, 1],还是 [1, 2, 3],还是 [0, 1, 3]

按照目前的设想,应该是 [0, 1],用自个儿的 createFields 直接重新生成, 这会导致什么问题吗?

这边倒不是导致问题,而是这个问题的答案会比较大程度地影响 FormState 的行为;我觉得也是 reset 到 [0, 1] 比较合理,不过这个跟现在 FormState 的行为是不符的(差别还比较大),当然那是另外一个改动,跟 formState.set() 关系不大

不过这跟另外一个事情有关,就是为什么我问你 set 是否会重置成未激活, 就 formstate (不是 formstate-x) 使用 reset(value) 的经验而言,我也许更希望的 set 是重置成未激活, 举个例子,我也许需要 set 成空值,但暂时不触发校验,直到用户重新经过那个表单 这时候在 reset 支持传值之前,我就只能整个 formstate 重新 create? 而如果默认行为是重置成未激活,需要激活我调一下 validate 方法就好了, 说白了就是把决定权归还给使用 set 的一方(本来使用 set 就是相对特殊的场景了), 并且 set 的时候不激活本身也没什么坏处, 而维持激活状态这个 feature 在以前的 formstate 需求里确实没遇到过

set 不是 reset,没理由将状态重置为未激活,坏处是可能让用户以为这个是个合法值;在我看来几乎所有 set 的场景都应该保持当前的激活状态,交给外部去做反而是增加使用的负担;不过这个问题跟这个 issue 关系不太大,可以先不管。

虽然我还没细想,但是就之前遇到的复杂情况镜像回源随手写个 demo 吧,后续想清楚了再补充一个完整的 shape 的 demo

这个可能比现在你这个 demo 里的更麻烦一点;其实你可以直接在现在镜像回源的代码基础上提个 PR 试下的(不利用 .validators() 追加的特性来实现就好),这样的场景下代码本身的繁琐(或者说刻意)程度是我觉得 .validators() 追加是一个重要 feature 的主要原因。

我同意追加 validator 这个特性确实应该克制地去使用:它会越过 createStategetValue 制造的界限,引入额外的耦合(当然 formState.set() 也会),所以如果能被限制在一定范围内使用会更好。

最后还有个问题是,通过这种 createFields() 的方式来做,跟通过 createState() 完整地创建一个新的 state 相比区别很小了,看起来收益比较有限

nighca commented 3 years ago

考虑:

function createItemState(value: Value) {
  return new FieldState(value).validators(validateItem)
}

function createListState(valueList: Value[]) {
  return new ArrayFormState(valueList, createItemState).validators(validateList)
  // or
  new FormState(valueList.map(createItemState), { createItemState })
}

const fooListState = new FormState([1])
fooListState.set([2, 3])

mode: objectFormState 目测不需要这种形式

lzfee0227 commented 3 years ago

mode: objectFormState 目测不需要这种形式

加入限制:使用 set 接口的时候 object 里的 fields 不会增删