movpushmov / effector-reform

MIT License
18 stars 2 forks source link

validate operator [валидация отдельных полей] #5

Open xaota opened 5 months ago

xaota commented 5 months ago

Хочется декларативно описывать валидации полей, но не в схеме (очень плохо смотрится для больших форм)

пример

/** @section описание формы */
type FormRegistration = {
  name: string;
  email: string;
  password: string;
  passwordConfirm: string;
  addresses: Array<string>
}

const form = createForm<FormRegistration>({
  schema: {
    name: "",
    email: "",
    password: "",
    passwordConfirm: "",
    addresses: []
  }
});

/** @section описание валидации полей */
/** @subsection поле ввода пароля */
/** проверка сложности пароля */
const validatePassword = message => ({ value }) => {
  const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return regex.test(value) ? undefined : message;
}

validate({                     // новый оператор
  field: form.fields.password, // какое поле валидируем
  trigger: ["change", "blur"], // когда запускать валидацию
  rules: [                     // правила валидации
    required("password is required"),
    validatePassword("password is not strong")
  ]
});

плюшки

значения других сторов для валидации, и запуск валидации только по условию

пример - валидация поля passwordConfirm: работает только если введён пароль и проверяет совпадение с ним

validate({
  field: form.fields.passwordConfirm,
  trigger: ["blur"],    // проверяем только когда закончили ввод

  source: form.$values, // type Shape из effector
  filter: (_, source) => source.values.password.length > 0,

  rules: [
    (field, source) => {
      const { value: passwordConfirm } = field;
      const { values } = source;
      if (values.password !== passwordConfirm) return "passwords mismatch"
    }
  ]
})

асинхронная валидация

пример - проверяем что уже зарегистрирован пользователь с таким email

validate({
  field: form.fields.email,
  trigger: ["blur"], // эта валидация нам тоже нужна не часто

  // rules для запуска всяких синхронных штук, а target - для запуска эвентов и эффектов для валидации
  // по идее такой же target как в операторе sample
  // (или тоже включить в rules, если получится прокинуть такое)
  target: checkEmailFx
})

пояснение

В этом кейсе можно подумать над тем как использовать что-то из экосистемы, например, farfetched - вызывать не эффект а событие query.start, и добавить оператор для этого, чтобы ручками не выставлять для поля всякие стейты $validated и не ловить и не прокидывать потом результат валидации в него

пример - тоже самое, "но ручками" через события, а не эффект

import { createQuery } from "@farfetched/farfetched-core";
const checkEmailQuery = createQuery(...);

const checkEmailQueryEvent = createEvent<{ value: string }>();
sample({
  clock: checkEmailQueryEvent,
  fn: ({ value }) => ..., // по-сути это mapParams
  target: checkEmailQuery.start
});

// запускаем валидацию - проверку что такой email уже существует
validate({
  field: form.fields.email,
  trigger: ["blur"],
  target: checkEmailQueryEvent
});

sample({
  // пусть для примера тут ответ 200 OK, но в теле есть инфо о том что пользователь с таким емейлом уже зареган
  clock: checkEmailQuery.finished.success,
  filter: ({ result }) => Boolean(result.error),
  fn: ({ result }): string => result.error?.message,
  target: form.fields.email.invalidate // вообще changeError, но это название имхо не очень
});

по-идее в перспективе в farfetched или других либах можно будет делать фабрики на что-то подобное

запуск сторонней логики

тут оператор validate не при чем, можно обойтись sample

пример если начали вводить passwordConfirm, а поле password ещё не заполнено - подсветим поле password

sample({
  clock: form.fields.passwordConfirm.changed,
  source: form.$values,
  filter: values => values.password.length === 0 && values.passwordConfirm.length > 0,
  // запуск правил валидации для поля password, которые вернут ошибку "password is required"
  target: form.fields.password.validate
})

Валидация для ArrayField

тут есть два случая

1) поля типа ArrayField в value при запуске проверки правила валидации передают номер элемента для которого происходит вызов валидатора

validate({
  field: fields.addresses,
  rules: [
     ({ value, index }) => index > 5 ? "у вас слишком много адресов" : undefined
  ]
})

2) валидация прям всего массива (специальный оператор который смотрит на массив как на массив) или может быть аргумент array: true в операторе validate

// или validateArray или validate({ field: ArrayField, array?: true }) чтобы отличать валидацию всего списка от валидации поля в списке
validateArray({
  field: fields.addresses,
  rules: [
     //  requiredArray
     ({ value, length }) => length === 0 ? "должен быть хотя бы один адрес" : undefined
  ]
});

тут придется ещё сделать доработки в useArrayField - чтобы этот хук также мог возвращать ошибки и статус валидации для всего списка

const addresses = useArrayField(form.fields.addresses);
...
return (
  <>
    {addresses.values.map(...)}
    {!addresses.isValid && <p className="error">{addresses.error}</p>}
  </>
);