5

我正在使用 SAFE 堆栈的 Elmish.Bridge 风格。

在视图的顶层,我创建了一个这样的输入字段:

Input.input [ Input.Value(model.FieldValue); Input.OnChange(fun e -> dispatch (EditFieldValue(e.Value))) ]

当我通过在该字段的中间键入来编辑该字段的值时,模型会按预期更新,但光标也会移动到输入文本的末尾

我的模型有好几层,完全由可序列化的类型(基元、字符串、集合、记录和联合)组成。

我尝试在玩具应用程序中重现它(模型不那么复杂),但它在那里按预期工作 - 光标保持位置。

有没有办法确定光标在这些情况下移动的原因?

4

6 回答 6

5

使用 DefaultValue 而不是 Value 应该可以解决这个问题。

这适用于 Fable.React 和 Fable.Elmish。使用 DefaultValue 不会在反应生命周期中启动组件/DOM 的更新,因此您不应该获得光标更改。

从下面的链接: 反应不受控制的组件

在 React 渲染生命周期中,表单元素的 value 属性将覆盖 DOM 中的 value。对于不受控制的组件,您通常希望 React 指定初始值,但不控制后续更新。要处理这种情况,您可以指定 defaultValue 属性而不是 value。

于 2019-12-03T09:33:29.753 回答
1

我遇到了类似的问题,结果证明是因为 React 正在删除我的 DOM 对象并重新创建它。(这反过来又是由于我的 React 元素虽然在两个代码分支中相同,但在两个不同分支中作为父元素具有不同的元素。)

如果 React 没有删除/重新创建你的 DOM 对象,你的光标应该表现正常。如果您想弄清楚是什么导致您的 DOM 被更新/创建,请在 Hooks.useEffectDisposable 调用中添加一些 printfns。在 effect 中添加一个 printfn,它会告诉您何时安装元素,而在 IDisposable 中添加另一个会告诉您何时卸载它。

使用不受控制的组件在某些情况下可以工作,但如果模型中的其他内容发生更改,它将阻止您的文本更新,例如在有人点击“确定”按钮后,您无法清除文本框。最好只是了解 React 生命周期并修复导致 React 重新渲染 DOM 的任何问题。

于 2019-12-04T23:19:16.407 回答
1

我错过了一些我不得不说的事情。

如果还有一个 textarea 标签,上述解决方法会导致焦点始终更改。

所以应该有一些逻辑来防止这种情况。

    module Works =

    type MsgX = | InputX of string * int * int
                | InputX2 of string
                | ChangeTime

    let subjectX , observableX = FSharp.Control.AsyncRx.subject<MsgX>()

    type ModelX = {
        // use timestamp to determine whether there should be setting cursor position or not.
        time : int
        input : int * string * int * int
        input2 : string
    }

    let updateX (modelX : ModelX) (msgX : MsgX) : ModelX =
        match msgX with
        | MsgX.InputX(s, start, ``end``) ->
            { modelX with
                time = modelX.time + 1;
                input = (modelX.time + 1, s, start, ``end``)
            }
        | ChangeTime -> { modelX with time = modelX.time + 1 }
        | MsgX.InputX2 s -> { modelX with input2 = s }

    type ComponentX(modelX : ModelX) as this =
        inherit Fable.React.Component<ModelX, unit>(modelX) with
        let mutable refTextArea : Option<Browser.Types.HTMLTextAreaElement> = None
        override _.render() : ReactElement =
            let _, s, _, _ = this.props.input
            div []
                [
                    textarea
                        [
                            Props.Ref(fun e -> refTextArea <- Some(e :?> Browser.Types.HTMLTextAreaElement))
                            OnChange this.OnChange
                            OnBlur this.OnBlur
                            Value s
                            Rows 10
                            Cols 40
                        ]
                        []
                    textarea
                        [
                            OnChange this.OnChange2
                            Value this.props.input2
                        ]
                        []
                ]
        override _.componentDidUpdate(_ : ModelX, _ : unit) : unit =
            refTextArea
            |> Option.filter (fun _ ->
                // determine whether there should be setting cursor position or not.
                let time, _, _, _ = this.props.input
                time = this.props.time
            )
            |> Option.iter (fun elem ->
                let _, _, start, ``end`` = this.props.input
                elem.selectionStart <- start;
                elem.selectionEnd <- ``end``
            )
        member _.OnChange(e : Browser.Types.Event) : unit =
            let target = e.target :?> Browser.Types.HTMLTextAreaElement
            let x = target.value
            let start = target.selectionStart
            let ``end``= target.selectionEnd
            async {
                do! subjectX.OnNextAsync(MsgX.InputX(x, start, ``end``))
            } |> Async.Start
        member _.OnChange2(e : Browser.Types.Event) : unit =
            subjectX.OnNextAsync(MsgX.InputX2 e.Value) |> Async.Start
        member _.OnBlur(e : Browser.Types.Event) : unit =
            subjectX.OnNextAsync(MsgX.ChangeTime) |> Async.Start

    let viewX (modelX : ModelX) (_ : Dispatch<MsgX>) : ReactElement =
        Fable.React.Helpers.ofType<ComponentX, ModelX, unit>(modelX) []

    let componentX =
        Fable.Reaction.Reaction.StreamView
            {time = 1; input = 0, "", 0, 0; input2 = ""}
            viewX
            updateX
            (fun _ o ->
                o
                |> FSharp.Control.AsyncRx.merge observableX
                |> Fable.Reaction.AsyncRx.tag "x"
            )
    mountById "elmish-app" (ofFunction componentX () [])

关键是使用时间戳。

当然,我承认这种解决方法非常复杂且丑陋。

请参考它。

于 2020-03-06T06:06:19.153 回答
1

起初,我的英语不好。请理解。

我也遇到了这个问题,所以我试图找到一个解决方法。

我只是猜测原因可能是 dom 事件和 react.js 重新渲染之间的时间间隔,但我无法确定。

关于解决方法的一个关键点是在 componentDidUpdate 生命周期中手动设置/获取光标位置。

下面是示例代码。

我希望它会有所帮助。

module NotWorks =

    type MsgX = | InputX of string
                | InputX2 of string

    let subjectX , observableX = FSharp.Control.AsyncRx.subject<MsgX>()

    type ModelX = {
        input : string
        input2 : string
    }

    let updateX (modelX : ModelX) (msgX : MsgX) : ModelX =
        match msgX with
        | MsgX.InputX(s) -> { modelX with input = (s) }
        | MsgX.InputX2(s) -> { modelX with input2 = (s) }

    type ComponentX(modelX : ModelX) as this =
        inherit Fable.React.Component<ModelX, unit>(modelX) with
        override _.render() : ReactElement =
            div []
                [
                    textarea
                        [
                            OnChange this.OnChange
                            Value this.props.input
                            Rows 10
                            Cols 40
                        ]
                        []
                    textarea
                        [
                            OnChange this.OnChange2
                            Value this.props.input2
                            Rows 10
                            Cols 40
                        ]
                        []
                ]

        override _.componentDidUpdate(_ : ModelX, _ : unit) : unit =
            ()
        member _.OnChange(e : Browser.Types.Event) : unit =
            let target = e.target :?> Browser.Types.HTMLTextAreaElement
            async {
                // in here, no interval. but the problem can appeared sometimes.
                do! subjectX.OnNextAsync(MsgX.InputX(target.value))
            } |> Async.Start

        member _.OnChange2(e : Browser.Types.Event) : unit =
            let target = e.target :?> Browser.Types.HTMLTextAreaElement
            let x = target.value
            async {
                do! Async.Sleep(1) // this makes interval. the problem appears always.
                do! subjectX.OnNextAsync(MsgX.InputX2(x))
            } |> Async.Start

    let viewX (modelX : ModelX) (_ : Dispatch<MsgX>) : ReactElement =
        Fable.React.Helpers.ofType<ComponentX, ModelX, unit>(modelX) []

    let componentX =
        Fable.Reaction.Reaction.StreamView
            {input = ""; input2 = ""}
            viewX
            updateX
            (fun _ o ->
                o
                |> FSharp.Control.AsyncRx.merge observableX
                |> Fable.Reaction.AsyncRx.tag "x"
            )


module Works =

    type MsgX = | InputX of string * int * int

    let subjectX , observableX = FSharp.Control.AsyncRx.subject<MsgX>()

    type ModelX = {
        input : string * int * int
    }

    let updateX (modelX : ModelX) (msgX : MsgX) : ModelX =
        match msgX with
        | MsgX.InputX(s, start, ``end``) -> { modelX with input = (s, start, ``end``) }

    type ComponentX(modelX : ModelX) as this =
        inherit Fable.React.Component<ModelX, unit>(modelX) with
        // we need a ref to get/set cursor position.
        let mutable refTextArea : Option<Browser.Types.HTMLTextAreaElement> = None
        override _.render() : ReactElement =
            let s, _, _ = this.props.input
            textarea
                [
                    Props.Ref(fun e -> refTextArea <- Some(e :?> Browser.Types.HTMLTextAreaElement))
                    OnChange this.OnChange
                    Value s
                    Rows 10
                    Cols 40
                ]
                []
        override _.componentDidUpdate(_ : ModelX, _ : unit) : unit =
            // set cursor position manually using saved model data.
            refTextArea
            |> Option.iter (fun elem ->
                let _, start, ``end`` = this.props.input // must use current model data but not previous model data.
                elem.selectionStart <- start;
                elem.selectionEnd <- ``end``
            )
        member _.OnChange(e : Browser.Types.Event) : unit =
            let target = e.target :?> Browser.Types.HTMLTextAreaElement
            // save cursor position.
            let x = target.value
            let start = target.selectionStart
            let ``end``= target.selectionEnd
            async {
                do! subjectX.OnNextAsync(MsgX.InputX(x, start, ``end``))
            } |> Async.Start

    let viewX (modelX : ModelX) (_ : Dispatch<MsgX>) : ReactElement =
        Fable.React.Helpers.ofType<ComponentX, ModelX, unit>(modelX) []

    let componentX =
        Fable.Reaction.Reaction.StreamView
            {input = "", 0, 0}
            viewX
            updateX
            (fun _ o ->
                o
                |> FSharp.Control.AsyncRx.merge observableX
                |> Fable.Reaction.AsyncRx.tag "x"
            )
于 2020-03-06T04:10:41.767 回答
1

使用Fable.React.Helpers.valueOrDefault代替DefaultValueor Value

/// `Ref` callback that sets the value of an input textbox after DOM element is created.
// Can be used instead of `DefaultValue` and `Value` props to override input box value.
let inline valueOrDefault value =
        Ref <| (fun e -> if e |> isNull |> not && !!e?value <> !!value then e?value <- !!value)

Maximillian在他的回答中指出,这是因为 React 正在重新创建 DOM 对象。在我试图弄清楚如何使用钩子来确定原因时,我最终学习了React Hooks以及如何从 Fable 中使用它们,但我从未弄清楚如何使用它们来找出导致 DOM 对象被删除的原因.

于 2020-07-02T08:25:12.013 回答
0

防止这种情况发生的解决方案是按如下方式扩充您的 onChange 函数。

let onChangeWithCursorAdjustment (myOnChangeFunction: Browser.Types.Event -> unit) (event: Browser.Types.Event) =
    let target = event.target :?> Browser.Types.HTMLInputElement
    let prevBeginPos = target.selectionStart
    let prevEndPos = target.selectionEnd

    myOnChangeFunction event

    Browser.Dom.window.requestAnimationFrame (fun _ -> 
        target.selectionStart <- prevBeginPos
        target.selectionEnd <- prevEndPos
    ) |> ignore

并像这样使用它(对于文本输入字段):

input [] [
    Type "text"
    Value someChangingValue
    OnChange (onChangeWithCursorAdjustment myOnChangeFunction)
]

这将停止光标跳到输入字段的末尾。

于 2020-12-02T12:02:11.003 回答