I'm trying to convert a form to use Material-ui TextField. How do I get my YUP validation to work with that? Here is my code:
import * as React from "react";
import { useState } from 'react';
import { Row, Col } from "react-bootstrap";
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";
import axios from "axios";
import Error from "../../Error";
type FormValues = {
username: string;
password: string;
repeatPassword: string;
fullName: string;
country: string;
email: string;
};
export default function CreatePrivateUserForm(props: any) {
const [errorMessage, setErrorMessage] = useState();
const createPrivateAccountSchema = Yup.object().shape({
username: Yup.string()
.required("Required")
.min(8, "Too Short!")
.max(20, "Too Long!")
.matches(/^[\w-.# ]+$/, {
message: "Inccorect carector"
}),
password: Yup.string()
.required("Required")
.min(10, "Too Short!")
.max(100, "Too Long!")
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s:]).*$/, {
message: "Password need to contain 1 uppercase character (A-Z), 1 lowercase character (a-z), 1 digit (0-9) and 1 special character (punctuation)"
}),
repeatPassword: Yup.string()
.required("Required")
.oneOf([Yup.ref("password")], "Passwords must match")
});
function handleSuccess() {
alert("User was created");
}
async function handleSubmit(values: FormValues) {
const token = await props.googleReCaptchaProps.executeRecaptcha("CreatePrivateUser");
const headers = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
recaptcha: token
}
};
const body = { username: values.username, password: values.password, repeatPassword: values.repeatPassword };
const url = "xxx";
try {
const response = await axios.post(url, body, headers);
if (response.status === 201) {
handleSuccess();
}
if (response.status === 400) {
console.log("Bad Request ...");
setErrorMessage('Bad Request');
} else if (response.status === 409) {
console.log("Conflict ...");
setErrorMessage('Conflict');
} else if (response.status === 422) {
console.log("Client Error ...");
setErrorMessage('Client Error');
} else if (response.status > 422) {
console.log("Something went wrong ...");
setErrorMessage('Something went wrong');
} else {
console.log("Server Error ...");
setErrorMessage('Server Error');
}
} catch (e) {
console.log("Fejl");
}
}
return (
<React.Fragment>
<Row>
<Col xs={12}>
<p>Please register by entering the required information.</p>
</Col>
</Row>
<Row>
<Col xs={12}>
<Formik
initialValues={{ username: "", password: "", repeatPassword: "" }}
validationSchema={createPrivateAccountSchema}
onSubmit={async (values, { setErrors, setSubmitting }) => {
await handleSubmit(values);
setSubmitting(false);
}}>
{({ isSubmitting }) => (
<Form>
{errorMessage ? <Error errorMessage={errorMessage} /> : null}
<Row>
<Col xs={6}>
<Row>
<Col xs={12}>
<TextField
label="Username"
helperText={touched.username ? errors.username : ""}
error={touched.username && Boolean(errors.username)}
type="text"
name="username"
margin="normal"
variant="filled"
/>
<ErrorMessage name='username'>{msg => <div className='error'>{msg}</div>}</ErrorMessage>
</Col>
</Row>
<Row>
<Col xs={12}>
<label htmlFor='password'>Password:</label>
<Field type='password' name='password' />
<ErrorMessage name='password'>{msg => <div className='error'>{msg}</div>}</ErrorMessage>
</Col>
</Row>
<Row>
<Col xs={12}>
<label htmlFor='repeatPassword'>Repeat password:</label>
<Field type='password' name='repeatPassword' />
<ErrorMessage name='repeatPassword'>{msg => <div className='error'>{msg}</div>}</ErrorMessage>
</Col>
</Row>
</Col>
</Row>
<Row>
<Col xs={12}>
<button type='submit' disabled={isSubmitting}>
Create User
</button>
</Col>
</Row>
</Form>
)}
</Formik>
</Col>
</Row>
</React.Fragment>
);
}
The first thing I can see is that you have stripped out the standard formik <Field /> components and directly changed to <TextField />. The <Field /> component is actually a special component that "will automagically hook up inputs to Formik. It uses the name attribute to match up with Formik state" according to the fomik documentation. As a result I believe that formik is no longer actually handling the inputs and instead these are uncontrolled components (html components without a value set by react state). With formik no-longer handling the inputs the Yup validation build into formik and being used via the schema prop won't function correctly.
To solve this issue you can either use a libary (material UI suggests this libary - https://github.com/stackworx/formik-material-ui) or you can make custom input components for formik. This allows you to set the <Field />'s component prop to a component that correctly wires up material UI with the formik data <Field /> exposes.
const CustomTextInput = ({
field, // { name, value, onChange, onBlur }
form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => (
<div>
<TextField
error={_.get(touched, field.name) && _.get(errors, field.name) && true}
helperText={_.get(touched, field.name) && _.get(errors, field.name)}
{...field}
{...props}
/>
</div>
)
Then in your form you can do this
<Field
name="fieldName"
component={CustomTextInput}
label="You can use the Material UI props here to adjust the input"
/>
You can find an example and more information about this on the formik documentation on field - https://jaredpalmer.com/formik/docs/api/field
Related
I'm stuck can a charitable soul advise me : I have a form that wraps 3 tabs
which contain text fields
interface TabPanelProps {
// eslint-disable-next-line react/require-default-props
children?: ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
aria-labelledby={`simple-tab-${index}`}
hidden={value !== index}
id={`simple-tabpanel-${index}`}
role="tabpanel"
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<div>{children}</div>
</Box>
)}
</div>
);
}
function getTab(index: number) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
export default function tt({ closeModal, testData }: any) {
const [value, setvalue] = useState(0);
const methods = useForm<MyInterface>();
const {
register,
handleSubmit,
formState: { errors, isDirty },
} = methods;
const onSubmit = (formValue: MyInterface) => {
console.log("isSubmitted");
console.log(formValue);
};
const handleChangeTab = (event: SyntheticEvent, newValue: number) => {
setvalue(newValue);
};
return (
<FormProvider {...methods} >
< form className="myForm"
onSubmit={methods.handleSubmit(onSubmit)} >
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs aria-label="basic tabs example" onChange={handleChangeTab} value={value}>
<Tab label="1" {...getTab(0)} />
<Tab label="2" {...getTab(1)} />
<Tab label="3" {...getTab(2)} />
</Tabs>
</Box>
<TabPanel index={0} value={value}>
<Component1 />
</TabPanel>
<TabPanel index={1} value={value}>
<Component2 testData={testData} />
</TabPanel>
<TabPanel index={2} value={value}>
<Component3 />
</TabPanel>
</Box>
<DialogActions>
<Button onClick={methods.handleSubmit(onSubmit)}>Enregistrer</Button>
<Button onClick={closeModal}>Annuler</Button>
</DialogActions>
</form>
</FormProvider>
);
}
2 and 3 have for example juste textField which are required in Component 1 but not in Component 2 and 3 :
import { useFormContext } from "react-hook-form";
import { TextField } from '#mui/material';
export default function Component1() {
const { register, formState } = useFormContext();
return (
<>
<TextField
error={Boolean(formState.errors?.name)}
fullWidth
helperText={Boolean(formState.errors?.name) === false ? "give the name" : formState.errors.name?.message}
{...register('name', { required: "name is required." })}
label="name"
variant="standard"
/>
</>
);
}
code in the Component2:
import { Controller, useFormContext } from 'react-hook-form';
import { Checkbox, Chip, FormControl, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent, TextField } from '#mui/material';
export default function Component3(props: any) {
const { register, control } = useFormContext();
return (
<>
<FormControl sx={{ m: 1, minWidth: '100%' }} variant="standard">
<InputLabel id="test">En prod</InputLabel>
<Controller
control={control}
defaultValue=""
name="test"
render={({ field }) => (
<Select labelId="test" {...field}>
<MenuItem value="yes">yes</MenuItem>
<MenuItem value="no">no</MenuItem>
</Select>
)}
/>
</FormControl>
my problem is:
when I change tab without clicking in a field of the form, that is sent even if the required fields are not filled.
On the contrary when I click in any field and the required fields are not filled in isDirty remains true.
Which is normal.
But if I fill in the required fields and I change tab isDirty always stays true and I can't send the form.
Thank you for your help!!
I want to use vue3 together with bootstrap 4/5 with veevalidate 4.
<template>
<Form as="form" #submit.prevent="onFormSubmit" class="needs-validation" :class="{ 'was-validated': wasValidated }">
<div class="form-group">
<label for="firstNameId">First Name *</label>
<Field name="firstname" as="input" id="firstNameId" type="text" rules="required|firstname" class="form-control" placeholder="First Name" v-model="firstName" aria-describedby="input-true input-false input-help" aria-invalid="true" />
<ErrorMessage as="div" name="firstname" v-slot="{ message }">
{{ message }}
<div class="invalid-feedback">
{{ message }}
</div>
</ErrorMessage>
<div class="valid-feedback">Good!</div>
</div>
<Form>
</template>
<script>
import { Field, Form, ErrorMessage, defineRule } from 'vee-validate';
defineRule('required', value => {
if (!value || !value.length) {
return 'This field is required.';
}
return true;
});
defineRule("firstname", (value) => {
if (!/^[a-zA-Z0-9( ),'.:/-]+$/i.test(value)) {
return "Please use only letters, numbers and the following special characters: ( ),'.:/-";
}
return true;
});
export default {
components: {
Form,
Field,
ErrorMessage
},
data () {
return {
firstName: "",
wasValidated: false,
},
methods: {
onFormSubmit(values) {
alert(JSON.stringify(values, null, 2));
console.log("Submitted");
console.log(values);
var forms = document.getElementsByClassName('needs-validation');
// Loop over them and prevent submission
var validation = Array.prototype.filter.call(forms, function(form) {
form.addEventListener('submit', function(event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
this.wasValidated = true;
}, false);
});
},
},
};
</script>
The problem is that I can't activate the div with the class invalid-feedback or valid-feedback.
I could add the class was-validated to the <form>-tag, but I get feedback first after the second click on the submit button.
<template>
<Form as="form"
#submit="onFormSubmit"
class="needs-validation"
:validation-schema="schema"
v-slot="{ errors }"
>
<div class="form-group">
<label for="firstNameId">First Name *</label>
<Field
name="firstName"
type="text"
placeholder="First Name"
v-model="firstName"
aria-describedby="input-true input-false input-help"
aria-invalid="true"
v-slot="{ meta, field }"
>
<input
v-bind="field"
name="firstName"
type="text"
class="form-control"
:class="{
'is-valid': meta.valid && meta.touched,
'is-invalid': !meta.valid && meta.touched,
}"
/>
</Field>
<ErrorMessage as="div" name="firstname" v-slot="{ message }" class="invalid-feedback">
{{ message }}
</ErrorMessage>
<Form>
</template>
<script setup>
import { markRaw } from 'vue';
import { Field, Form, ErrorMessage} from 'vee-validate';
import * as yup from 'yup';
</script>
<script>
export default {
components: {
Form,
Field,
ErrorMessage
},
data () {
return {
schema: markRaw(yup.object().shape({
firstName: yup.string().min(0).max(20).label('First Name'),
})),
};
},
methods: {
onFormSubmit(values) {
console.log(JSON.stringify(values, null, 2));
},
},
};
</script>
I'm fairly new Ionic React and trying to create an App using Ionic framework. I have a use case where I would like to redirect to another page upon a button press and validation from a function.
Here is the component I am working with
import React, { useState } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonInput,
IonButton,
IonRow,
IonCol,
useIonViewWillEnter,
useIonViewWillLeave,
IonLabel
} from '#ionic/react';
import './LogIn.css'
import MainMenu from "../MainMenu";
const LogIn: React.FC<{register?: boolean; onClose?: any}> = () => {
const [email, setEmail] = useState<string>();
const [password, setPassword] = useState<string>();
function handleLoginButtonPress() {
if (validateEmail() && password !== undefined && password.trim() !== "") {
// Network call here to check if a valid user
console.log("valid email")
return <MainMenu/>
}
}
function validateEmail () {
if (email !== undefined && email.trim() !== "") {
const regexp = /^(([^<>()[\]\\.,;:\s#"]+(\.[^<>()[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regexp.test(email);
}
return false;
}
useIonViewWillEnter(() => {
console.log("Entering LogIn");
});
useIonViewWillLeave(() => {
console.log("Exiting LogIn");
});
return (
<IonPage id="loginIonPage">
<IonHeader>
<IonToolbar color="primary" mode="md">
<IonTitle class="ion-text-center">Login or Register</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent id="loginPage">
<div id="loginDialog">
<form>
<IonLabel>Email</IonLabel>
<IonRow>
<IonCol id="userName">
<IonInput
name="email"
value={email}
onIonChange={e => setEmail(e.detail.value!)}
clearInput
type="email"
placeholder="Email"
class="input"
padding-horizontal
clear-input="true"
required/>
</IonCol>
</IonRow>
<IonLabel>Password</IonLabel>
<IonRow>
<IonCol id="password">
<IonInput
clearInput
name="password"
value={password}
onIonChange={e => setPassword(e.detail.value!)}
type="password"
placeholder="Password"
class="input"
padding-horizontal
clear-input="true"
required/>
</IonCol>
</IonRow>
</form>
</div>
<div id="buttons">
<IonButton
onClick={handleLoginButtonPress}
type="submit"
strong={true}
expand="full"
style={{ margin: 20 }}>
Log in
</IonButton>
<IonButton
routerLink="/registerUser"
type="submit"
strong={true}
expand="full"
style={{ margin: 20 }}>
Register
</IonButton>
</div>
</IonContent>
</IonPage>
);
};
export default LogIn;
How can I redirect to another page from the function handleLoginButtonPress which is invoked upon clicking on Login button. I tried reading through ionic doc but that did not help as it redirects whenever there is button click or click on an IonElement like IonLabel.
Use react-router-dom hooks
https://reacttraining.com/react-router/web/api/Hooks/usehistory
import { useHistory } from "react-router-dom";
const ExampleComponent: React.FC = () => {
const history = useHistory();
const handleLoginButtonPress = () => {
history.push("/main");
};
return <IonButton onClick={handleLoginButtonPress}>Log in</IonButton>;
};
export default ExampleComponent;
Thanks, #MUHAMMAD ILYAS and it would have worked for me. But I managed to leverage the react state as below, I have added as an answer as I could not format the code there,
const ValidateUser : React.FC = () => {
const [login, setLogin] = useState(false);
function isUserLoggedIn() {
return login;
}
return(
<IonReactRouter>
<IonRouterOutlet>
<Route path="/" render={() => isUserLoggedIn()
? <MainMenu/>
: <LogIn setUserLoggedIn={() => {setLogin(true)}}/>} exact={true}/>
</IonRouterOutlet>
</IonReactRouter>
);
};
const LogIn: React.FC<{setUserLoggedIn?: any}> = ({setUserLoggedIn}) => {
.
.
function onLoginButtonPress() {
setUserLoggedIn();
}
}
Thanks
This issue applied the standard implementation without additional customisations.
The is no value on submit of the form and onChange does not fire with the current value.
<Form onSubmit={handleSubmit(this.onSubmit)}>
<Form.Group>
<Field
component={SemanticDatepicker}
name="working"
dateFormat="DD/MM/YYYY"
label="Date of birth"
placeholder="select your DOB"
size="small"
onChange={(e, value) => {
console.log(e, value);
}}
/>
</Form.Group>
<Form.Field
control={Button}
color="purple"
className="submit-btn"
type="submit"
width={6}
>
Save
</Form.Field>
</Form>
A minimal version can be found here https://github.com/chrishj59/datepickerIssue
I could capture the onChange value by creating a custom component which wrapped the <SemanticDatepicker />. I also added mapStateToProps to log the redux-form values. You can consider this as a starting point and work on this.
//SongList.js
state = {
date: "",
};
render(){
const DatePick = () => {
return (
<SemanticDatepicker
onChange={this.datePickerOnChange}
format="YYYY-MM-DD"
/>
);
};
return (
<Form onSubmit={handleSubmit(this.onSubmit)}>
<Form.Group>
<Field
component={DatePick}
name="working"
dateFormat="DD/MM/YYYY"
label="Date of birth"
placeholder="select your DOB"
size="small"
/>
</Form.Group>
<Form.Field
control={Button}
color="purple"
className="submit-btn"
type="submit"
width={6}
>
Save
</Form.Field>
</Form>
);
}
//App.js
import { connect } from "react-redux";
const App = (props) => {
console.log(props.form);
return (
<div>
{" "}
<SongList />
</div>
);
};
const mapStateToProps = (state) => ({
form: state.form,
});
export default connect(mapStateToProps)(App);
I managed to resolve using semantic-ui-redux-form-fields to wrap the Semantic UI component. This now gives the correct format and value appearing in the validate function and formProps.
import React from "react";
imp ort { fieldEnhance } from "semantic-ui-redux-form-fields";
import { compose } from "redux";
import SemanticDatepicker from "react-semantic-ui-datepickers";
import "react-semantic-ui-datepickers/dist/react-semantic-ui-datepickers.css";
const DatePickerPure = (props) => {
const { currentValue, input, ...rest } = props;
const defaultProps = {
format: "DD/MMM/YYYY",
onChange: (event, data) => input.onChange(data.value),
value: currentValue,
...rest,
};
return <SemanticDatepicker {...props} {...defaultProps} />;
};
export default compose(fieldEnhance)(DatePickerPure);
I created form with formik.
I want to edit record with the form. I want to fetch data from database and fill the form with them.
However with no success.
Here is my code.
class UserForm extends Component {
componentWillMount(){
this.setState({firstName:''})
var repository = new Repository();
repository.getUser(function(user){
this.setState({
firstName: user.firstName,
});
}.bind(this),this.props.currentId)
}
render() {
return (
<Formik
initialValues={{
firstName: this.state.firstName,
}}
validationSchema = {Yup.object().shape({
firstName: Yup.string().required('First name is required'),
})}
onSubmit={item => { this.props.addRecordToTable(item)}}
render={({ setFieldValue, values, errors, touched, isSubmitting }) =>
<Form>
<div>
{ touched.firstName && errors.firstName && <p>{errors.firstName}</p> }
<Field type="input" name="firstName" placeholder="First name"/>
</div>
<input type="submit"></input>
</Form>}
/>
);
}
}
export default UserForm;
Input firstName will never be filled with loaded data.
How can i solve this?
Set enableReinitialize to true in <Formik ... />