2
votes

I have a JSON object which contains an array of JSON objects. My need is out of 2 array elements (vehicles), I just need to ensure that at least one is filled with data i.e. both can't be empty.. see my declaration below and my Yum schema

 const initialValues = {
  applicantID: "",
  applicantTypeID: "1",
  firstName: "",
  lastName: "",
  email: "[email protected]",
  address1: "",
  address2: "",
  suburb: "",
  state: "AZ",
  postcode: "",
  phone: "",
  mobile: "",
  business_unit_school: "",
  building: "",
  level: "",
  room: "",
  applicationStatusID: "1",
  **vehicles: [
    { registrationNumber: "", make: "", model: "" },
    { registrationNumber: "", make: "", model: "" },
  ],**
  
};

const validationSchema = Yup.object({
  applicantID: Yup.string().required("Employee Number required."),
  firstName: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("Firstname required."),
  lastName: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("Lastname required."),
  email: Yup.string().email("Invalid email format").required("Email required."),
  address1: Yup.string()
    .min(2, "Too short.")
    .max(255, "Too Long!")
    .required("Address required."),
  address2: Yup.string().max(255, "Max 255 characters allowed."),
  suburb: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("Suburb required."),
  state: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("State required."),
  business_unit_school: Yup.string()
    .min(2, "Too Short!")
    .max(100, "Max 100 characters allowed.")
    .required("Business unit required."),
  **vehicles: Yup.array().of(
    Yup.object().shape({
      registrationNumber: Yup.string().required("Required"),
    })
  ),**
postcode: Yup.string().required("Postcode required."),
  phone: Yup.number()
    .required("Phone number required")
    .typeError("You must specify a number"),
  mobile: Yup.number().required("").typeError("You must specify a number"),
});

My above vehicles validation works though it forces user to fill in registrationNumber element of both array items under vehicle which is not I want. Any help would be much appreciated. I also tried below and it doesn't work ...

let vehicleschema = Yup.object({
  vehicles: Yup.array().of(
    Yup.object({
      registrationNumber: Yup.string().required("Required"),
      make: Yup.string().required("Required"),
    })
  ),
});

const validationSchema = Yup.object({
vehicles: vehicleschema.validateAt("vehicles[0].registrationNumber", initialValues)
}),

I get below error on validation ...

TypeError: field.resolve is not a function
(anonymous function)
node_modules/yup/es/object.js:146
  143 | 
  144 | innerOptions.path = makePath(_templateObject(), options.path, prop);
  145 | innerOptions.value = value[prop];
> 146 | field = field.resolve(innerOptions);
      | ^  147 | 
  148 | if (field._strip === true) {
  149 |   isChanged = isChanged || prop in value;
View compiled
ObjectSchema._cast
node_modules/yup/es/object.js:136
  133 | });
  134 | 
  135 | var isChanged = false;
> 136 | props.forEach(function (prop) {
      | ^  137 |   var field = fields[prop];
  138 |   var exists = has(value, prop);
  139 | 

ok, after using Luis's solution below, it seems to validate registration numbers though I am now ended up with multiple error text. I am using Formik with Yup.. the Formik code is as per below ...

<Grid
              container
              item
              lg={12}
              md={12}
              xs={12}
              spacing={15}
              name="vehicles"
              style={{ marginBottom: "-4em" }}
            >
              <Box mx={3} my={2} textAlign="center">
                <h2>Vehicle Details</h2>
              </Box>
            </Grid>
            <Grid
              container
              item
              lg={12}
              md={12}
              xs={12}
              spacing={15}
              name="vehicles"
              style={{ marginBottom: "-4em" }}
            ></Grid>
            {initialValues.vehicles.map((vehicle, index) => (
              <Grid
                container
                item
                lg={10}
                md={12}
                xs={12}
                spacing={5}
                justify="space-between"
                className={classes.rowSpacing}
                key={index}
              >
                <Grid item lg={3} md={5} xs={12}>
                  <Field
                    component={TextField}
                    fullWidth={true}
                    label="Registration Number"
                    name={`vehicles[${index}].registrationNumber`}
                  />
                  <FormHelperText error>
                    <ErrorMessage name="vehicles" />
                  </FormHelperText>
                </Grid>

                <Grid item lg={2} md={5} xs={12}>
                  <Field
                    component={TextField}
                    fullWidth={true}
                    label="Make"
                    name={`vehicles[${index}].make`}
                  />
                </Grid>
                <Grid item lg={2} md={5} xs={12}>
                  <Field
                    component={TextField}
                    fullWidth={true}
                    label="Model"
                    name={`vehicles[${index}].model`}
                  />
                </Grid>
                <br />
              </Grid>
            ))}
          </Grid>

see multiple error below ..error text "at least one registration number is required" getting repeated twice

enter image description here

Hi Luis that updated solution didn't work and it could be due to the fact that I use both Formik-material-ui and material-ui in my forms..see below.. to distinguish between two TextField elements, I am using alias of muiTextField here

import {TextField as muiTextField} from "@material-ui/core";

import { TextField, Select } from "formik-material-ui";

below is my updated code ...

<Grid item lg={3} md={5} xs={12}>
                  <Field
                    //component={TextField}
                    fullWidth={true}
                    label="Registration Number"
                    name={`vehicles[${index}].registrationNumber`}
                    render={() => (
                      <muiTextField
                         error={Boolean(errors.vehicles)}
                         helperText= {
                            errors.vehicles && getVehiclesErrors(errors.vehicles)
                         }
                      />
                   )}
                  />
                  
                </Grid>

The formik-material-ui is just a wrapper around common material-ui elements. I am curious as to why you have 'email' reference in the getVehicleErrors() function. is that a typo? After I updated my code (as per above) here muiTextField refers to the TextField of material-ui, now I see no vehicle registration fields on my form..see below

enter image description here

Managed to fix the issue with below changes....

    const getVehiclesErrors = (errors) => {
    return Array.isArray(errors)
      ? errors.filter((registrationNumber, i, arr) => arr.indexOf(registrationNumber) === i)
      : errors;
  }; 



<Grid item lg={3} md={5} xs={12}>
                      <Field
                        component={TextField}
                        fullWidth={true}
                        label="Registration Number"
                        name={`vehicles[${index}].registrationNumber`}
                      />
                      {errors.vehicles && touched.vehicles ? (
                        <div style={{ color: "red" }}>
                          {getVehiclesErrors(errors.vehicles)}
                        </div>
                      ) : null}
                    </Grid>

enter image description here

2

2 Answers

1
votes

you can do like this way:

*I've done an example where at least one registrationNumber in array should be filled for your vehicles schema:

vehicles: Yup.array(
    Yup.object({
      registrationNumber: Yup.string(),
      make: Yup.string().required("make Required"),
    }).test(
      "registrationNumber test",
      // The error message that should appears if test failed
      "at least one registrationNumber should be filled",
      // Function that does the custom validation to this array
      validateAgainstPrevious
    )
  )

The function below its just an example. You can do your own logic here.

function validateAgainstPrevious() {
  // In this case, parent is the entire array
  const { parent } = this;
  
  // filtered array vechicles that doens't have registrationNumber
  const filteredArray = parent.filter((e) => !e.registrationNumber);
  
  // If length of vehicles that doesn't have registrationNumber is equals to vehicles  array length then return false;
  if (filteredArray.length === parent.length) return false;

  return true;
}

UPDATED

For the multiple error text issue, you can do a workaround:

Instead you pass TextField component to Field, like this:

<Field
   component={TextField}
   fullWidth={true}
   label="Registration Number"
   name={`vehicles[${index}].registrationNumber`}
/>

you can do this:

<Field
   // component={TextField}
   fullWidth={true}
   label="Registration Number"
   name={`vehicles[${index}].registrationNumber`}
   render={() => (
      <TextField
         error={Boolean(errors.vehicles)}
         helperText= {
            errors.vehicles && getVehiclesErrors(errors.vehicles)
         }
      />
   )}
/>

And created this function:

const getVehiclesErrors = (errors) => {
  return Array.isArray(errors)
    ? errors.filter((email, i, arr) => arr.indexOf(email) === i)
    : errors;
};
1
votes

Instead Yup.object({}) use Yup.object().shape({ ... all the properties comes here})