Yup validation: Require all or none fields of a language

Viewed 622

We have quite a large form with multilang inputs - a few fields:

  • name_german
  • name_french
  • name_italian
  • description_german
  • description_french
  • description_italian
  • ... many more

In case that one german field is filled, all other german fields should be required. Same goes with the other languages. It should be possible to have both german and french fields filled, but the italian ones can be empty.

Somehow I can't get it working. This is what I tried:

Yup.object().shape({
     name_german: Yup.string().test(
         'requireAllIfOneIsFilled',
         'formvalidation.required.message',
         function () {
            return multiLanguageTest(this.parent, 'german');
         },
     ),
    ... // same for other fields
});

Then do the test like this:

const multiLanguageTest = (formValues, language: 'german' | 'french' | 'italian'): boolean => {
    const errorCount = dependentFields.reduce((errorCount, rawFieldName) => {
        const postFixedField = `${rawFieldName}_${language}`;

        if (!formValues[postFixedField]) {
            return errorCount + 1;
        }
        return errorCount;
    }, 0);

    return errorCount === 0;
};

This gives me quite an undebuggable behavior. Am I using the concept of .test wrong?

1 Answers

It is possible to validate in this manner using the context option passed in when calling .validate.

A simple but illustrative example of the context option - passing a require_name key (with truthy value) to the .validate call will change the schema to be .required():

const NameSchema = yup.string().when(
  '$require_name',
  (value, schema) => value ? schema.required() : schema
);

const name = await NameSchema.validate('joe', {
  context: {
    require_name: true,
  }
});

If we can pass in the required fields to the schema during validation, we can use the following schema for your validation case:

const AllOrNoneSchema = yup.object({
  name_german: yup.string()
    .when('$name_german', (value, schema) => value ? schema.required() : schema),
  description_german: yup.string()
    .when('$description_german', (value, schema) => value ? schema.required() : schema),
  date_german: yup.string()
    .when('$date_german', (value, schema) => value ? schema.required() : schema),
  // ...other_languages
});

In your case, we must pass in a required context key for each language if one or more of the fields is filled out. i.e. If one German field has been filled out, we want to pass an object with all ${something}_german keys whose values are true:

{
  name_german: true,
  description_german: true,
  ...
}

To do this, we can create a helper function that takes your form object and returns a record with boolean values as in the example above:

const createValidationContext = (form) => {
  const entries = Object.entries(form);
  
  const requiredLanguages = entries.reduce((acc, [key, value]) => {
    return !value.length ? acc : [...acc, key.split('_')[1]];
  }, []);
  
  return Object.fromEntries(
    entries.map(([key, value]) => [key, requiredLanguages.includes(key.split('_')[1])])
  )
};

Putting it all together, we have the following working solution:

const yup = require("yup");

const AllOrNoneSchema = yup.object({
  name_german: yup.string()
    .when('$name_german', (value, schema) => value ? schema.required() : schema),
  description_german: yup.string()
    .when('$description_german', (value, schema) => value ? schema.required() : schema),
  date_german: yup.string()
    .when('$date_german', (value, schema) => value ? schema.required() : schema),
});

const createValidationContext = (form) => {
  const entries = Object.entries(form);
  
  const requiredLanguages = entries.reduce((acc, [key, value]) => {
    return !value.length ? acc : [...acc, key.split('_')[1]];
  }, []);
  
  return Object.fromEntries(
    entries.map(([key, value]) => [key, requiredLanguages.includes(key.split('_')[1])])
  )
};

const form = {
  description_german: "nein",
  date_german: "juli",
  name_german: "nena",
}

const formContext = createValidationContext(form);

console.log(formContext); // { description_german: true, date_german: true, name_german: true }

const validatedForm = await AllOrNoneSchema.validate(form, { context: formContext });

console.log(validatedForm); // { description_german: "nein", date_german: "juli", name_german: "nena" }

A live example of this solution, with multiple languages, can be seen an tested on RunKit, here: https://runkit.com/joematune/6138ca762dc6340009691d8a

For more information on Yup's context api, check out their docs here: https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema

Related