Yup use different schema based on sibling of a parent (parent.parent)

Viewed 2624

In the schema I would like to adjust the schema based on the sibling of the parent.

If for example: if toggleMonday is true, then weekdays -> monday should have a specific validation schema.

Now the following example works. However, it is quite verbose.

const schema = yup.object().shape({
  toggleMonday: yup.bool().required(),
  toggleTuesday: yup.bool().required(),
  toggleWednesday: yup.bool().required(),
  toggleThursday: yup.bool().required(),
  toggleFriday: yup.bool().required(),
  toggleSaturday: yup.bool().required(),
  toggleSunday: yup.bool().required(),
  weekdays: yup.object()
   // works, toggleMonday is a sibling of weekdays
   .when('toggleMonday', {
        is: true,
        then: yup.object().shape({
          monday: yup.array().of(yup.object().shape(daySchema)).daySchemaFirstTimeslotRequired(),
          tuesday: yup.array().of(yup.object().shape(daySchema)),
          wednesday: yup.array().of(yup.object().shape(daySchema)),
          thursday: yup.array().of(yup.object().shape(daySchema)),
          friday: yup.array().of(yup.object().shape(daySchema)),
          saturday: yup.array().of(yup.object().shape(daySchema)),
          sunday: yup.array().of(yup.object().shape(daySchema)),
        }),
      })
      .when('toggleTuesday', {
        is: true,
        then: yup.object().shape({
          monday: yup.array().of(yup.object().shape(daySchema)),
          tuesday: yup.array().of(yup.object().shape(daySchema)).daySchemaFirstTimeslotRequired(),
          wednesday: yup.array().of(yup.object().shape(daySchema)),
          thursday: yup.array().of(yup.object().shape(daySchema)),
          friday: yup.array().of(yup.object().shape(daySchema)),
          saturday: yup.array().of(yup.object().shape(daySchema)),
          sunday: yup.array().of(yup.object().shape(daySchema)),
        }),
      })
      // etc.
});

As you can see it is pretty repetitive.

The the mixed.when() method is used in this example. However it seems that you can only target sibling or sibling children fields.

Nesting it under each day respectively does not work since the days are nested in 'weekdays'.

Something in the lines of:

const schema = yup.object().shape({
  toggleMonday: yup.bool().required(),
  toggleTuesday: yup.bool().required(),
  toggleWednesday: yup.bool().required(),
  toggleThursday: yup.bool().required(),
  toggleFriday: yup.bool().required(),
  toggleSaturday: yup.bool().required(),
  toggleSunday: yup.bool().required(),
  weekdays: yup.object()
    // does not work, toggleMonday is not a sibling of 'monday'
    monday: yup.array().when('toggleMonday', {
      is: true,
      then: yup.array().of(yup.object().shape(daySchema)).daySchemaFirstTimeslotRequired(),
      otherwise: yup.array().of(yup.object().shape(daySchema)),
    }),
    tuesday: yup.array().when('toggleMonday', {
      is: true,
      then: yup.array().of(yup.object().shape(daySchema)).daySchemaFirstTimeslotRequired(),
      otherwise: yup.array().of(yup.object().shape(daySchema)),
    }),
    // etc.
});

Any advice?

2 Answers

Yup gives you a declarative api, but you're not limited to writing huge object literals like that. Your schema is rather complex, but if there's no other way to change it, consider writing it programmatically instead -

const weekdays =
  [ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ]

const schema = yup.object().shape({
  toggleMonday: yup.bool().required(),
  toggleTuesday: yup.bool().required(),
  toggleWednesday: yup.bool().required(),
  toggleThursday: yup.bool().required(),
  toggleFriday: yup.bool().required(),
  toggleSaturday: yup.bool().required(),
  toggleSunday: yup.bool().required(),
  weekdays: weekdays.reduce(whenToggle, yup.object())
})

Now we just have to implement whenToggle -

const day =
  yup.object().shape(daySchema)

const title = (str = "") =>
  str.substr(0,1).toUpperCase() + str.substr(1)

const whenToggle = (y = {}, day = "") =>
  y.when
    ( `toggle${title(day)}`
    , { is: true, then: yup.object().shape(firstTimeslot(day)) }
    )

And finally implement firstTimeslot -

const dayList =
  yup.array().of(day)

const firstTimeslot = (day = "") =>
  weekdays.reduce
    ( (r, d) =>
        d === day
          ? { ...r, [d]: dayList.daySchemaFirstTimeslotRequired() }
          : { ...r, [d]: dayList }
    , {}
    )

After some digging I found this PR: https://github.com/jquense/yup/pull/201

Seems that a workaround (source) is to use context:

// Pass the root data to context
schema.validateSync(data, {context: {data: data}})

// In your test, you can just access the root data
.test('myValidator', 'my validation message', function (value) {
  const data = this.options.context.data;
  // Do what you need to do with the root data
})

So that would mean I should use context to get the toggle items via props, and use them in the schema.

A bit unrelated to this Question: I am also using formik for my input values. That does only support context via the HOC withFormik (source)

I'll stick to the verbose method since the implementation will other wise be so specific that it's not easily re-usable throughout my app because of the implementation details.

Related