DEV Community

Shaswat Prabhat
Shaswat Prabhat

Posted on

Auto-scroll in React Native forms

TL;DR

Creation of React-Native forms with ability to scroll to error fields and focus on that input field.

Create forms with React Hook Form

There are simpler ways to implement forms in React-Native with error mapping and submission as a part of the screen state logic.
For our purposes we needed complex logic for validation and blur messages.
Hence we chose to use React Hook Form
It has a rich API for form modes, setting errors explicitly, triggering validations etc.
The latest version also comes packed with a useFormContext which can be used for extended validation in case of custom React Hook Form components.

A very basic React-Native usage of the library will look like below:

import React from "react";
import { Text, View, TextInput, Button, Alert } from "react-native";
import { useForm, Controller } from "react-hook-form";
export default function App() {
  const { control, handleSubmit, errors } = useForm();
  const onSubmit = data => console.warn("Form Data",JSON.stringify(data));
return (
    <View>
      <Text>First name</Text>
      <Controller
        as={TextInput}
        control={control}
        name="firstName"
        onChange={args => args[0].nativeEvent.text}
        rules={{ required: true }}
        defaultValue=""
      />
      {errors.firstName && <Text>This is required.</Text>}         
      <Text>Last name</Text>
      <Controller
        as={TextInput}
        control={control}
        name="lastName"
        onChange={args => args[0].nativeEvent.text}
        defaultValue=""
      />
     <Button title="Submit" onPress={handleSubmit(onSubmit)} />
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

We can choose to wrap a custom component with the Hooks library as well, the API exposes onChangeName and onBlurName, which can be used if the custom component has different names for these events.

These can be used to listen in to changes in custom components and trigger actions like display validation messages etc.

A sample for a custom component will look like below:

...
import { Controller, useForm } from 'react-hook-form';
...
const { control, errors, clearError, setError, triggerValidation, getValues, setValue } = useForm;
...
<Controller
  name="mySampleInput"
  control={control}

  as={<CustomInput
  label='My Custom Label'
  maxLength={9}
  validationMessage={
  errors && errors.annualGrossIncome && 'My Validation error' }
  testID="mySampleInput"
  />}
  onBlurName="customOnBlur"
  customOnBlur={handleCustomBlur}

  rules={{
  required: true,
  }}
  onChangeName="onValueChange"

  onChange={args => { 
  clearError('mySampleInput');
  return args[0].value;
  }}
/>
...
Enter fullscreen mode Exit fullscreen mode

If we want to create a React Hook Form custom component with its own custom validation logic then we need to ensure that the component receives the same useForm hook.

For this the library recently added useFormContext which can be used to drill the data to the nested React Hook Form-based component.

...
const useFormObj = useForm({ defaultValues: 
  {mySampleInput:"a sample value"},
});
const { control, errors, clearError, setError, triggerValidation, getValues, setValue } = useFormObj;
...
<FormContext {...useFormObj}>
// The form code here
</FormContext>
Enter fullscreen mode Exit fullscreen mode

Auto scroll for Form

Due to limited screen real state, in case of a bigger form we want the screen to scroll to the first input field with the error.

In case of big forms even when a validation message is shown, a user might not know which fields are erroneous and might keep pressing the submit button in vain.

The first step to this is to wrap the form with a ScrollView.
Next, to achieve the scroll-to effect, we developed a useAutoScroll hook.

The Hook has four parts:

  1. setScrollRef: This is used to keep track of ref for the wrapping ScrollView. This is the ref against which we can trigger the scrollTo method with a y coordinate .
  2. scrollTracker: This is a helper function which wraps the component with a View and then gets the absolute y coordinate of the View.
  3. scrollTo: This exposed method takes in the errors key array from React Hook Forms and then computes the first error field. Then it scrolls to that field.
  4. captureRef: A ref for components. We will come back to this.

Below we start with setScrollRef. This just captures the ref value for wrapping ScrollView.

...
const useAutoScroll = () => {
  const yCoordinates = useRef({});
  let scrollRef = null;
  const setScrollRef = ref => {
   if (ref) scrollRef = ref;
  };
...
  return {
    setScrollRef
  };
}
Enter fullscreen mode Exit fullscreen mode

Then for each component we call a scrollTracker method. ScrollTracker takes in two arguments, the component and its name prop for Controller. This is then used to construct and object internally with the below structure:

const yCoorodinates = {
   'mySampleInput': {
      {y:'', ref:''}
    }
}
Enter fullscreen mode Exit fullscreen mode

We tried using onLayout for the View to get the y-coordinate of the component, and in most cases this might just be enough.

But the catch is, onLayout gives the y-coordinate with respect to its immediate View. In case we are using a styled View or a wrapper, the y-coordinate might be different than the absolute value needed with respect to the Screen.

To circumvent this we used the measure native method on the View ref.

const scrollTracker = (component, inputKey) => {
   let viewRef;
// The below logic worked for our custom components.
// Might need tweaking to work for other set of components.
const getScrollToY = py => py - Dimensions.get('window').height / 5.5;
   const getCoordinates = () => {
       viewRef.measure((fx, fy, width, height, px, py) => {
          if (yCoordinates.current[inputKey]) {
            if (!yCoordinates.current[inputKey].y) {
              yCoordinates.current[inputKey].y = getScrollToY(py);
            }
          } 
          else {
            yCoordinates.current[inputKey] = {};
            yCoordinates.current[inputKey].y = getScrollToY(py);
          }
       });
    };
  return (
      <View
        testID={`${inputKey}Wrapper`}
        ref={ref => {
          if (ref) viewRef = ref;
        }}
      onLayout={getCoordinates}
     >
      {component}
     </View>);
};
Enter fullscreen mode Exit fullscreen mode

Then we added a scrollTo method which takes in the errors object keys and then gets the FIRST error key. Meaning the key with least y coordinate value that happens to be in errors array.

We then scroll to that component.

const scrollTo = errors => {
// Util method to get First invalid key, this can be custom.
   const firstInvalidKey =   util.getFirstConditionalKey(yCoordinates.current, 'y', errors);
if (yCoordinates.current[firstInvalidKey].ref) {
     yCoordinates.current[firstInvalidKey].ref.handleFocus({
      nativeEvent: { text: 'Dummy name' },
    });
}
  scrollRef.scrollTo(0,yCoordinates.current[firstInvalidKey].y);
};
Enter fullscreen mode Exit fullscreen mode

This looks great! But we can do more.

In case of text inputs it would be far better if we can focus on the field.
For this we added a captureRef which captures the ref of the component.
We can then add this to our coordinate object such that each input key will now have a y-coordinate and ref within it.
Then our scrollTo can be modified to call .focus() on the captured Ref if it exists.

Even custom components like Select, RadioButtons etc can be modified to accept a focus() callBack and action accordingly.

const captureRef = inputKey => ref => {
   if (ref) {
      if (yCoordinates.current[inputKey]) {
         yCoordinates.current[inputKey].ref = ref;
      } else {
         yCoordinates.current[inputKey] = {};
         yCoordinates.current[inputKey].ref = ref;
      }
   }
};
Enter fullscreen mode Exit fullscreen mode

Our final version of useAutoScroll looks like below, with associated tests:

Usage:

And that's it. We have created a ref-heavy component which works with almost all ref-respecting elements.

Top comments (1)

Collapse
 
anurbecirovic profile image
Anur Becirovic

Can we see the part with utils ?
const firstInvalidKey = util.getFirstConditionalKey(yCoordinates.current, 'y', errors);
this function