everyday_potato
everyday_potato

Reputation: 113

Joi: Cannot change error message for an optional array where if there are items, they cannot be empty strings

I have a form for a project where the description is required, and if they want to add details, those textareas need to be filled out. But it's okay if there are no details added. So if they want to add a detail, that text area has to be filled. But an empty array is allowed.

I am having trouble overwriting the default error for the missing details, the default being "details[0]" must not be a sparse array.

A form that shows a blank textarea underneath the text 'Write a description for your project. (Required)' Beneath the textarea is a button to add a detail. The Save button is disabled.

A form with two textareas, one for the description and a new one asking for a detail that is also blank. The Save button is disabled.

The textareas are now outlined in read. The description textarea has the text 'Description required' underneath. The detail textarea has the text 'details[0] must not be a sparse array item'.

Schema:

const descriptionDetailSchema = Joi.object({ 
  description: Joi.string().required().messages({
    'string.base': 'Description is required',
    'string.empty': 'Description is required'
  }),
  details: Joi.array().items(
    Joi.string().messages({
      'string.empty': 'Detail is required'
    })
  )
});
const DescriptionAndDetailForm = forwardRef(
  ({ activityIndex, item, index, saveDescription, setFormValid }, ref) => {
    DescriptionAndDetailForm.displayName = 'DescriptionAndDetailForm';
    const {
      handleSubmit,
      control,
      formState: { error, errors, isValid, isValidating },
      getValues
    } = useForm({
      defaultValues: {
        description: item.description,
        details: item.details
      },
      mode: 'onBlur',
      reValidateMode: 'onBlur',
      resolver: joiResolver(descriptionDetailSchema)
    });

    useEffect(() => {
      console.log('isValid changed');
      console.log({ errors, isValid, isValidating });
      setFormValid(isValid);
    }, [isValid]);

    useEffect(() => {
      console.log('errors changed');
      console.log({ errors, isValid, isValidating });
    }, [errors]);

    useEffect(() => {
      console.log('isValidating changed');
      const { error, value } = descriptionDetailSchema.validate(getValues());
      console.log({ error, value, errors, isValid, isValidating });
    }, [isValidating, errors]);

    const initialState = item;

    function reducer(state, action) {
      switch (action.type) {
        case 'updateField':
          return {
            ...state,
            [action.field]: action.value
          };
        case 'addDetail': {
          const newDetail = newDescriptionDetail();
          return {
            ...state,
            details: [...state.details, newDetail]
          };
        }
        case 'removeDetail': {
          const detailsCopy = [...state.details];
          detailsCopy.splice(action.index, 1);
          return {
            ...state,
            details: detailsCopy
          };
        }
        case 'updateDetails': {
          const detailsCopy = [...state.details];
          detailsCopy[action.detailIndex].detail = action.value;

          return {
            ...state,
            details: detailsCopy
          };
        }
        default:
          throw new Error(
            'Unrecognized action type provided to DescriptionAndDetailForm reducer'
          );
      }
    }

    const [state, dispatch] = useReducer(reducer, initialState);

    const handleDescriptionChange = e => {
      dispatch({
        type: 'updateField',
        field: 'description',
        value: e.target.value
      });
    };

    const onSubmit = e => {
      e.preventDefault();
      saveDescription(activityIndex, index, state);
      handleSubmit(e);
    };

    const handleAddDetail = () => {
      dispatch({ type: 'addDetail' });
    };

    const handleDeleteDetail = (descriptionIndex, detailIndex) => {
      dispatch({ type: 'removeDetail', index: detailIndex });
    };

    const handleDetailChange = (e, i) => {
      dispatch({
        type: 'updateDetails',
        detailIndex: i,
        value: e.target.value
      });
    };

    return (
      <form
        index={index}
        key={`activity${activityIndex}-index${index}-form`}
        onSubmit={onSubmit}
      >
        <Controller
          key={`activity${activityIndex}-index${index}`}
          name="description"
          control={control}
          render={({ field: { onChange, ...props } }) => (
            <TextField
              {...props}
              label="Description"
              multiline
              rows="4"
              onChange={e => {
                handleDescriptionChange(e);
                onChange(e);
              }}
              errorMessage={errors?.description?.message}
              errorPlacement="bottom"
            />
          )}
        />
        {state.details.map(({ key, detail }, i) => (
          <Review
            key={key}
            onDeleteClick={ () => handleDeleteDetail(index, i) }
            onDeleteLabel="Remove"
            skipConfirmation
            ariaLabel={`${i + 1}. ${detail}`}
            objType="Detail"
          >
            <div>
              <Controller
                name={`details.${i}`}
                control={control}
                render={({ field: { onChange, ...props } }) => (
                  <TextField
                    {...props}
                    id={`${activityIndex}-detail${i}`}
                    name={`details.${i}`}
                    label="Detail"
                    value={detail}
                    multiline
                    rows="4"
                    onChange={e => {
                      handleDetailChange(e, i);
                      onChange(e);
                    }}
                    errorMessage={errors?.details && errors?.details[i]?.message}
                    errorPlacement="bottom"
                  />
                )}
              />
            </div>
          </Review>
        ))}
        <div>
          <Button
            key={`activity${activityIndex}-index${index}-add-metric`}
            onClick={handleAddDetail}
          >
            <Icon icon={faPlusCircle} />
            Add Detail
          </Button>
        </div>
        <input
          type="submit"
          ref={ref}
          hidden
        />
      </form>
    );
  }
);

Upvotes: 1

Views: 689

Answers (1)

Milosz
Milosz

Reputation: 56

You need to make sure your schema is returning a value if you're using .custom() for validation

Upvotes: 0

Related Questions