How do I access a Formik context's validationSchema? - formik

tl;dr The result of useFormikContext() doesn't include its validationSchema. Based on its TypeScript types, it exists on the object as an optional property, but for some reason, I don't know how to make it appear.
I have a generic component that takes in a fieldName prop. I think use useField(fieldName) to retrieve the data. Now I want to check if there's a validationSchema, and if there is, do something with it (e.g. determine if the field has a max() test).
const MyComp = ({ fieldName, maxLength, ...props }) => {
// for some reason, validationSchema is always undefined
const { validationSchema } = useFormikContext();
if (maxLength == null && validationSchema) {
// never gets here because validationSchema is always undefined
maxLength = Yup.reach(validationSchema, fieldName)
?.tests.find(t => t.OPTIONS.name === 'max')
?.OPTIONS.params.max;
}
return (
<div>
The maxLength of <code>{fieldName}</code> length is{' '}
<code>{maxLength ?? 'undefined'}</code>.
{'\n'}
Its <code>validationSchema</code> via{' '}
<code>useFormikContext()</code> is{' '}
<b>{
validationSchema === null ? 'null' :
validationSchema ? 'definied' :
'not defined'
}</b>.
</div>
);
};
const schema = Yup.object({
withoutMaxLength: Yup.string(),
withMaxLength: Yup.string().max(10),
withMaxLengthProp: Yup.string()
});
const initialValues = {
withoutMaxLength: "",
withMaxLength: "",
withMaxLengthProp: ""
};
const App = () => (
<Formik
initialValues={initialValues}
onSumbit={() => {}}
validationSchema={schema}
>
<Form>
<MyComp fieldName="withoutMaxLength" />
<MyComp fieldName="withMaxLength" />
<MyComp fieldName="withMaxLengthProp" maxLength={10} />
</Form>
</Formik>
);
Code Sandbox
Result
As you can see above, useFormikContext().validationSchema is always undefined even though it is set as a property of <Formik />. Any idea why or what I can do about it?

It turns out this is a known bug in formik, and has been fixed in v3. My best option appears to be yarn patch, or it would be if formik weren't an external for my library.

Related

Can't get the first value by using useState in a functionn

I need to show the text according to the data value. By running the code, I want to see the 'Test1: 1' can be shown after I clicked the button, but I can't. Any method to make this happen?
Below is a sample sandbox link including the code.
https://codesandbox.io/s/restless-wildflower-9pl09k?file=/src/Parent.js
export default function Parent(props) {
const [data, setData] = useState(0);
const onClick = () => {
setData(1);
console.log(data);
setData(2);
};
return (
<>
<button onClick={onClick}> Click here </button>
{data === 1 ? <div>Test1: {data}</div> : <div>Test2: {data}</div>}
</>
);
}
The setState function returned by useState does not directly update the state. Instead it is used to send the value that React will use during the next asynchronous state update. console.log is an effect so if you want to see data logged every time it is changed, you can use React.useEffect. Run the code below and click the 0 button several times to see the state changes and effects in your browser.
function App() {
const [data, setData] = React.useState(0)
React.useEffect(_ => console.log("data", data), [data])
return <button
onClick={_ => setData(data + 1)}
children={data}
/>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Your comment talks about a network request example. Custom hooks can be designed to accommodate complex use cases and keep your components easy to write.
function App() {
const [{fetching, error, data}, retry] = useAsync(_ => {
return fetch("https://random-data-api.com/api/users/random_user")
.then(res => res.json())
}, [])
if (fetching) return <pre>Loading...</pre>
if (error) return <pre>Error: {error.message}</pre>
return <div>
<button onClick={retry} children="retry" />
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
}
function useAsync(f, deps) {
const [state, setState] = React.useState({fetching: true})
const [ts, setTs] = React.useState(Date.now())
React.useEffect(_ => {
f()
.then(data => setState({fetching: false, data}))
.catch(error => setState({fetching: false, error}))
}, [...deps, ts])
return [
state,
_ => {
setState({fetching: true, error: null, data: null})
setTs(Date.now())
}
]
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
The reason the console.log(data) did not reflect the latest data is because of the React manages state. Calls to setState() are asynchronous, and if you want to rely on the new value of the state, the correct way is to pass a function of old state, returning the current state. ref. documentation

formik initial checkbox value not disabling submit button

In my form, I have a checkbox for captcha that needs toggled for the form to be valid, however on load, the form still shows as valid. I can check the box to enable the form, and then uncheck it again, and THEN the form shows as properly disabled.
I am using the HOC withFormik implementation. I have tried using setTouched & setFieldTouched on the checkbox, and even setting this.props.touched.captcha = true. I have also tried passing isInitialValid: false to withFormik.
Another thing I tried is passing mapPropsToTouched, but that method doesn't get called.
I am using formik v 1.5.8
How can I make the form initially disabled until I engage the checkbox?
export class LoginForm extends React.Component {
render() {
const {
initialValues,
initialTouched,
validationSchema,
onSubmit,
children
} = this.props;
return (
<div>
<Formik
initialValues={initialValues}
initialTouched={initialTouched}
validationSchema={validationSchema}
onSubmit={onSubmit}
render={(formikProps) => {
return (
<Form>
{React.Children.map(children, (child, index) => {
return (
<div className='Form__row'>
{React.cloneElement(child, { index })}
</div>
);
})}
</Form>
);
}}
/>
</div>
);
}
}
const mapDispatchToProps = {};
export function mapStateToProps(state) {
return {};
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withFormik({
// isInitialValid: false,
// mapPropsToTouched: (props) => {
// console.log('🚀 ~ file: LoginForm.jsx ~ line 257 ~ props', props);
// },
mapPropsToValues: (props) => {
return {
username: props.previousUsername ? props.previousUsername : '',
password: '',
rememberMe: true,
captcha: false
};
}
})
)(LoginForm);

Conditional validation of multiple fields using Yup

I have 3 input fields (name, id, and postcode), and I use formik along with Yup for the validation.
What I want to achieve here is, I want to match each input field with a known combination of name, id and postcode (I have a predefined default value for id, name, postcode).
And if the values entered on all the 3 input fields exactly match with the default values of name, id, and postcode, then I have to show the formik error on each of the fields(*something like default not allowed). If one of these fields is different from the default values, do not show the error on any fields.
For eg, if my default values for each fields are name="testName", id="testID", postCode="testPostCode", show validation error on each field only if all 3 input values matches with the defaultValues.
This is what I've now:
const defaultValues = {
name: 'testName',
id: 'testID',
postCode: 'testPostCode'
}
const YUP_STRING = Yup.string().ensure().trim();
const validationSchema = yup.object().shape({
name: YUP_STRING.required('required'),
id: YUP_STRING.required('required'),
postcode: YUP_STRING.required('required'),
})
I've tried several variations, but nothing worked here. Can anyone help me find a solution for this?
You can do something like <Field validate={(value)=>validate(value,values)} name="name" type="text" />
In more detail..
<Formik
initialValues={defaultValues}
onSubmit={values => alert(JSON.stringify(values)}
>
{({ errors, touched, values }) => (
<Form>
<Field validate={(value) => validate(value, values)} name="name" type="text" />
{errors.name && touched.name ? <div>{errors.name}</div> : null}
<button type="submit">Submit</button>
</Form>
)}
</Formik>
And define validate function as
const validate = (value, values) => {
if(values === defaultValues){
return "Default Values not allowed"
} else return undefined
}
Or if you want to do it with validationSchema you can do something as by adding test function on each of the field, I have only done for name:
const validationSchema = yup.object().shape({
name: YUP_STRING.required('required')
.test('name', "No default please", function (item) {
const currentValues = {
name: this.parent.name,
id: this.parent.id,
postCode: this.parent.postCode
}
return !(currentValues === defaultValues)
}
),
id: YUP_STRING.required('required'),
postcode: YUP_STRING.required('required'),
})
No arrow function to be able to use this

How to access values inside validationSchema in formik

values are accessible only if I use validation but not inside validationschema
<Formik
initialValues={initialValues}
validationSchema={validationSchema(values)}
onSubmit={actions.handleSubmit}
>
<Form>
if I use useFormikContext(); values are not accessible inside validationSchema because initialization happens after Formik.
how to solve this problem.
Use validate instead of validationSchema
Pass your Form data as 4th argument in validateYupSchema which represents context and can be accessed later in schema.
Pass your schema as 2nd argument in validateYupSchema.
<Formik
validate={(values) => {
try {
validateYupSchema(values, validationSchema, true, values);
} catch (err) {
return yupToFormErrors(err); //for rendering validation errors
}
return {};
}}
onSubmit={} />
Now we can access any form value using this.options.context inside test function in schema
If you need to validate one formik value conditionally based on another formik values,
you can achieve using when() method chaining inside of validationSchema like so:
const validationSchema = Yup.object().shape({
is_female: Yup.boolean('Select The Pricing'),
female_price: Yup.string().when('is_female', {
is: isFemale => isFemale === true,
then: Yup.string().required('Female Price is required')
}),
male_price: Yup.string().when('is_female', {
is: isFemale => isFemale === false,
then: Yup.string().required('Male Price is required')
})
});
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={actions.handleSubmit}
/>
Hope this helps.

Form validation with react and material-ui

I am currently trying to add validation to a form that is built using material-ui components. I have it working but the problem is that the way I am currently doing it the validation function is currently being called on every state change in the input (i.e. every letter that is typed). However, I only want my validation to occur once the typing has stopped.
Given my current code:
class Form extends React.Component {
state = {open: false, email: '', password: '', email_error_text: null, password_error_text: null, disabled: true};
handleTouchTap() {
this.setState({
open: true,
});
}
isDisabled() {
let emailIsValid = false;
let passwordIsValid = false;
if (this.state.email === "") {
this.setState({
email_error_text: null
});
} else {
if (validateEmail(this.state.email)) {
emailIsValid = true
this.setState({
email_error_text: null
});
} else {
this.setState({
email_error_text: "Sorry, this is not a valid email"
});
}
}
if (this.state.password === "" || !this.state.password) {
this.setState({
password_error_text: null
});
} else {
if (this.state.password.length >= 6) {
passwordIsValid = true;
this.setState({
password_error_text: null
});
} else {
this.setState({
password_error_text: "Your password must be at least 6 characters"
});
}
}
if (emailIsValid && passwordIsValid) {
this.setState({
disabled: false
});
}
}
changeValue(e, type) {
const value = e.target.value;
const nextState = {};
nextState[type] = value;
this.setState(nextState, () => {
this.isDisabled()
});
}
login() {
createUser(this.state.email, this.state.password);
this.setState({
open: false
});
}
render() {
let {open, email, password, email_error_text, password_error_text, disabled} = this.state;
const standardActions = (
<FlatButton
containerElement={<Link to="/portal" />}
disabled={this.state.disabled}
label="Submit"
onClick={this.login.bind(this)}
/>
);
return (
<div style={styles.container}>
<Dialog
open={this.state.open}
title="Enter Your Details to Login"
actions={standardActions}
>
<span className="hint--right hint--bounce" data-hint="Enter in your email address">
<TextField
hintText="Email"
floatingLabelText="Email"
type="email"
errorText={this.state.email_error_text}
onChange={e => this.changeValue(e, 'email')}
/>
</span>
<br />
<span className="hint--right hint--bounce" data-hint="Enter your password">
<TextField
hintText="Password"
floatingLabelText="Password"
type="password"
errorText={this.state.password_error_text}
onChange={e => this.changeValue(e, 'password')}
/>
</span>
</Dialog>
<h1>VPT</h1>
<h2>Project DALI</h2>
<RaisedButton
label="Login"
primary={true}
onTouchTap={this.handleTouchTap.bind(this)}
/>
</div>
);
}
}
export default Form;
Is there a way that I can achieve my desired functionality, without making a major change to the code, or does it need to be completely refactored?
Does the check have to happen after a certain delay? A solution that I think would suffice in most situations would be to split your code up a bit. Don't trigger your isDisabled() function in changedValue(). Instead have it run on the onBlur event instead.
Try this:
<TextField
hintText="Password"
floatingLabelText="Password"
type="password"
errorText={this.state.password_error_text}
onChange={e => this.changeValue(e, 'password')}
onBlur={this.isDisabled}
/>
and then your function becomes:
changeValue(e, type) {
const value = e.target.value;
const nextState = {};
nextState[type] = value;
this.setState(nextState);
}
Current Material-UI version doesn't use the errorText prop but there is still a way that you can use to display error and apply validation to the TextField in Material-UI.
We use the error(Boolean) property to denote if there is an error or not. Further to display the error text use helperText property of the TextField in the Material-UI, just provide it the error text you want to display.
Do it like:
<TextField
value={this.state.text}
onChange={event => this.setState({ text: event.target.value })}
error={text === ""}
helperText={text === "" ? 'Empty!' : ' '}
/>
Simplest is to call form.reportValidity(). form can be obtained by calling event.currentTarget.form.
This library that I had created, takes care of everything related to validating fields and it supports material-ui components as well...
To validate your fields, you just need to wrap you field component and you are done... saving a lot of effort in managing state yourself manually.
<Validation group="myGroup1"
validators={[
{
validator: (val) => !validator.isEmpty(val),
errorMessage: "Cannot be left empty"
}, ...
}]}>
<TextField value={this.state.value}
className={styles.inputStyles}
style={{width: "100%"}}
onChange={
(evt)=>{
console.log("you have typed: ", evt.target.value);
}
}/>
You can use onblur text field event. It's triggered when input looses focus.

Resources