6
votes

I'm using Formik's FieldArray to add objects dynamically to an array, rendering additional form elements as objects are push()ed to the array.

My schema looks like this:

const EMAIL_SCHEMA = Yup.object().shape({
  address: Yup.string().email().required( 'E-mail address is required.' ),
  isPreferredContact: Yup.boolean()
})

const SCHEMA = Yup.object().shape({
  emails: Yup.array()
             .of( EMAIL_SCHEMA )
             .ensure()
             .compact( v => !v.address )
             .required( 'At least one e-mail address is required.' )
})

For each e-mail input, there is a corresponding checkbox to indicate if it is the preferred contact e-mail address. No e-mail addresses are required to be marked as preferred.

What I would like to do is validate that the array contains at most one object where isPreferredContact is true. If there are 3 e-mail objects in the array and isPreferredContact is false for all of them, that's a valid state. That is to say:

let values = [
  {address: '[email protected]', isPreferredContact: false},
  {address: '[email protected]', isPreferredContact: false},
  {address: '[email protected]', isPreferredContact: false}
] // OK

let values = [
  {address: '[email protected]', isPreferredContact: true},
  {address: '[email protected]', isPreferredContact: false},
  {address: '[email protected]', isPreferredContact: false}
] // OK

let values = [
  {address: '[email protected]', isPreferredContact: true},
  {address: '[email protected]', isPreferredContact: true},
  {address: '[email protected]', isPreferredContact: false}
] // Invalid

I see this answer

Yup: deep validation in array of objects

shows that the compact() method can be used to validate for at least one, because if after removing "falsy" values from the array, the array is empty, then it's easy to treat the schema key as invalid.

There's nothing I can see, though, for validating that the array contains at most one object with a property = value predicate.

Is there a way to do this?

1

1 Answers

8
votes

After poking through Yup's issues in GitHub, staring at the API docs (the doc for addMethod is really terrible), and testing in Code Sandbox, I found that this works:

Yup.addMethod(Yup.array, 'atMostOne', function(args) {
  const { message, predicate } = args
  return this.test('atMostOne', message, function(list) {
    // If there are 2+ elements after filtering, we know atMostOne must be false.
    return list.filter(predicate).length < 2
  })
})

The predicate, obviously, is a function that takes an element of the array and performs a test on it that returns a boolean.

For an array of scalar values, this is as simple as el => el === value. For an array of objects, it would be el => el.property === value or el[property] === value.

Hope this helps anybody else curious about this.