Yup schema - `when` method is not working - reactjs

Recently I am suffering with creating yup schema. Since I am doing it I see that the method when() totally doesn't work for me like it should as documentation said and other solutions I found on the internet.
When my checkbox is checked to true all the fields on the schema should be required, but they don't. As seen in my example I tried three ways, and none of them is working. Maybe there is someone who knows what am I doing wrong?
My test code is:
import "./styles.css";
import * as yup from "yup";
import { FormProvider, useForm } from "react-hook-form";
import { yupResolver } from "#hookform/resolvers/yup";
import React, { useEffect } from "react";
export default function App() {
const schema = yup.object({
isRequired: yup.bool(),
firstName: yup.string().when("isRequired", (_, schema) => {
return schema.required();
}),
lastName: yup.string().when("isRequired", () => {
return yup.string().required();
}),
contact: yup.string().when("isRequired", {
is: true,
then: yup.string().required("Required")
})
});
const methods = useForm({
mode: "all",
defaultValues: {
isRequired: true,
firstName: "",
lastName: "",
contact: ""
},
resolver: yupResolver(schema),
shouldUnregister: true
});
const { register, watch } = methods;
const isRequired = watch("isRequired");
useEffect(() => {
console.log(schema.fields);
}, [isRequired, schema]);
return (
<FormProvider {...methods}>
<form autoComplete="off" noValidate>
<input type="checkbox" {...register("isRequired")} />
<input {...register("firstName")} />
<input {...register("lastName")} />
<input {...register("contact")} />
</form>
</FormProvider>
);
}
https://codesandbox.io/s/infallible-wildflower-dsyon

You third solution of how to use when was correct. The reason why it wasn't working is that in your first two when statements for firstName and lastName you always return yups required method without checking if isRequired is true.
const schema = yup.object({
isRequired: yup.bool(),
firstName: yup.string().when("isRequired", {
is: true,
then: yup.string().required()
}),
lastName: yup.string().when("isRequired", {
is: true,
then: yup.string().required()
}),
contact: yup.string().when("isRequired", {
is: true,
then: yup.string().required("Required")
})
});
And here is how it would be done when passing a function to when.
const schema = yup.object({
isRequired: yup.bool(),
firstName: yup
.string()
.when("isRequired", (isRequired, schema) =>
isRequired ? yup.string().required() : schema
),
lastName: yup
.string()
.when("isRequired", (isRequired, schema) =>
isRequired ? yup.string().required() : schema
),
contact: yup
.string()
.when("isRequired", (isRequired, schema) =>
isRequired ? yup.string().required() : schema
)
});

Related

Unable to submit a multi-select checkbox input using formik in react

My form using formik does not currently return a value for the multi-select checkbox input component. I know I need to wrap the check box function component in the formik Field for formik to recognise and grab the component data. However, when ever I do so, It throws an error and the page goes blank.
How best can I integrate this component with formik so I can successfully submit the form.
Checkbox multi-select input compoenet
import React, { useState } from "react";
import { MultiSelect } from "react-multi-select-component";
const options = [
{ label: 'Sunday', value: 'sunday' },
{ label: 'Monday', value: 'monday'},
{ label: 'Tuesday', value: 'tuesday'},
{ label: 'Wednessday', value: 'wednessday'},
{ label: 'Thursday', value: 'thursday'},
{ label: 'Friday', value: 'friday'},
{ label: 'Saturday', value: 'saturday'},
{ label: "Week", value: "week", disabled: true },
];
const SelectFields = ({name}) => {
const [selected, setSelected] = useState([]);
return (
<div>
{/* <pre>{JSON.stringify(selected)}</pre> */}
<MultiSelect
options={options}
value={selected}
onChange={setSelected}
labelledBy="Select"
name={name}
/>
</div>
);
};
export default SelectFields;
Parent component where I'm using formik
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const NewRates = () => {
// code here were removed...
const initialValues = {
rateName: '',
price: '',
availableForPurchase: '',
availableType: '',
accessLevel: false,
validationType: '',
durationOfRate: '',
startTime: '',
endTime: '',
startTimeDate: '',
endTimeDate: '',
};
const validationSchema = Yup.object().shape({
});
const handleRates = (formValue) => {
console.log('formValue', formValue)
};
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleRates}
>
<Form>
{!successful && (
<FormWrapper>
// codes here were removed.
<>
<h6>Purchase Availability</h6>
<FormGroup>
<label htmlFor="AvailabilityForPurchase">
Select Days as required
<SelectFields name='availableForPurchase'/>
<ErrorMessage
name='availableForPurchase'
component='div'
className='alert alert-danger'
/>
</label>
</FormGroup>
....other codes
I have checked other similar solutions. But none come close to solving my issue.
You need to create a reusable version of multi select, which is bound to formik. Create a new component/file called MultiSelectField and useField to route the formik state into the MultiSelect component:
import { useField } from "formik"
import { MultiSelect } from "react-multi-select-component";
export const MultiSelectField = ({name, ...otherProps}) => {
const [field, meta, helpers] = useField({name})
return <MultiSelect value={field.value} onChange={(items) => helpers.setValue(items)} {...otherProps}/>
}
Now create your specific dropdown wrapper around this:
import React, { useState } from "react";
import { MultiSelectField } from "./MultiSelectField";
const options = [
{ label: 'Sunday', value: 'sunday' },
{ label: 'Monday', value: 'monday'},
{ label: 'Tuesday', value: 'tuesday'},
{ label: 'Wednessday', value: 'wednessday'},
{ label: 'Thursday', value: 'thursday'},
{ label: 'Friday', value: 'friday'},
{ label: 'Saturday', value: 'saturday'},
{ label: "Week", value: "week", disabled: true },
];
const SelectFields = ({name}) => {
return (
<MultiSelectField
options={options}
labelledBy="Select"
name={name}
/>
);
};
export default SelectFields;
And now use this as you already do <SelectFields name='availableForPurchase'/>

Typescript, React, strongly typing useHistory

I am sending my useHistory (react-router-dom) variable as a parameter to the employeee.service in which I use the "history.push" method with a state and a pathname. unfortunetaly I cannot seem to find out what the correct type would be. I used:
History<unknown>
History<Location>
but both do not seem to understand the state that I pass. Does anyone know how to strongly type this? any help much appreciated!
The create method in the service:
export const createEmployee = async (body: IEmployee, history: any) => {
try {
const employeesResponse = await fetch(`http://localhost:3000/employees`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (employeesResponse.status !== 201) {
const response: IHttpResponse = {
status: employeesResponse.status,
error: {message: employeesResponse.statusText},
data: {content: ''}
}
return response
}
const employeeResult: IEmployee[] = await employeesResponse.json();
const response: IHttpResponse = {
status: employeesResponse.status,
error: {message: ''},
data: {content: employeeResult}
}
history.push({
pathname: '/',
state: { detail: 'reload', response: response },
});
} catch (error) {
console.log('error while creating', error);
const response: IHttpResponse = {
status: error.status,
error: {message: error.statusText},
data: {content: ''}
}
return response;
}
}
the component using the service
import React, { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Button, Input, Label } from 'reactstrap';
import FormGroup from 'reactstrap/es/FormGroup';
import { yupResolver } from '#hookform/resolvers/yup';
import * as yup from 'yup';
import Select from 'react-select';
import { useHistory } from 'react-router-dom';
import { gendersList, statusList } from '../models/lists/formLists';
import { IOptionType } from '../models/IOptionType';
import { createEmployee } from '../services/employee.service';
import { IEmployee } from './../models/IEmployee';
export const AddEmployeeForm = () => {
const history = useHistory();
const [gender, setGender] = useState<IOptionType>({ label: gendersList[0].label, value: gendersList[0].value });
const [status, setStatus] = useState<IOptionType>({ label: statusList[0].label, value: statusList[0].value });
const validationSchema = yup.object().shape({
firstname: yup.string().required().min(2),
lastname: yup.string().required().min(2),
email: yup.string().email().required(),
status: yup.object().shape({
label: yup.string(),
value: yup.string(),
}),
gender: yup.object().shape({
label: yup.string(),
value: yup.string(),
}),
});
const {
handleSubmit,
control,
formState: { errors },
register,
} = useForm({
resolver: yupResolver(validationSchema),
});
const handleGenderChange = (option: IOptionType) => {
setGender({ label: option.label, value: option?.value });
};
const handleStatusChange = (option: IOptionType) => {
setStatus({ label: option.label, value: option?.value });
};
const onSubmit = async (data: any) => {
console.log('errors', errors);
const body: IEmployee = {
first_name: data.firstname,
last_name: data.lastname,
email: data.email,
gender: gender.value ? gender.value : '',
status: status.value ? status.value : '',
};
console.log('Data', body);
createEmployee(body, history);
};
return (
<div className="col-12">
<form onSubmit={handleSubmit(onSubmit)}>
<FormGroup>
<Label for="firstname">First name</Label>
{errors.firstname && <p className="text-danger error-message">{errors.firstname.message}</p>}
<Input {...register('firstname')} />
</FormGroup>
<FormGroup>
<Label for="lastname">Last name</Label>
{errors.lastname && <p className="text-danger error-message">{errors.lastname.message}</p>}
<Input {...register('lastname')} />
</FormGroup>
<FormGroup>
<Label for="email">Email</Label>
{errors.email && <p className="text-danger error-message">{errors.email.message}</p>}
<Input {...register('email')} />
</FormGroup>
<FormGroup>
<Label for="gender">Gender</Label>
<Controller
name="gender"
control={control}
render={({ field: { onChange, onBlur, value, ref } }) => (
<Select
options={gendersList}
onChange={(value) => handleGenderChange({ value: value?.value, label: value?.label })}
onBlur={onBlur}
defaultValue={gender}
selected={value}
/>
)}
/>
</FormGroup>
<FormGroup>
<Label for="status">Status</Label>
<Controller
name="status"
control={control}
render={({ field: { onChange, onBlur, value, ref } }) => (
<Select
options={statusList}
onChange={(value) => handleStatusChange({ value: value?.value, label: value?.label })}
onBlur={onBlur}
value={status}
selected={value}
/>
)}
/>
</FormGroup>
<Button type="submit">Submit</Button>
</form>
</div>
);
};
You may want to look at how RouterProps is defined. Maybe helpful.
import { useHistory, RouterProps } from 'react-router-dom';
You can achieve the this like so:
const history = useHistory<{ detail: string, response: IHttpResponse }>();
console.log(history.location.state.detail); // should log 'reload'
You can use the RouterChildContext to access the useHistory type.
const foo = (history: RouterChildContext['router']['history']) => {
history.push('/your-path');
}
This is from the react-router type declaration file:
// This is the type of the context object that will be passed down to all children of
// a `Router` component:
export interface RouterChildContext<Params extends { [K in keyof Params]?: string } = {}> {
router: {
history: H.History;
route: {
location: H.Location;
match: match<Params>;
};
};
}
My workaround is:
import { useHistory, useLocation } from "react-router-dom";
type THistory<T = unknown> = ReturnType<typeof useHistory<T>>
type TLocation<T = unknown> = ReturnType<typeof useLocation<T>>;
const goTo = (history: THistory<{payload: string}>) => {
history.push({
pathname: '/',
state: { payload: 'some data' },
});
}

TypeError: formik.getFieldProps is not a function

I've included a Field component to useFormik on my Nextjs app, and now this error message is showing up:
Server Error
TypeError: formik.getFieldProps is not a function
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
../../pages/_document.tsx (91:33) # Function.getInitialProps
89 | }
90 |
> 91 | const { html, head } = await ctx.renderPage({ enhanceApp })
| ^
92 | const styles = [...flush()]
93 | return { html, head, styles }
94 | }
Console shows the same error message:
TypeError: formik.getFieldProps is not a function
The form worked fine before, but I'm trying to add the radio buttons.
This is the new code that was added that introduced the problem:
const RadioButtons = (props) => {
const {
label, name, options, ...rest
} = props;
return (
<div>
<label>{label}</label>
<Field name={name} {...rest}>
{
({ field }) => options.map((options) => (
<React.Fragment key={option.key}>
<input
type="radio"
id={option.value}
{...field}
value={option.value}
checked={field.value === option.value}
/>
<label htmlFor={option.value}>{option.key}</label>
</React.Fragment>
))
}
</Field>
<ErrorMessage name={name} />
</div>
);
};
Used here:
const Footer = () => {
const radioOptions = [
{ key: 'Option 1', value: 'Investor Interest' },
{ key: 'Option 2', value: 'Leasing Interest' },
];
const formik = useFormik({
initialValues: {
fullname: '',
company: '',
email: '',
interest: '',
},
validationSchema: Yup.object({
fullname: Yup.string()
.max(50, 'Must be 50 characters or less')
.required('Required'),
company: Yup.string()
.max(50, 'Must be 50 characters or less')
.required('Required'),
email: Yup.string().email('Invalid email address')
.required('Required'),
interest: Yup.string().required('Required'),
}),
onSubmit: (values) => {
console.log(JSON.stringify(values, null, 2));
},
});
return (
etc...
<RadioButtons
label="Some label here"
name="interest"
options={radioOptions}
/>
)
Is it because I'm adding them in Nextjs, due to SSR? I feel like the code is correct, so I'm not sure what is 'cause the issue here.
I've also posted this question here, but I'm not sure that I'll get a response quickly there.

Cannot focus on first error input in Formik

I have the following implementation of a formik component that renders a form,
I am trying to access the first error field so I can focus on it but with no avail, i will show code
const CompanyProfile = () => {
const CompanySchema = Yup.object().shape({
name: Yup.string()
.min(2, 'too short')
.required(ERROR_REQUIRED),
industry: Yup.string().notRequired(),
address: Yup.string().notRequired(),
crn: Yup.number().required(ERROR_REQUIRED),
website: Yup.string()
.notRequired()
.min(2, 'too short'),
employeesNbr: Yup.string().required(ERROR_REQUIRED),
phoneNumber: Yup.string().required(ERROR_REQUIRED),
userRole: Yup.string().required(ERROR_REQUIRED),
personCheck: Yup.boolean().required(ERROR_REQUIRED),
});
const registerCompany = async values => {
try {
const data = values;
delete data.personCheck;
await addCompany(data);
} catch (error) {
console.log(error);
}
};
const successSubmit = values => {
registerCompany(values);
};
const forSubmit = formik => {
console.log('not valid');
const { errors } = formik;
const keys = Object.keys(errors);
console.log(formik);
if (keys.length > 0) {
const selector = `[id="${keys[0]}"]`;
const errorElement = document.getElementsByName(selector);
errorElement.focus();
}
};
const formik = useFormik({
initialTouched: false,
validateOnChange: true,
validateOnBlur: true,
initialValues: {
name: '',
industry: '',
address: '',
crn: '',
website: '',
employeesNbr: '',
phoneNumber: '',
userRole: '',
personCheck: false,
},
validationSchema: CompanySchema,
onSubmit: values => {
successSubmit(values);
},
handleSubmit: formik => {
forSubmit(formik);
},
});
return (
<Skeleton pageTitle={PAGE_TITLE_COMPANY_PROFILE}>
<CompanyProfileForm formik={formik} />
</Skeleton>
);
};
export default connect(CompanyProfile);
I dont know where I am going wrong, I attached the name,value, onchange correctly in the input fields because I am able to extract the values
Thank you

React Apollo returning Null when Graphiql and Altair succeed with exact same mutation?

I've tested this mutation on my django-graphene GraphiQL endpoint, and tested it through Altair (postman for graphql) at the exact same endpoint that my apollo client is pointed at. I run the same mutation with the same formatting, and it works with GraphiQL and Altair - new database entry.
Through react-apollo, it doesn't produce an error, and my django console prints: [29/Nov/2017 01:51:08] "POST /graphql HTTP/1.1" 200 75
Yet nothing actually hits the database. I tried console.log the query, and it prints the data structure, but the object it was supposed to be create just says "null".
I've rebuilt it twice to no avail. Here's the Altair mutation that works as expected:
mutation {
leadCreate(
newLead:{
firstName: "Bob",
lastName: "Dole",
email: "BobDole#graphql.com",
staff: "1"
}) {
lead {
id
}
}
}
Which returns the result in Altair:
STATUS: OK STATUS CODE: 200 TIME SPENT: 641ms
{
"data": {
"leadCreate": {
"lead": {
"id": "2773"
}
}
}
}
Same result in GraphiQL.
Here's my Apollo Link setup in my index.js:
const httpLink = createHttpLink({
uri: 'http://localhost:8000/graphql',
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root'),
);
registerServiceWorker();
I should note that all my queries work properly, so I'm fairly confident that the above is all correct.
Here's my LeadQuickCreate.js component:
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { Button, Input } from 'antd';
import { USER_ID } from '../../Utilities/constants';
class LeadQuickCreate extends Component {
state = {
firstName: '',
lastName: '',
phone: '',
email: '',
};
createLink = async () => {
const staff = localStorage.getItem(USER_ID);
const {
firstName, lastName, phone, email,
} = this.state;
const newLead = await this.props.createQuickLead({
variables: {
firstName,
lastName,
phone,
email,
staff,
},
});
console.log('NewLead = ', newLead);
};
render() {
const {
firstName, lastName, phone, email,
} = this.state;
return (
<div>
<div>
<Input
value={firstName}
onChange={e => this.setState({ firstName: e.target.value })}
type="text"
placeholder="Lead's First Name"
/>
<Input
value={lastName}
onChange={e => this.setState({ lastName: e.target.value })}
type="text"
placeholder="Lead's Last Name"
/>
<Input
value={phone}
onChange={e => this.setState({ phone: e.target.value })}
type="text"
placeholder="Lead's Phone Number"
/>
<Input
value={email}
onChange={e => this.setState({ email: e.target.value })}
type="text"
placeholder="Lead's email address"
/>
</div>
<Button type="primary" onClick={() => this.createLink()}>
Submit
</Button>
</div>
);
}
}
const CREATE_QUICK_LEAD = gql`
mutation CreateQuickLead(
$firstName: String!
$lastName: String
$phone: String
$email: String
$staff: ID!
) {
leadCreate(
newLead: {
firstName: $firstName
lastName: $lastName
phone: $phone
email: $email
staff: $staff
}
) {
lead {
id
}
}
}
`;
export default graphql(CREATE_QUICK_LEAD, { name: 'createQuickLead' })(LeadQuickCreate);
When I click the Submit button, the console log prints this:
{data: {…}}
data:
leadCreate:
lead: null
__typename: "LeadSerializerMutation"
etc.
So I'm stuck. Any ideas on where it's getting lost?
Thank you!
EDIT: Egads! When closely reviewing the response after sending a 'correct format' form as suggested, I realized that the "staff" const was submitting as a string. Not sure why my backend wasn't throwing a visible error, but a quick "parseInt(staff) before submitting and it works!
Finally noticed that the ID! that was expected was being sent as a string, and the graphene endpoint was looking for an integer. Simply changing my mutation call to this worked:
createLead = async values => {
const { firstName, lastName, phone, email, } = values;
let staff = localStorage.getItem(USER_ID);
staff = parseInt(staff);
const newLead = await this.props.createQuickLead({
variables: {
firstName,
lastName,
phone,
email,
staff,
},
});

Resources