I want to share my 3 approaches of handling cascading form fields.
- First one is general common approach, using state variables.
- Second is to use ordinary variables and one boolean state variable to trigger the state effect (refresh page).
- Third is, dynamic form fields with ordinary variables.
First Approach, using state variables
Here in this post we see the second approach, using ordinary variables to handle cascading form fields based on country, state, city data.
Packages
react-native-element-dropdown
react-native-paper
we are using react-native-element-dropdown for drop down fields
Base Page
import React, { useState, useEffect } from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { Dropdown } from "react-native-element-dropdown";
export default function App() {
return (
<View style={styles.container}>
<View>
<Text>Country</Text>
<ZDropDown
data={[]}
labelField=""
valueField=""
value={null}
isFocus={false}
onChange={null}
/>
<Text>State</Text>
<ZDropDown
data={[]}
labelField=""
valueField=""
value={null}
isFocus={false}
onChange={null}
/>
<Text>City</Text>
<ZDropDown
data={[]}
labelField=""
valueField=""
value={null}
isFocus={false}
onChange={null}
/>
</View>
<View>
<Text>Selected Country</Text>
<Text style={styles.selectedValue}>{country.selectedCountry.name}</Text>
<Text>Selected State</Text>
<Text style={styles.selectedValue}>{state.selectedState.name}</Text>
<Text>Selected City</Text>
<Text style={styles.selectedValue}>{city.selectedCity.name}</Text>
</View>
<TouchableOpacity onPress={null} style={styles.clrBtn}>
<Text style={styles.clrBtnTxt}>Reset</Text>
</TouchableOpacity>
</View>
);
}
const ZDropDown = ({
data,
labelField,
valueField,
value,
onFocus,
onBlur,
onChange,
isFocus,
}) => {
return (
<Dropdown
mode={"auto"}
style={[styles.dropdown, isFocus ? { borderColor: "dodgerblue" } : {}]}
placeholderStyle={styles.placeholderStyle}
selectedTextStyle={styles.selectedTextStyle}
inputSearchStyle={styles.inputSearchStyle}
iconStyle={styles.iconStyle}
search={data.length > 5}
maxHeight={300}
searchPlaceholder="Search..."
data={data}
labelField={labelField}
valueField={valueField}
placeholder={!isFocus ? "Select item" : "..."}
value={value}
onFocus={onFocus}
onBlur={onBlur}
onChange={onChange}
/>
);
};
const styles = StyleSheet.create({
// style props
});
ZDropDown
is a custom component.
Sample data
const listCountry = [
{ countryId: "1", name: "india" },
{ countryId: "2", name: "uk" },
{ countryId: "3", name: "canada" },
{ countryId: "4", name: "us" },
];
const listSate = [
{ stateId: "1", countryId: "1", name: "state1_india" },
{ stateId: "4", countryId: "2", name: "state1_uk" },
{ stateId: "7", countryId: "3", name: "state1_canada" },
{ stateId: "10", countryId: "4", name: "state1_us" },
];
const listCity = [
{
cityId: "1",
stateId: "1",
countryId: "1",
name: "city1_state1_country1",
},
{
cityId: "6",
stateId: "2",
countryId: "1",
name: "city6_state2_country1",
},
{
cityId: "7",
stateId: "3",
countryId: "1",
name: "city7_state3_country1",
},
{
cityId: "23",
stateId: "8",
countryId: "3",
name: "city23_state8_country3",
},
{
cityId: "30",
stateId: "10",
countryId: "4",
name: "city30_state10_country4",
},
{
cityId: "35",
stateId: "12",
countryId: "4",
name: "city35_state12_country4",
},
{
cityId: "36",
stateId: "12",
countryId: "4",
name: "city36_state12_country4",
},
];
Form Variables
In the first approach we used 4 state variables but here we use 4 ordinary variables (3 for dropdown, 1 for focus field) and a state variable which is used to switch and trigger the state effect (refresh page).
var country = {
list: [],
selectedCountry: {},
selectedValue: null,
};
var state = {
list: [],
selectedState: {},
selectedValue: null,
};
var city = {
list: [],
selectedCity: {},
selectedValue: null,
};
var focusField = '';
export default function App() {
const [refreshPage, setRefreshPage] = useState(false);
return (
<View style={styles.container}>
<View>
<Text>Country</Text>
<ZDropDown
data={country.list}
labelField="name"
valueField="countryId"
value={country.selectedValue}
isFocus={focusField === 'country'}
onChange={null}
/>
<Text>State</Text>
<ZDropDown
data={state.list}
labelField="name"
valueField="stateId"
value={state.selectedValue}
isFocus={focusField === 'state'}
onChange={null}
/>
<Text>City</Text>
<ZDropDown
data={city.list}
labelField="name"
valueField="cityId"
value={city.selectedValue}
isFocus={focusField === 'city'}
onChange={null}
/>
</View>
. . .
</View>
);
}
Focus and Blur events get triggered more than onChange event so it is better to maintain a separate variable to represent the focus field and avoid data mess up situations.
Load Country
Load country dropdown from the sample data. (you can use api call)
export default function App() {
. . .
const loadCountry = () => {
// load data from api call
country.list = [...listCountry];
// switching value will refresh the ui
setRefreshPage(!refreshPage);
};
useEffect(() => {
loadCountry();
}, []);
return (
. . .
)
Focus / Blur
We're using a function to set the focus field and refresh the page by switching the state variable(refreshPage)
const changeFocusField = (fld = '') => {
focusField = fld;
setRefreshPage(!refreshPage);
};
<Text>Country</Text>
<ZDropDown
. . .
isFocus={focusField === 'country'}
onFocus={() => changeFocusField('country')}
onBlur={() => changeFocusField('')}
onChange={null}
/>
<Text>State</Text>
<ZDropDown
. . .
isFocus={focusField === 'state'}
onFocus={() => changeFocusField('state')}
onBlur={() => changeFocusField('')}
onChange={null}
/>
<Text>City</Text>
<ZDropDown
. . .
isFocus={focusField === 'city'}
onFocus={() => changeFocusField('city')}
onBlur={() => changeFocusField('')}
onChange={null}
/>
We are half way done now.
Load State STATE
On country selected, we need to load the respective states STATES based on the country selection.
Update country field, focus off country and load STATES based on country.
<Text>Country</Text>
<ZDropDown
. . .
onChange={(item) => {
country.selectedCountry = item;
country.selectedValue = item.countryId;
focusField = '';
loadState();
}}
/>
Did you see the difference in the first approach? Previously there were 3 state variables updated but here only one state variable gets updated.
When country changed, both states and cities will be changed. So before setting up the new value we need to clear the existing data.
const loadState = async () => {
state = { list: [], selectedState: {}, selectedValue: null };
city = { list: [], selectedCity: {}, selectedValue: null };
const arr = listSate.filter(
(ele) => ele.countryId === country.selectedValue
);
if (arr.length) {
state.list = arr;
}
refreshPage(!refreshPage);
};
Load City
And load city field based on country and state.
<Text>State</Text>
<ZDropDown
. . .
onChange={(item) => {
state.selectedValue = item.stateId;
state.selectedState = item;
changeFocusField('');
loadCity();
}}
/>
const loadCity = async () => {
city = { list: [], selectedCity: {}, selectedValue: null };
const arr = listCity.filter((ele) => ele.stateId === state.selectedValue);
if (arr.length) city.list = arr;
setRefreshPage(!refreshPage);
};
All set, the form fields are working properly now.
2 more additional features, one is resetting the page and the other one is show warning.
Reset page
Form variables and focus variable should be cleared.
. . .
const resetForm = () => {
country = {list: [...listCountry],selectedCountry: {}, selectedValue: null};
state = { list: [], selectedState: {}, selectedValue: null };
city = { list: [], selectedCity: {}, selectedValue: null };
focusField = '';
setRefreshPage(!refreshPage);
};
. . .
<TouchableOpacity onPress={() => resetForm()} style={styles.clrBtn}>
<Text style={styles.clrBtnTxt}>Reset</Text>
</TouchableOpacity>
. . .
Warning
We have to show a warning msg if the parent field value is null. For that we are using SnackBar component from paper.
import { Snackbar } from "react-native-paper";
. . .
var snackMsg = '';
export default function App() {
. . .
const [visible, setVisible] = useState(false);
const onToggleSnackBar = () => setVisible(!visible);
const onDismissSnackBar = () => setVisible(false);
. . .
return (
<View style={styles.container}>
. . .
<Snackbar duration={2000} visible={visible} onDismiss={onDismissSnackBar}>
{snackMsg}
</Snackbar>
</View>
);
}
We moved snackMsg to ordinary variable from state variable.
Since State and City fields have parent field, they have to be validated.
<Text>State</Text>
<ZDropDown
onFocus={() => {
focusField('state');
if (!country.selectedValue) {
snackMsg = 'Select country';
onToggleSnackBar();
changeFocusField('country');
}
}}
. . .
/>
<Text>City</Text>
<ZDropDown
onFocus={() => {
focusField('city');
if (!country.selectedValue) {
snackMsg = 'Select country';
onToggleSnackBar();
changeFocusField('country');
} else if (!state.selectedValue) {
snackMsg = 'Select state';
onToggleSnackBar();
changeFocusField('state');
}
}}
. . .
/>
Done.
Thank you.
Full code reference here
Top comments (0)