This post is a follow up to Forms in React Native with Formik and Yup where we'll make a responsive scrollview.
Let's take a look at the problem we're trying to solve:
When our form content takes up less space than the height of the screen (minus the statusbar), everything looks fine:
iPhone X
iPhone 5s
Now let's add another field to the form, Name. This is the result
iPhone X
iPhone 5s
See the problem with the iPhone 5S display? The button is cut off. In this case, we want to let the user scroll the view while for the iPhone X, things will continue to be the same.
Make sure you've completed the steps in Forms in React Native with Formik and Yup. We'll be picking up things from there
This is what our UserRegistrationScreen looks like
import React from 'react';
import { SafeAreaView, View, StyleSheet, Image, Text } from 'react-native';
import { Formik } from 'formik';
import FlashMessage, { showMessage } from 'react-native-flash-message';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import Button from '../../components/Button';
import Input from '../../components/Input';
import validate from '../../utils/FormValidations/registrationValidation';
const logo = require('../../../assets/logo/logo.png');
export default class UserRegistration extends React.Component {
render() {
return (
<SafeAreaView style={styles.container}>
<KeyboardAwareScrollView contentContainerStyle={styles.container}>
<Image source={logo} resizeMode="contain" style={styles.logo} />
<Formik
initialValues={{
email: '',
password: '',
passwordConfirm: ''
}}
onSubmit={(values, { resetForm }) => {
console.log(values);
showMessage({
message: 'Success!',
type: 'success'
});
resetForm({});
}}
validate={validate}
>
{({ handleSubmit, handleChange, errors, values, touched }) => (
<View style={styles.formWrapper}>
<View style={styles.inputWrapper}>
<Input
placeholder="EMAIL"
onChangeText={handleChange('email')}
value={values.email}
/>
{errors.email && touched.email && (
<Text style={styles.errorInput}>
{errors.email.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="PASSWORD"
password
onChangeText={handleChange('password')}
value={values.password}
/>
{errors.password && touched.password && (
<Text style={styles.errorInput}>
{errors.password.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="CONFIRM PASSWORD"
password
onChangeText={handleChange('passwordConfirm')}
value={values.passwordConfirm}
/>
{errors.passwordConfirm && touched.passwordConfirm && (
<Text style={styles.errorInput}>
{errors.passwordConfirm.toUpperCase()}
</Text>
)}
</View>
<Button onClick={handleSubmit} text="Register" />
</View>
)}
</Formik>
<FlashMessage position="top" />
</KeyboardAwareScrollView>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
width: '100%'
},
logo: {
width: 200
},
formWrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%'
},
inputWrapper: {
marginBottom: 48,
width: 180
},
errorInput: {
color: 'red',
textAlign: 'center',
marginTop: 4
}
});
With just a few changes, we can get the scroll working
import React from 'react';
import { SafeAreaView, View, StyleSheet, Image, Text } from 'react-native';
import { Formik } from 'formik';
import FlashMessage, { showMessage } from 'react-native-flash-message';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import Button from '../../components/Button';
import Input from '../../components/Input';
import validate from '../../utils/FormValidations/registrationValidation';
const logo = require('../../../assets/logo/logo.png');
export default class UserRegistration extends React.Component {
render() {
return (
<SafeAreaView style={styles.container}>
<KeyboardAwareScrollView
style={styles.scrollViewContainer}
contentContainerStyle={{
flexGrow: 1,
alignItems: 'center'
}}
>
<Image source={logo} resizeMode="contain" style={styles.logo} />
<Formik
initialValues={{
name: '',
email: '',
password: '',
passwordConfirm: ''
}}
onSubmit={(values, { resetForm }) => {
console.log(values);
showMessage({
message: 'Success!',
type: 'success'
});
resetForm({});
}}
validate={validate}
>
{({ handleSubmit, handleChange, errors, values, touched }) => (
<View style={styles.formWrapper}>
<View style={styles.inputWrapper}>
<Input
placeholder="NAME"
onChangeText={handleChange('name')}
value={values.email}
/>
{errors.name && touched.name && (
<Text style={styles.errorInput}>
{errors.name.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="EMAIL"
onChangeText={handleChange('email')}
value={values.email}
/>
{errors.email && touched.email && (
<Text style={styles.errorInput}>
{errors.email.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="PASSWORD"
password
onChangeText={handleChange('password')}
value={values.password}
/>
{errors.password && touched.password && (
<Text style={styles.errorInput}>
{errors.password.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="CONFIRM PASSWORD"
password
onChangeText={handleChange('passwordConfirm')}
value={values.passwordConfirm}
/>
{errors.passwordConfirm && touched.passwordConfirm && (
<Text style={styles.errorInput}>
{errors.passwordConfirm.toUpperCase()}
</Text>
)}
</View>
<Button onClick={handleSubmit} text="Register" />
</View>
)}
</Formik>
<FlashMessage position="top" />
</KeyboardAwareScrollView>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
width: '100%'
},
scrollViewContainer: { flex: 1, width: '100%' },
logo: {
width: 200
},
formWrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%'
},
inputWrapper: {
marginBottom: 48,
width: 180
},
errorInput: {
color: 'red',
textAlign: 'center',
marginTop: 4
}
});
Great, the scroll works for the iPhone 5S but it also is enabled for the iPhone X even though we're well within the screen height.
iPhone X
iPhone 5s
We want a way to get the current height of the scrollview to make a decision, as to whether we want to allow the user to scroll the screen. ScrollView has a property called onContentSizeChange that gives us just that. We'll calculate the height of the current display, and if it is greater than the scrollview context plus the size of the statusbar, we'll toggle the flag scrollEnabled.
scrollEnabled = contentHeight + statusBarHeight > SCREEN_HEIGHT
I'd also like to add some padding for when we scroll, to ensure that the Register button has some breathing room.
import React from 'react';
import {
SafeAreaView,
View,
StyleSheet,
Image,
Text,
Dimensions
} from 'react-native';
import { Formik } from 'formik';
import FlashMessage, { showMessage } from 'react-native-flash-message';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { Constants } from 'expo';
import Button from '../../components/Button';
import Input from '../../components/Input';
import validate from '../../utils/FormValidations/registrationValidation';
const logo = require('../../../assets/logo/logo.png');
const SCREEN_HEIGHT = Dimensions.get('window').height;
const PADDING_BOTTOM = 24;
export default class UserRegistration extends React.Component {
state = {
scrollEnabled: false,
};
_onContentSizeChange = (_, contentHeight) => {
const statusBarHeight = Constants.statusBarHeight;
const scrollEnabled = contentHeight + statusBarHeight > SCREEN_HEIGHT;
this.setState({
scrollEnabled
});
};
render() {
let { scrollEnabled } = this.state;
return (
<SafeAreaView style={styles.container}>
<KeyboardAwareScrollView
style={styles.scrollViewContainer}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: scrollEnabled ? PADDING_BOTTOM : 0,
alignItems: 'center'
}}
onContentSizeChange={this._onContentSizeChange}
scrollEnabled={scrollEnabled}
>
<Image source={logo} resizeMode="contain" style={styles.logo} />
<Formik
initialValues={{
name: '',
email: '',
password: '',
passwordConfirm: ''
}}
onSubmit={(values, { resetForm }) => {
console.log(values);
showMessage({
message: 'Success!',
type: 'success'
});
resetForm({});
}}
validate={validate}
>
{({ handleSubmit, handleChange, errors, values, touched }) => (
<View style={styles.formWrapper}>
<View style={styles.inputWrapper}>
<Input
placeholder="NAME"
onChangeText={handleChange('name')}
value={values.email}
/>
{errors.name && touched.name && (
<Text style={styles.errorInput}>
{errors.name.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="EMAIL"
onChangeText={handleChange('email')}
value={values.email}
/>
{errors.email && touched.email && (
<Text style={styles.errorInput}>
{errors.email.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="PASSWORD"
password
onChangeText={handleChange('password')}
value={values.password}
/>
{errors.password && touched.password && (
<Text style={styles.errorInput}>
{errors.password.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="CONFIRM PASSWORD"
password
onChangeText={handleChange('passwordConfirm')}
value={values.passwordConfirm}
/>
{errors.passwordConfirm && touched.passwordConfirm && (
<Text style={styles.errorInput}>
{errors.passwordConfirm.toUpperCase()}
</Text>
)}
</View>
<Button onClick={handleSubmit} text="Register" />
</View>
)}
</Formik>
<FlashMessage position="top" />
</KeyboardAwareScrollView>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
width: '100%'
},
scrollViewContainer: { flex: 1, width: '100%' },
logo: {
width: 200
},
formWrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%'
},
inputWrapper: {
marginBottom: 48,
width: 180
},
errorInput: {
color: 'red',
textAlign: 'center',
marginTop: 4
}
});
iPhone X
iPhone 5s
Let's finally move on to giving the user an indication that there's more content below. I've chosen to use a down arrow icon that will disappear when the user has scrolled close to the end.
import React from 'react';
import {
SafeAreaView,
View,
StyleSheet,
Image,
Text,
Dimensions
} from 'react-native';
import { Formik } from 'formik';
import FlashMessage, { showMessage } from 'react-native-flash-message';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { Constants } from 'expo';
import { Ionicons } from '@expo/vector-icons';
import Button from '../../components/Button';
import Input from '../../components/Input';
import validate from '../../utils/FormValidations/registrationValidation';
const logo = require('../../../assets/logo/logo.png');
const SCREEN_HEIGHT = Dimensions.get('window').height;
const PADDING_BOTTOM = 24;
const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }) => {
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - PADDING_BOTTOM
);
};
export default class UserRegistration extends React.Component {
state = {
scrollEnabled: false,
showMoreIndicator: true
};
_onContentSizeChange = (_, contentHeight) => {
const statusBarHeight = Constants.statusBarHeight;
const scrollEnabled = contentHeight + statusBarHeight > SCREEN_HEIGHT;
this.setState({
scrollEnabled
});
};
_onScroll = ({ nativeEvent }) => {
if (isCloseToBottom(nativeEvent)) {
this._disableMoreIndicator();
return;
}
this._enableMoreIndicator();
};
_disableMoreIndicator = () => {
this.setState({
showMoreIndicator: false
});
};
_enableMoreIndicator = () => {
this.setState({
showMoreIndicator: true
});
};
_scrollToEnd = () => {
if (this._scrollView) {
this._scrollView.scrollToEnd();
}
};
render() {
let { showMoreIndicator, scrollEnabled } = this.state;
if (!scrollEnabled) showMoreIndicator = false;
return (
<SafeAreaView style={styles.container}>
<KeyboardAwareScrollView
style={styles.scrollViewContainer}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: scrollEnabled ? PADDING_BOTTOM : 0,
alignItems: 'center'
}}
onContentSizeChange={this._onContentSizeChange}
scrollEnabled={scrollEnabled}
innerRef={ref => {
this._scrollView = ref;
}}
onScroll={this._onScroll}
>
<Image source={logo} resizeMode="contain" style={styles.logo} />
<Formik
initialValues={{
name: '',
email: '',
password: '',
passwordConfirm: ''
}}
onSubmit={(values, { resetForm }) => {
console.log(values);
showMessage({
message: 'Success!',
type: 'success'
});
resetForm({});
}}
validate={validate}
>
{({ handleSubmit, handleChange, errors, values, touched }) => (
<View style={styles.formWrapper}>
<View style={styles.inputWrapper}>
<Input
placeholder="NAME"
onChangeText={handleChange('name')}
value={values.email}
/>
{errors.name && touched.name && (
<Text style={styles.errorInput}>
{errors.name.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="EMAIL"
onChangeText={handleChange('email')}
value={values.email}
/>
{errors.email && touched.email && (
<Text style={styles.errorInput}>
{errors.email.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="PASSWORD"
password
onChangeText={handleChange('password')}
value={values.password}
/>
{errors.password && touched.password && (
<Text style={styles.errorInput}>
{errors.password.toUpperCase()}
</Text>
)}
</View>
<View style={styles.inputWrapper}>
<Input
placeholder="CONFIRM PASSWORD"
password
onChangeText={handleChange('passwordConfirm')}
value={values.passwordConfirm}
/>
{errors.passwordConfirm && touched.passwordConfirm && (
<Text style={styles.errorInput}>
{errors.passwordConfirm.toUpperCase()}
</Text>
)}
</View>
<Button onClick={handleSubmit} text="Register" />
</View>
)}
</Formik>
<FlashMessage position="top" />
</KeyboardAwareScrollView>
{showMoreIndicator && (
<View style={styles.moreIndicator}>
<Ionicons
name="ios-arrow-dropdown-circle"
size={32}
color="black"
onPress={this._scrollToEnd}
/>
</View>
)}
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
width: '100%'
},
scrollViewContainer: { flex: 1, width: '100%' },
logo: {
width: 200
},
formWrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%'
},
inputWrapper: {
marginBottom: 48,
width: 180
},
errorInput: {
color: 'red',
textAlign: 'center',
marginTop: 4
},
moreIndicator: { position: 'absolute', right: 16, bottom: 16 }
});
And that's a wrap for this 2 part series.