Reputation: 281
I'm using TextField component in Jetpack Compose. How to select all text when it receive focus?
Upvotes: 28
Views: 10521
Reputation: 1411
Considering that it is recommended to migrate to Modifier.Node
instead of using composed {}
:
Using @Composable builder functions for modifiers is not recommended, as they cause unnecessary recompositions. To avoid this, you should use Modifier.Node instead. It will allow you to accomplish the same things while being very performant.
There is another API for creating custom modifiers, composed {}. This API is no longer recommended due to the performance issues it created, and like with the extension factory functions case, Modifier.Node is recommended instead.
Here is migrated version of Tom Berghuis's answer:
private class FocusSelectAllElementNode(
private var textFieldValueState: MutableState<TextFieldValue>
) : Modifier.Node(), FocusEventModifierNode {
override fun onFocusEvent(focusState: FocusState) {
if (focusState.isFocused) {
coroutineScope.launch {
val textFieldValue = textFieldValueState.value
textFieldValueState.value = textFieldValue.copy(
selection = TextRange(0, textFieldValue.text.length)
)
}
}
}
fun updateTextFieldValueState(textFieldValueState: MutableState<TextFieldValue>) {
this.textFieldValueState = textFieldValueState
}
}
private data class FocusSelectAllElement(
private val textFieldValueState: MutableState<TextFieldValue>
) : ModifierNodeElement<FocusSelectAllElementNode>() {
override fun create(): FocusSelectAllElementNode {
return FocusSelectAllElementNode(textFieldValueState)
}
override fun update(node: FocusSelectAllElementNode) {
node.updateTextFieldValueState(textFieldValueState)
}
override fun InspectorInfo.inspectableProperties() {
name = "onFocusSelectAll"
}
}
fun Modifier.onFocusSelectAll(
textFieldValueState: MutableState<TextFieldValue>
): Modifier = this then FocusSelectAllElement(textFieldValueState)
Upvotes: 0
Reputation: 569
I wrote a Modifier extension function that works in spite of bug pointed out by @Pylyp
fun Modifier.onFocusSelectAll(textFieldValueState: MutableState<TextFieldValue>): Modifier =
composed(
inspectorInfo = debugInspectorInfo {
name = "textFieldValueState"
properties["textFieldValueState"] = textFieldValueState
}
) {
var triggerEffect by remember {
mutableStateOf<Boolean?>(null)
}
if (triggerEffect != null) {
LaunchedEffect(triggerEffect) {
val tfv = textFieldValueState.value
textFieldValueState.value = tfv.copy(selection = TextRange(0, tfv.text.length))
}
}
onFocusChanged { focusState ->
if (focusState.isFocused) {
triggerEffect = triggerEffect?.let { bool ->
!bool
} ?: true
}
}
}
usage
@Composable
fun SelectAllOnFocusDemo() {
var tfvState = remember {
mutableStateOf(TextFieldValue("initial text"))
}
TextField(
modifier = Modifier.onFocusSelectAll(tfvState),
value = tfvState.value,
onValueChange = { tfvState.value = it },
)
}
Upvotes: 6
Reputation: 3359
I want to add to the Phil's answer I wanted to update the state dynamically and I ended up with this:
var state by remember(textVal) {
mutableStateOf(TextFieldValue(text = textVal, selection = TextRange(textVal.length)))
}
It does two things, first it updates the field if your textVal
changes, also puts the cursor at the end.
Upvotes: 3
Reputation: 4176
I didn't have 100% success with @nglauber answer. You should add a small delay and it works great. For example:
val state = remember {
mutableStateOf(TextFieldValue(""))
}
// get coroutine scope from composable
val scope = rememberCoroutineScope()
TextField(
value = state.value,
onValueChange = { text -> state.value = text },
modifier = Modifier
.onFocusChanged {
if (it.hasFocus) {
// start coroutine
scope.launch {
// add your preferred delay
delay(10)
val text = state.value.text
state.value = state.value.copy(
selection = TextRange(0, text.length)
)
}
}
}
)
Upvotes: 5
Reputation: 88022
@nglauber solution doesn't seems to work anymore.
Debugging shows that onFocusChanged
is called before onValueChange
and within one view life cycle. A selection changed during onFocusChanged
has no effect on TextField
, since it is overridden during onValueChange
.
Here's a possible workaround:
var state by remember {
mutableStateOf(TextFieldValue("1231"))
}
var keepWholeSelection by remember { mutableStateOf(false) }
if (keepWholeSelection) {
// in case onValueChange was not called immediately after onFocusChanged
// the selection will be transferred correctly, so we don't need to redefine it anymore
SideEffect {
keepWholeSelection = false
}
}
TextField(
value = state,
onValueChange = { newState ->
if (keepWholeSelection) {
keepWholeSelection = false
state = newState.copy(
selection = TextRange(0, newState.text.length)
)
} else {
state = newState
}
},
modifier = Modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val text = state.text
state = state.copy(
selection = TextRange(0, text.length)
)
keepWholeSelection = true
}
}
)
I think it should be possible to make it easier, so I created this question on Compose issue tracker.
Upvotes: 8
Reputation: 23964
In this case you should use TextFieldValue
as state of your TextField
, and when it receive focus, you set the selection
using the TextFieldValue
state.
val state = remember {
mutableStateOf(TextFieldValue(""))
}
TextField(
value = state.value,
onValueChange = { text -> state.value = text },
modifier = Modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val text = state.value.text
state.value = state.value.copy(
selection = TextRange(0, text.length)
)
}
}
)
Here's the result:
Notice that depending on you're touching the cursor goes to the touched position instead of select the entire text. You can try to figure it out if this is a bug or a feature :)
Upvotes: 25