Accessing value of child component within Formik form - reactjs

I have a form that uses Formik and contains two fields - email and siteID
The email field uses a TextInput whereas siteID comes from my own component called DropDown that uses react-native-dropdown-picker
When my form is submitted I can see the value of email within handleSubmit() but siteID remains as 0 and not the selected value.
How can I access the selected value from my DropDown child component when submitting the parent form?
My form
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.loginContainer}>
<Formik
validationSchema={loginValidationSchema}
initialValues={{
siteID: 0,
email: "",
}}
onSubmit={(values) => handleSubmit(values)}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
isValid,
}) => (
<>
<Text style={styles.label}>Email</Text>
<TextInput
name="email"
style={styles.textInput}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
keyboardType="email-address"
/>
<DropDown name="siteID" />
</>
)}
</Formik>
</View>
</SafeAreaView>
);
DropDown component
import * as React from 'react';
import {StyleSheet} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {usePostRequest} from '../../client';
import {useState, useEffect} from 'react';
const DropDown = () => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickervalue, pickersetValue] = useState(null);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={pickervalue}
items={pickeritems}
setOpen={pickersetOpen}
setValue={pickersetValue}
setItems={pickersetItems}
/>
);
};

Based on the docs, to use your own component, you should wrap your component in a Field as follows:
const DropDownField = <Field name="siteId" component={DropDown} />
When using a Field formik will inject props (name, value, onChange, onBlur) into your component allowing you to control the form state.
So you can then redeclare your component with those props as follows:
import * as React from 'react';
import {StyleSheet} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {usePostRequest} from '../../client';
import {useState, useEffect} from 'react';
const DropDown = ({ field: { name, value, onChange }) => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={value}
items={pickeritems}
setOpen={pickersetOpen}
setValue={onChange}
setItems={pickersetItems}
name={name}
/>
);
};
Essentially you wire your component up to the onChange and value props. Since you assigned a name of siteId to the DropDownField component (which you should now use in your form), any time you call onChange site Id will update.
Link to docs

You can use setFieldValue provided by Formik to set a field manually.
A good example of usage is provided in this example
You can pass setFieldValue to your DropDown Component:
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.loginContainer}>
<Formik
validationSchema={loginValidationSchema}
initialValues={{
siteID: 0,
email: "",
}}
onSubmit={(values) => handleSubmit(values)}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
isValid,
setFieldValue,
}) => (
<>
<Text style={styles.label}>Email</Text>
<TextInput
name="email"
style={styles.textInput}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
keyboardType="email-address"
/>
<DropDown name="siteID" setSiteID={setFieldValue} />
</>
)}
</Formik>
</View>
</SafeAreaView>
);
And trigger it from inside of the DropDown Component:
const DropDown = ({ setSiteID }) => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickervalue, pickersetValue] = useState(null);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
// You can use useEffect or create a custom handleValueChange
// and pass it to DropDownPicker
useEffect(() => {
setSiteID('siteID', pickervalue);
}, [pickersetValue])
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={pickervalue}
items={pickeritems}
setOpen={pickersetOpen}
setValue={pickersetValue}
setItems={pickersetItems}
/>
);
};

try this,
get setFieldValue from formik props.
add onChangeDropdown to <Dropdown /> like,
<DropDown name="siteID" onChangeDropdown={(value)=>{
setFieldValue("siteID",value)
}}/>
get the props inside Dropdown component,
const DropDown = (props) => {
add onChangeValue prop to your DropDownPicker
onChangeValue={props.onChangeDropdown}

Related

Reset and setFieldValue with react-seelect within Formik

I have a Formik form which uses check-boxes and selects. For the select field I am using react-select. There is no problem to submit the form but when I want to reset the form the react-select part is not clearing and also when I want to prefill the form after a push of a button with setFieldValue, again the react-select part is not responding.
How can I make it work this form with react-select within Formik?
https://codesandbox.io/s/wonderful-breeze-dyhi3b
App.js
import React from "react";
import FormikReactSelect from "./components/FormikReactSelect";
function App() {
return <FormikReactSelect />
}
export default App;
FormikReactSelect.js
import React, {useRef} from "react";
import {Form, Field, Formik, FieldArray} from "formik";
import SelectField from "./SelectField";
function FormikReactSelect() {
const formikRef = useRef();
const drinkDist = ["Wine", "Beer", "Whiskey"];
const countryDist = ["US", "FR", "DE", "BE", "IT"];
const arrToValLab = (arr) => {
if (arr !== undefined) {
const valLab = arr.map((t) => ({
value: t,
label: t,
}));
return valLab;
} else {
return null;
}
};
const onClick = () => {
if (formikRef.current) {
formikRef.current.setFieldValue("drinks", ["Whiskey"]);
}
if (formikRef.current) {
formikRef.current.setFieldValue("countries", ["FR"]);
}
};
return (
<>
<Formik
innerRef={formikRef}
enableReinitialize={true}
initialValues={{drinks: [], countries: []}}
>
{({
values,
handleChange,
handleSubmit,
handleBlur,
resetForm,
setFieldValue,
}) => (
<Form noValidate>
Drinks:
<FieldArray
name="drinks"
render={(arrayHelpers) => (
<div>
{drinkDist.map((r, i) => (
<div key={i}>
<label>
<Field
name="drinks"
type="checkbox"
value={r}
checked={values.drinks.includes(r)}
onChange={(e) => {
if (e.target.checked) {
arrayHelpers.push(r);
} else {
const idx = values.drinks.indexOf(r);
arrayHelpers.remove(idx);
}
}}
/>
{" " + r}
</label>
</div>
))}
</div>
)}
/>
Countries:
<Field
component={SelectField}
name="countries"
options={arrToValLab(countryDist)}
/>
<button
type="button"
onClick={() => {
resetForm();
}}
>
Reset
</button>
{JSON.stringify(values, null, 2)}
</Form>
)}
</Formik>
Sending preset values:
<button onClick={onClick}>Set Field</button>
</>
);
}
export default FormikReactSelect;
SelectFireld.js
import React from "react";
import Select from "react-select";
import {useField} from "formik";
export default function SelectField(props) {
const [field, state, {setValue, setTouched}] = useField(props.field.name); // eslint-disable-line
const onChange = (value) => {
let arrValue = [];
value.map((k) => arrValue.push(k.value));
setValue(arrValue);
};
return <Select {...props} isMulti onChange={onChange} onBlur={setTouched} />;
}

TypeError: undefined React Hooks Form- React Native

I have called Controller from react-hook-form in a separate component in my react native app and displaying that component on login screen that is showing this error:
TypeError: undefined is not an object (evaluating 'e.substring')
CustomInput.js
import { View, TextInput, StyleSheet } from 'react-native'
import React from 'react'
import { Controller } from 'react-hook-form';
const CustomInput = ({control, name, placeholder, secureTextEntry}) => {
return (
<View style={styles.container}>
<Controller control={control} name={name} render={({field: {value, onChange, onBlur}, fieldState: {error}}) =>
<TextInput style={styles.input} value={value} onChangeText={onChange} onBlur={onBlur} placeholder={placeholder} secureTextEntry={secureTextEntry} /> } />
</View>
);
};
LoginScreen.js
import CustomInput from '../../components/CustomInput/CustomInput';
import { useNavigation } from '#react-navigation/native';
import {useForm} from 'react-hook-form';
const LoginScreen = () => {
const {control, handleSubmit, formState: { errors } } = useForm();
const navigation = useNavigation();
const onLoginPressed = (data) => {
console.log(data, errors)
};
const onForgotPasswordPressed = () => {
navigation.navigate('ForgotPassword');
}
const onRegisterPressed = () => {
navigation.navigate('Register')
}
return (
<ScrollView contentContainerStyle={{ flexGrow: 1, justifyContent: 'center' }} showsVerticalScrollIndicator={false}>
<View style={styles.root}>
<Image source={Logo} style={[styles.logo, {height : height * 0.2}]} resizeMode={'contain'} />
//CUSTOM INPUT HOLDING THE CONTROLLER
<CustomInput name='username' placeholder='Username' control={control} />
<CustomInput password='password' placeholder='Password' secureTextEntry={true} control={control}/>
<CustomButton text={loading ? 'Loading...' : 'Login Account'} onPress={handleSubmit(onLoginPressed)} />
</View>
</ScrollView>
);
};
However, If I directly add the controller in the Login Screen Like this (React Hooks form showing undefined username in logs) it works fine

Focusing the next input after reaching the max length React Native

I want to be able to move the focus from one input onto the next after the user has entered 2 numbers in the input box. Instead of the user manually clicking the next field I'd like the focus to switch there automatically.
Below is how I've currently implemented my two input fields.
I've tried the things mentioned here using refs but doesn't seem to work at all https://www.codegrepper.com/code-examples/csharp/react+native+textinput+focus
<Controller
control={form.control}
defaultValue={initialMonthValue}
name={monthName}
render={({ onChange, onBlur, value }) =>
<Input
containerStyle={{ width:80 }}
editable={!isSubmitting}
autoCapitalize="none"
numberOfLines={1}
multiline={false}
keyboardType='number-pad'
placeholder="MM"
maxLength={2}
placeholderTextColor={Colors.Grey}
onChangeText={onChange}
onBlur={onBlur}
secureTextEntry={false}
invalid={!!monthError}
value={value} />}
/>
<Slash text="/" />
<Controller
control={form.control}
defaultValue={initialYearValue}
name={yearName}
render={({ onChange, onBlur, value }) =>
<Input
containerStyle={{ width:80 }}
editable={!isSubmitting}
autoCapitalize="none"
numberOfLines={1}
multiline={false}
keyboardType='number-pad'
placeholder="YY"
placeholderTextColor={Colors.Grey}
onChangeText={onChange}
onBlur={onBlur}
secureTextEntry={false}
invalid={!!yearError}
value={value} />}
/>
If I understand what are you looking to implement then this should work for you.
import { useRef, useEffect, useState } from "react";
export default function FormInputs() {
const inputRef = useRef(null);
const [length, setLength] = useState(0);
const handleChange = (e) => {
// Handle type checking here
setLength(length + 1);
};
useEffect(() => {
// if the Ref doesn't exist for some reason then break.
if (!inputRef) return;
const input = inputRef.current;
if (length === 2) input.focus();
}, [length]);
return (
<>
<input onChange={handleChange} />
<input ref={inputRef} />
</>
);
}
https://codesandbox.io/s/wonderful-platform-no55n?file=/src/App.js:77-590
if you would love me to help you implement this on your code then don't hesitate to share the components needed for the code to be functional.
You can use TextInput's focus function. Here is a working example:
import React from 'react';
import {SafeAreaView, StyleSheet, TextInput} from 'react-native';
const App = () => {
const [textInputValue1, setTextInputValue1] = React.useState('');
const textInputRef2 = React.useRef<TextInput | null>();
const [textInputValue2, setTextInputValue2] = React.useState('');
return (
<SafeAreaView>
<TextInput
style={styles.textInput}
value={textInputValue1}
onChangeText={(value) => {
if (value.length >= 2) {
textInputRef2.current?.focus(); // Focus on the other TextInput
}
setTextInputValue1(value);
}}
/>
<TextInput
style={styles.textInput}
ref={(ref) => (textInputRef2.current = ref)}
value={textInputValue2}
onChangeText={(value) => setTextInputValue2(value)}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
textInput: {
backgroundColor: 'gray',
margin: 20,
minHeight: 40,
},
});
export default App;
You can focus other Input's ref when you reach maxLength on first input' maxLength prop's value like this:
import React, {useRef} from 'react';
import {View, TextInput} from 'react-native';
const App = () => {
const secondTextInputRef = useRef(null);
const [firstTextInputValue, setFirstTextInputValue] =
React.useState('');
const [secondTextInputValue, setSecondTextInputValue] =
React.useState('');
const maxInputSize = 3;
const App = () => {
return (
<View>
<TextInput
value={firstTextInputValue}
maxLength={maxInputSize}
onChangeText={(value) => {
setFirstTextInputValue(value);
if (value.length >= maxInputSize) {
secondTextInputRef.current?.focus();
}
}}/>
<TextInput
ref={(ref) => (secondTextInputRef.current = ref)}
value={secondTextInputValue}
onChangeText={(value) => setSecondTextInputValue(value)} />
</View>
);
};
export default App;

Material-UI Autocomplete, React Hook Form - Changing InputValue in Material UI Autocomplete with Use State in an OnChange?

I've been customising a Material UI Autocomplete within a Controller from React Hook Form, as part of a much larger form, with some difficulty.
The dropdown lists suggestions drawn from the database (props.items, represented here as objects) and if the suggestion is not there, there's the option to add a new one in a separate form with a button from the dropdown. This 'secondComponent' is opened with conditional rendering.
As it gets passed to the second form, the data is stored in state (heldData) and then passed back into the form via React Hook Form's reset, here as reset(heldData).
This updates the value of the form perfectly, as I have an onChange event that sets the value according to what was passed in. React Hook Form handles that logic with the reset and gives the full object to the onChange.
However, I also want to set the InputValue so that the TextField is populated.
In order to create a dynamic button when there are no options ('Add ....(input)... as a guest'), I store what is typed into state as 'texts'. I thought that I could then use the OnChange event to use the same state to update the inputValue, as below. However, when I setTexts from the onChange, the change isn't reflected in the inputValue.
Perhaps this is because the useState is async and so it doesn't update the state, before something else prevents it altogether. If so, it's much simpler than the other code that I have included, but wasn't certain. I have excluded most of the form (over 500 lines of code) but have tried to keep any parts that may be appropriate. I hope that I have not deleted anything that would be relevant, but can update if necessary.
Apologies. This is my first question on Stack Overflow and I'm quite new to React (and coding) and the code's probably a mess. Thank you
**Form**
import React, { useState, useEffect} from "react";
import AutoCompleteSuggestion from "../general/form/AutoCompleteSuggestion";
import SecondComponent from './SecondComponent'
import { useForm } from "react-hook-form";
const items = {
id: 2,
name: "Mr Anderson"
}
const items2 = {
id: 4,
name: "Mr Frog"
}
const defaultValues = {
guest: 'null',
contact: 'null',
}
const AddBooking = () => {
const { handleSubmit, register, control, reset, getValues} = useForm({
defaultValues: defaultValues,
});
const [secondComponent, setSecondComponent] = useState(false);
const [heldData, setHeldData] = useState(null)
const openSecondComponent = (name) => {
setSecondComponent(true)
const data = getValues();
setHeldData(data);
}
useEffect(() => {
!secondComponent.open?
reset(heldData):''
}, [heldData]);
const onSubmit = (data) => {
console.log(data)
};
return (
<>
{!secondComponent.open &&
<form onSubmit={handleSubmit(onSubmit)}
<AutoCompleteSuggestion
control={control}
name="guest"
selection="id"
label="name"
items={items}
openSecondComponent={openSecondComponent}
/>
<AutoCompleteSuggestion
control={control}
name="contact"
selection="id"
label="name"
items={items2}
openSecondComponent={openSecondComponent}
/>
</form>
};
{secondComponent.open?
<SecondComponent/>: ''
};
</>
);
};
And this is the customised AutoComplete:
**AutoComplete**
import React, { useState } from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete, from "#material-ui/lab/Autocomplete";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";
import { Controller } from "react-hook-form";
import Button from "#material-ui/core/Button";
const AutoCompleteSuggestion = (props) => {
const [texts, setTexts] = useState('');
return (
<>
<Controller
name={props.name}
control={props.control}
render={({ onChange }) => (
<Autocomplete
options={props.items}
inputValue={texts} //NOT GETTING UPDATED BY STATE
debug={true}
getOptionLabel={(value) => value[props.label]}
noOptionsText = {
<Button onClick={()=> props.opensSecondComponent()}>
Add {texts} as a {props.implementation}
</Button>}
onChange={(e, data) => {
if (data==null){
onChange(null)
} else {
onChange(data[props.selection]); //THIS ONCHANGE WORKS
setTexts(data[props.label]) //THIS DOESN'T UPDATE STATE
}}
renderInput={(params) => (
<TextField
{...params}
onChange = { e=> setTexts(e.target.value)}
/>
)}
renderOption={(option, { inputValue }) => {
const matches = match(option[props.label1, inputValue);
const parts = parse(option[props.label], matches);
return (
<div>
{parts.map((part, index) => (
<span
key={index}
style={{ fontWeight: part.highlight ? 700 : 400 }}
>
{part.text}
</span>
))}
</div>
);
}}
/>
)}
/>
</>
);
};
export default AutoCompleteSuggestion;

Validate nested component form item from parent form with Ant Design

Is there a way I can validate a nested component form item from its parent form when submitting?
Here is an example, I know I could handle both items in the same form and this is not the best way to pass the data from child to parent component (It's just a simple example to illustrate my question). I have a more complex situation than this example..
App.js
import React from "react";
import { FormComponentProps } from "antd/lib/form/Form";
import { Input, Form } from "ant";
import ChildComponent from "./ChildComponent";
function App() {
const [state, setState] = React.useState("");
const [childValue, setChildValue] = React.useState("");
const handleSubmit = e => {
e.preventDefault();
props.form.validateFields((err, values) => {
if (!err) {
console.log(state);
console.log(childValue);
}
});
};
const { getFieldDecorator } = props.form;
return (
<Form onSubmit={handleSubmit}>
<Form.Item
label="Name"
labelAlign="left"
colon={false}
labelCol={{ span: 8 }}
wrapperCol={{ span: 14 }}
required={false}
>
{getFieldDecorator("name", {
rules: [
{
required: true
}
]
})(
<Input
type="text"
name="Name"
onChange={e => setState(e.target.value)}
/>
)}
</Form.Item>
<ChildComponent setChildValue={setChildValue} />
</Form>
);
}
const WrappedApp = Form.create({ name: "App" })(App);
export default WrappedApp;
ChildComponent.js
import React, { useEffect } from "react";
import { Input, Form } from "antd";
function ChildComponent(props) {
const [state, setState] = React.useState("");
useEffect(() => {
props.setChildValue(state);
}, [state]);
const { getFieldDecorator } = props.form;
return (
<Form.Item
label="Last"
labelAlign="left"
colon={false}
labelCol={{ span: 8 }}
wrapperCol={{ span: 14 }}
required={false}
>
{getFieldDecorator("last", {
rules: [
{
required: true
}
]
})(
<Input
type="text"
name="Last"
onChange={e => setState(e.target.value)}
/>
)}
</Form.Item>
);
}
export default ChildComponent;
When I submit the form from App.js, I want to also check the validation of item from the ChildComponent.js.
Is there a way I can pass a reference from App.js form to validate the item in ChildComponent?
P.s.
I have tried passing props.form from App.js to ChildComponent.js. It didn't check the validation for the ChildComponent item.

Resources