Fable.React - 编辑输入字段将光标移动到文本末尾
Fable.React - editing an Input field moves cursor to end of text
我正在使用 SAFE 堆栈的 Elmish.Bridge 风格。
在视图的顶层,我创建了一个输入字段,如下所示:
Input.input [ Input.Value(model.FieldValue); Input.OnChange(fun e -> dispatch (EditFieldValue(e.Value))) ]
当我通过在字段的中间键入来编辑该字段的值时,模型会按预期更新,但光标也会移动到末尾输入的文本.
我的模型有好几层,完全由可序列化类型(基元、字符串、集合、记录和联合)构成。
我尝试在玩具应用程序中重现它(其中模型复杂得多),但它在那里按预期工作 - 光标保持位置。
在这种情况下,有什么方法可以确定光标移动的原因吗?
使用 DefaultValue 而不是 Value 应该可以解决这个问题。
这适用于 Fable.React 和 Fable.Elmish。使用 DefaultValue 不会在反应生命周期中启动组件更新 / DOM,因此您不应该获得光标更改。
来自下面的 link:
react uncontrolled components
In the React rendering lifecycle, the value attribute on form elements will override the value in the DOM. With an uncontrolled component, you often want React to specify the initial value, but leave subsequent updates uncontrolled. To handle this case, you can specify a defaultValue attribute instead of value.
我有一个类似的问题,原来是因为 React 正在删除我的 DOM 对象并重新创建它。 (这反过来是由于我的 React 元素虽然在两个代码分支中相同,但在两个不同的分支中作为父元素具有不同的元素。)
如果 React 没有 delete/recreate 您的 DOM 对象,您的光标应该会正常运行。如果您想弄清楚是什么导致您的 DOM 成为 updated/created,请在 Hooks.useEffectDisposable 调用中添加一些 printfns。在 effect 中添加一个 printfn,它将告诉您元素何时被挂载,在 IDisposable 中添加另一个以告诉您元素何时被卸载。
使用不受控制的组件在某些情况下可以工作,但如果模型中的其他内容发生变化,例如,它会阻止您的文本更新。在有人点击 "OK" 按钮后,您无法清除文本框。最好只关注 React 生命周期并解决导致 React 重新渲染 DOM.
的任何问题
起初,我的英语不好。请理解。
我也遇到过这个问题,所以我试图找到解决方法。
我只是猜测原因可能是 dom 事件和 react.js 重新渲染之间的时间间隔,但我无法确定。
解决方法的一个关键点是 set/get 在 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"
)
我漏掉了一些我不得不说的话。
如果还有一个 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 () [])
重点是使用时间戳。
当然,我承认这个解决方法非常复杂和丑陋。
请参考一下。
使用Fable.React.Helpers.valueOrDefault
代替DefaultValue
或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 指出 that it's because React is recreating the DOM object. In my attempt to figure out how to use hooks to determine why, I ended up learning about React Hooks and about how to use them from Fable,但我一直不知道如何使用它们来找出导致 DOM 对象被删除的原因。
防止这种情况发生的一个解决方案是按如下方式扩充您的 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)
]
这将停止光标跳到输入字段的末尾。
我正在使用 SAFE 堆栈的 Elmish.Bridge 风格。
在视图的顶层,我创建了一个输入字段,如下所示:
Input.input [ Input.Value(model.FieldValue); Input.OnChange(fun e -> dispatch (EditFieldValue(e.Value))) ]
当我通过在字段的中间键入来编辑该字段的值时,模型会按预期更新,但光标也会移动到末尾输入的文本.
我的模型有好几层,完全由可序列化类型(基元、字符串、集合、记录和联合)构成。
我尝试在玩具应用程序中重现它(其中模型复杂得多),但它在那里按预期工作 - 光标保持位置。
在这种情况下,有什么方法可以确定光标移动的原因吗?
使用 DefaultValue 而不是 Value 应该可以解决这个问题。
这适用于 Fable.React 和 Fable.Elmish。使用 DefaultValue 不会在反应生命周期中启动组件更新 / DOM,因此您不应该获得光标更改。
来自下面的 link: react uncontrolled components
In the React rendering lifecycle, the value attribute on form elements will override the value in the DOM. With an uncontrolled component, you often want React to specify the initial value, but leave subsequent updates uncontrolled. To handle this case, you can specify a defaultValue attribute instead of value.
我有一个类似的问题,原来是因为 React 正在删除我的 DOM 对象并重新创建它。 (这反过来是由于我的 React 元素虽然在两个代码分支中相同,但在两个不同的分支中作为父元素具有不同的元素。)
如果 React 没有 delete/recreate 您的 DOM 对象,您的光标应该会正常运行。如果您想弄清楚是什么导致您的 DOM 成为 updated/created,请在 Hooks.useEffectDisposable 调用中添加一些 printfns。在 effect 中添加一个 printfn,它将告诉您元素何时被挂载,在 IDisposable 中添加另一个以告诉您元素何时被卸载。
使用不受控制的组件在某些情况下可以工作,但如果模型中的其他内容发生变化,例如,它会阻止您的文本更新。在有人点击 "OK" 按钮后,您无法清除文本框。最好只关注 React 生命周期并解决导致 React 重新渲染 DOM.
的任何问题起初,我的英语不好。请理解。
我也遇到过这个问题,所以我试图找到解决方法。
我只是猜测原因可能是 dom 事件和 react.js 重新渲染之间的时间间隔,但我无法确定。
解决方法的一个关键点是 set/get 在 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"
)
我漏掉了一些我不得不说的话。
如果还有一个 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 () [])
重点是使用时间戳。
当然,我承认这个解决方法非常复杂和丑陋。
请参考一下。
使用Fable.React.Helpers.valueOrDefault
代替DefaultValue
或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 指出
防止这种情况发生的一个解决方案是按如下方式扩充您的 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)
]
这将停止光标跳到输入字段的末尾。