-
I want to set keyboard focus to an input that I have added to the UI so that the user can start typing immediately. I've found a working solution - but it feels cumbersome and begs a question. We can break my IRL example down to a simple To-Do-list. type Item =
{ Text: string
Done: bool
Added: bool } // 🚩 added to Focus() the Textbox for Text in the view function // ❓ TODO is this required?
type Model = { List: Item list }
type Msg =
| AddItem
| RemoveItem of Item
| TextUpdated of Item * string
| ToggleDone of Item * bool
let initModel = { List = [] }
let update msg model =
match msg with
| AddItem ->
let newItem =
{ Text = ""
Done = false
// 🚩 set here to Focus() the Textbox in the view function
// ❓ TODO is this required? otherwise, how to reset to false after Focus?
Added = true }
{ model with
List = model.List @ [ newItem ] }
| RemoveItem item ->
{ model with
List = model.List |> List.except [ item ] }
| ToggleDone(item, isDone) ->
let items =
model.List
|> List.map (fun i -> if i = item then { item with Done = isDone } else i)
{ model with List = items }
| TextUpdated(item, text) ->
let items =
model.List |> List.map (fun i -> if i = item then { i with Text = text } else i)
{ model with List = items } Defining and setting the let view model =
HWrap() {
for item in model.List do
VStack(5) {
HStack(5) {
CheckBox(item.Done, (fun isDone -> ToggleDone(item, isDone)))
let textBox = TextBox(item.Text, (fun value -> TextUpdated(item, value)))
// 🚩 new item identified! please hold your nose while I Focus() its TextBox...
if item.Added then
let newText = ViewRef<TextBox>()
textBox.reference (newText)
let rec attachFocus =
fun _ _ ->
let rec focus =
fun _ _ ->
newText.Value.Focus() |> ignore // what I actually want to do
newText.Value.AttachedToVisualTree.RemoveHandler(focus) // to clean up, works without
newText.Value.AttachedToVisualTree.AddHandler(focus) // to enable successful Focus()
newText.Attached.RemoveHandler(attachFocus) // to clean up, works without
newText.Attached.AddHandler(attachFocus) // to have access to newText.Value
else
textBox
Button("❌", RemoveItem item).tip (ToolTip("remove this item"))
}
}
Button("Add item", AddItem)
} Can this be solved more elegantly? Otherwise, how would you suggest resetting the |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
Here are the corresponding Avalonia docs. |
Beta Was this translation helpful? Give feedback.
-
In this scenario what we normally do is create custom modifier to handle and hide some of this complexity. ie module FocusAttributes =
let Focus =
Attributes.defineBool "Focus" (fun oldValueOpt newValueOpt node ->
let target = node.Target :?> TextBox
let setFocus x y =
target.Focus()|> ignore
match newValueOpt with
| ValueNone -> ()
| ValueSome v ->
target.AttachedToVisualTree.AddHandler(setFocus)
target.AttachedToVisualTree.RemoveHandler(setFocus)
)
type FocusModifiers =
[<Extension>]
static member inline focus(this: WidgetBuilder<'msg, #IFabTextBox>, moveUp: bool) =
this.AddScalar(FocusAttributes.Focus.WithValue(moveUp))
let view model =
HWrap() {
for item in model.List do
VStack(5) {
HStack(5) {
CheckBox(item.Done, (fun isDone -> ToggleDone(item, isDone)))
TextBox(item.Text, (fun value -> TextUpdated(item, value)))
.focus(item.Added) // Use the new modifier here
Button("❌", RemoveItem item)
.tip(ToolTip("remove this item"))
}
}
Button("Add item", AddItem)
} |
Beta Was this translation helpful? Give feedback.
-
@edgarfgp Thank you for pointing me in the right direction :) I've modified your suggestion slightly to only set the focus if the boolean passed is true, clean up the handler only after it has run and open it up to be used with open System.Runtime.CompilerServices
open Avalonia.Input
open Fabulous
open Fabulous.Avalonia
module FocusAttributes =
/// Allows setting the Focus on an Avalonia.Input.InputElement
let Focus =
Attributes.defineBool "Focus" (fun oldValueOpt newValueOpt node ->
let target = node.Target :?> InputElement
let rec focusAndCleanUp x y =
target.Focus() |> ignore
target.AttachedToVisualTree.RemoveHandler(focusAndCleanUp) // to clean up
if newValueOpt.IsSome && newValueOpt.Value then
target.AttachedToVisualTree.AddHandler(focusAndCleanUp))
type FocusModifiers =
[<Extension>]
/// Sets the Focus on an IFabInputElement if set is true; otherwise does nothing.
static member inline focus(this: WidgetBuilder<'msg, #IFabInputElement>, set: bool) =
this.AddScalar(FocusAttributes.Focus.WithValue(set))
I also found a solution for resetting the type Msg =
...
| Focused of Item
...
let update msg model =
match msg with
...
| Focused item ->
let items =
model.List
|> List.map (fun i -> if i = item then { i with Added = false } else i)
{ model with List = items }
... With these additions in place, I can hide the handler attachment details from the view function and clean up the let view model =
HWrap() {
for item in model.List do
VStack(5) {
HStack(5) {
CheckBox(item.Done, (fun isDone -> ToggleDone(item, isDone)))
let textBox = TextBox(item.Text, (fun value -> TextUpdated(item, value)))
// 🚩 new item identified! Let's Focus() its TextBox...
if item.Added then
textBox.focus(true).onGotFocus (fun args -> Focused item)
else
textBox
Button("❌", RemoveItem item).tip (ToolTip("remove this item"))
}
}
Button("Add item", AddItem)
} |
Beta Was this translation helpful? Give feedback.
@edgarfgp Thank you for pointing me in the right direction :)
I've modified your suggestion slightly to only set the focus if the boolean passed is true, clean up the handler only after it has run and open it up to be used with
InputElement
s other thanTextBox
: