Search code examples
cssreact-nativeflexbox

Stick two flex elements with dynamic growing in react-native


With react-native, I'm looking forward having a TextInput stuck with a MaterialIcons.Button within the same line.

I want the whole elements be centered horizontally but cannot achieve this with the following code:

import React from 'react';
import {
  StyleSheet, TextInput, View,
} from 'react-native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';

const WordInput = () => {
  return (
    <View style={styles.container}>
      <View style={styles.textInputContainer}>
        <TextInput
          textAlign="left"
        />
      </View>
      
      <View style={styles.arrowButtonContainer}>
        <MaterialIcons.Button
          name="arrow-forward-ios"
        />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    flex: 1,
  },
  textInputContainer: {
    flex: 1,
    alignItems: 'flex-end',
    justifyContent: 'center',
  },
  arrowButtonContainer: {
    flex: 1,
    alignItems: 'flex-start',
    justifyContent: 'center',
  },
});

Here is the associated expo snack link.

The problem is that when I type text inside the TextInput, the Button doesn't move at all. I would like it to dynamically shift to the right when the TextInput's width grow. The overall should be horizontally centered.

Does anyone know how I can proceed?

Thanks!


Solution

  • Unfortunately <TextInput> doesn't support any sort of "auto-growing" behaviour by default, but you could implement something yourself using a hidden <Text> layer that has the same font styling and sizing rules as your <TextInput>. If you then render your input value to that <Text> element, you can measure the layout of that element and apply the measured width to your <TextInput> component.

    import { useState } from 'react';
    import { StyleSheet, TextInput, View, Dimensions, Text } from 'react-native';
    import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
    
    export default function App() {
      const [value, setValue] = useState();
      const [containerWidth, setContainerWidth] = useState(
        Dimensions.get('window').width
      );
      const [textWidth, setTextWidth] = useState(0);
      const [buttonWidth, setButtonWidth] = useState(0);
    
      const inputWidth = Math.min(textWidth, containerWidth - buttonWidth);
    
      return (
        <View style={styles.root}>
          <View
            style={styles.inner}
            onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}>
            <Text
              style={styles.hiddenText}
              onLayout={(e) => setTextWidth(e.nativeEvent.layout.width)}
              numberOfLines={1}
              accessibilityElementsHidden
              importantForAccessibility="no-hide-decendants">
              {value}
            </Text>
            <TextInput
              textAlign="left"
              placeholder="enter text"
              style={[styles.input, { width: inputWidth }]}
              onChangeText={setValue}
            />
            <View onLayout={(e) => setButtonWidth(e.nativeEvent.layout.width)}>
              <MaterialIcons.Button name="arrow-forward-ios" />
            </View>
          </View>
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      root: {
        flex: 1,
        justifyContent: 'center',
      },
      inner: {
        flexDirection: 'row',
        justifyContent: 'center',
        borderWidth: 1,
        borderColor: 'red',
      },
      input: {
        borderWidth: 1,
        borderColor: 'green',
        minWidth: 100,
      },
      hiddenText: {
        position: 'absolute',
        top: 0,
        left: 0,
        opacity: 0,
      },
    });
    

    Here's an expo snack example.