Если вы когда-нибудь пользовались фреймворком AngularJS, то вы знаете, как легко в нем валидируются формы. Однако в React ситуация несколько хуже. Оно и понятно, ведь это всего лишь библиотека. Но благодаря сообществу из этой библиотеки можно сделать мощное средство для создания полноценных SPA-приложений. На данный момент создано огромное множество компонентов, которые способны удовлетворить большинство нужд разработчиков на React. В данной статье я бы хотел описать подход, который использовал для валидации форм с помощью компонента Formsy-react.

Начну с того, что компонентов для валидации форм существует достаточно много (здесь представлено 32). Я попробовал некоторые из них и решил остановиться именно на Formsy, так как он выглядел не слишком замудреным и при этом достаточно гибким. Принцип работы я покажу на примере формы логина. Чтобы не заморачиваться со стилями, будем использовать react-bootstrap.

В тестовом приложении логин возможен 3 способами: по логину, электронной почте или номеру телефона. Соответственно, для каждого вида логина нужны свои конкретные валидации. Для простоты примера, я покажу лишь компонент с формой логина, ссылку на репозиторий проекта вы сможете найти в конце статьи.

Итак, у нас есть компонент Login, который и отвечает за нашу форму. В нем есть 3 различных способа логина, соответственно, удобно было бы отобразить их с помощью вкладок:

Исходный код вкладок с полями ввода
<Tab.Pane eventKey="login">
  <Formsy.Form onValidSubmit={this.handleLogin} onValid={this.enableButton} onInvalid={this.disableButton}>

  <FormGroup>
    <ControlLabel>Login</ControlLabel>
      <TextInput name="login" type="text" validations={{minLength: 5}} validationErrors={{minLength: 'Enter at least 5 sumbols'}} required/>
    </FormGroup>

    <FormGroup>
      <ControlLabel>Password</ControlLabel>
        <TextInput name="loginPassword" type="password" validations={{strongPassword: validations.strongPassword}} validationErrors={{strongPassword: "Enter a strong password! At least 6 symbols"}} required/>
    </FormGroup>

    <FormGroup>
      <Button type="submit" bsStyle="primary" disabled={!this.state.isButtonEnabled} block>Login</Button>
    </FormGroup>

    </Formsy.Form>
</Tab.Pane>

<Tab.Pane eventKey="email">
  <Formsy.Form onValidSubmit={this.handleLogin} onValid={this.enableButton} onInvalid={this.disableButton}>

    <FormGroup>
      <ControlLabel>Email</ControlLabel>
        <TextInput name="email" type="text" validations={{isGoogleEmail: validations.isGoogleEmail}} validationErrors={{isGoogleEmail: 'Only Gmail boxes are accepted'}} required/>
    </FormGroup>

    <FormGroup>
      <ControlLabel>Password</ControlLabel>
      <TextInput name="loginPassword" type="password" validations={{strongPassword: validations.strongPassword}} validationErrors={{strongPassword: "Enter a strong password! At least 6 symbols"}} required/>
    </FormGroup>

    <FormGroup>
      <Button type="submit" bsStyle="primary" disabled={!this.state.isButtonEnabled} block>Login</Button>
    </FormGroup>

  </Formsy.Form>
</Tab.Pane>

<Tab.Pane eventKey="phone">
  <Formsy.Form onValidSubmit={this.handleLogin} onValid={this.enableButton} onInvalid={this.disableButton}>

    <FormGroup>
      <ControlLabel>Phone</ControlLabel>
      <TextInput name="phone" type="tel" validations={{isPhoneNumber: validations.isPhoneNumber, containsPlusPrefix: validations.containsPlusPrefix}} validationErrors={{isPhoneNumber: 'You should enter a valid phone number', containsPlusPrefix: 'Enter your number without +'}} required/>
    </FormGroup>

    <FormGroup>
      <ControlLabel>Password</ControlLabel>
      <TextInput name="phonePassword" type="password" validations={{strongPassword: validations.strongPassword}} validationErrors={{strongPassword: "Your password should contain at least 1 number, 1 lowercase letter, 1 uppercase letter"}} required/>
    </FormGroup>

    <FormGroup>
      <Button type="submit" bsStyle="primary" disabled={!this.state.isButtonEnabled} block>Login</Button>
    </FormGroup>

  </Formsy.Form>
</Tab.Pane>


Каждая вкладка представляет из себя форму, внутри которой лежит 2 поля и кнопка для отправки формы.

Начнем по порядку, с компонента Formsy.Form. В нем нас интересует 3 свойства (props): onValidSubmit, onValid, onInvalid.

Свойство onValidSubmit отвечает за отправку формы. Если все данные введены корректно и пользователь нажимает кнопку логин, то происходит вызов функции this.handleLogin, которая отправляет данные на сервер. Эта функция должна иметь один параметр, который будет содержать в себе объект. В этом объекте хранятся названия полей и их значения.

Свойства onValid и onInvalid отвечают за состояние кнопки отправки. В них стоит передать функцию, которая будет включать или выключать кнопку в зависимости от корректности введенных данных.

Далее у нас есть поля, которые необходимо проверять на валидность введенных данных. Для нормального функционирования данного компонента, нам необходимо создать собственный компонент для ввода данных (TextInput). На сайте Formsy можно найти готовый компонент, который включает в себя все что нужно и требует минимум изменений перед использованием. В наш компонент необходимо передать все стандартные свойства для тега input, такие как type и пр, а также несколько специальных свойств, которые помогут с валидацией компонента.

Первым таким свойством является name. Когда пользователь нажмет на кнопку отправки данных, вы сможете легко получить нужный input по имени, а также его значение.

Также необходимо обозначить свойство validations. В данном свойстве ожидается объект. В компоненте Formsy уже заложены некоторые валидации, например, minLength, которую я использовал в поле логин. Не сложно догадаться, что с помощью этой проверки мы можем установить минимальное число введенных символов. Есть и много других встроенных валидаций, например, на проверку корректности адреса электронной почты, телефона и другие. Однако, этот компонент не был бы так хорош, если бы нельзя было создавать собственные функции проверки. И это возможно!

Например, в поле с вводом электронной почты я объявил собственную проверку того, что человек вводит адрес электронной почты, зарегистрированный на gmail.com. Функция проверки выглядит следующим образом:

Исходный код функции проверки
function isGoogleEmail(values, value) {
  if (typeof value !== 'undefined' && value.indexOf('gmail.com') === -1) {
    return false;
  }
  return true;
}


Функция принимает в себя 2 параметра: все поля формы и текущее поле, которое проходит проверку. Первый параметр содержит массив всех полей и их значений, которые присутствуют в форме. Во втором параметре value содержится содержимое текущего поля. Внутри функции обязательно нужно проверить, существует ли value, а также произвести любые другие проверки. Если функция возвращает false, значит проверка не прошла и пользователь не сможет отправить форму. В обратном случае все ок, данное поле прошло проверку и не содержит ошибок

Второе свойство, которое мы должны передать в поле ввода — validationErrors. Вся прелесть этого свойства в том, что для каждой ошибки оно будет выдавать свое сообщение. Таким образом, можно повесить на одно поле разные проверки (например, кол-во введенных символов, наличие хотя бы одного специального символа и пр) и для каждой из этих проверок выдавать свою ошибку (а не писать в одном сообщении о том, что поле должно содержать минимум 8 символов, 2 цифры, 3 буквы и пр). Данный подход используется в поле для ввода телефона, где вначале происходит «супер надежная проверка» на то, что введенный текст действительно является телефоном, а потом проверяется, не стоит ли в начале '+'.

Таким образом, данный компонент здорово помогает с валидацией форм, которой так не достает в React. Полный код данного примера вы можете посмотреть в этом репозитории, а также не забудьте посмотреть в репозиторий Formsy, там еще очень много всего интересного. Надеюсь, что статья поможет вам при создании вашего приложения.

Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (17)


  1. stardust_kid
    27.07.2016 13:42

    Советую также взглянуть на React Formal. Обеспечивает валидацию форм на основе схем. Есть много преднастроенных и можно писать свои. Очень гибкая конфигурация и разработчик старается не навязывать своих подходов.

    Код формы выглядит примерно так:
    var Form = require('react-formal')
      , yup = require('yup')
    
    // if we are using a different set of inputs
    // we can set some defaults once at the beginning
    Form.addInputTypes(
      require('react-formal-inputs'))
    
    var defaultStr = yup.string().default('')
    
    var modelSchema = yup.object({
    
        name: yup.object({
          first: defaultStr.required('please enter a first name'),
          last:  defaultStr.required('please enter a surname'),
        }),
    
        dateOfBirth: yup.date()
          .max(new Date(), "You can't be born in the future!"),
    
        colorId: yup.number().nullable()
          .required('Please select a color')
      });
    
    var form = (
      <Form
        schema={modelSchema}
        defaultValue={modelSchema.default()}
      >
        <div>
          <label>Name</label>
    
          <Form.Field name='name.first' placeholder='First name'/>
          <Form.Field name='name.last' placeholder='Surname'/>
    
          <Form.Message for={['name.first', 'name.last']}/>
        </div>
    
        <label>Date of Birth</label>
        <Form.Field name='dateOfBirth'/>
        <Form.Message for='dateOfBirth'/>
    
        <label>Favorite Color</label>
        <Form.Field name='colorId' type='select'>
          <option value={null}>Select a color...</option>
          <option value={0}>Red</option>
          <option value={1}>Yellow</option>
          <option value={2}>Blue</option>
          <option value={3}>other</option>
        </Form.Field>
        <Form.Message for='colorId'/>
    
      <Form.Button type='submit'>Submit</Form.Button>
    </Form>)
    
    React.render(form, mountNode);
    


  1. SoloFeeD
    27.07.2016 14:14

    Зачем передавать в в функцию все поля с данными для проверки конкретного поля?


    1. theWaR_13
      27.07.2016 14:15

      Честно говоря, я тоже не совсем понимаю, зачем автор компонента это сделал. Возможно, существуют какие-то кейсы, когда это необходимо.


    1. shir
      27.07.2016 14:19
      +4

      Первое что приходит на ум — в зависимости от другого поля (например, чекбокса), проверять заполнено ли поле.


    1. crocodile2u
      27.07.2016 14:22
      +2

      пример:
      dropdown-box «Откуда узнали про наш проект?» Последняя опция — «свой вариант». Если ее выбираешь, появляется текстовое поле. Это текстовое поле нужно проверить на заполненность только если в dropdown выбрано «свой вариант».


    1. VasilioRuzanni
      27.07.2016 14:53

      Еще кейс — «дата начала не может быть позже даты окончания [чего-либо]» и ее более сложные вариации.


      1. SoloFeeD
        27.07.2016 14:54

        Та кейсов можно много придумать, так какая разница плодить в одном методе ифы, или сделать отдельно конкретный метод и передавать туда те поля, которые нужны для валидации?


    1. Miraage
      27.07.2016 15:11

      Просто люди не знают, зачем нужен паттерн Mediator.


  1. yociyavi
    27.07.2016 14:44

    Пользуюсь https://validatejs.org/ Либа не зависит ни от фреймворка, ни от платформы.


  1. n0ne
    27.07.2016 17:42

    А мне больше нравится redux-form… и её material-ная версия


  1. indestructable
    27.07.2016 21:50
    +1

    Считаю, что валидация форм — это тупиковый путь, который годится только для простейших приложений. Валидировать нужно бизнес сущности, и обязательно на стороне сервера (а на стороне клиента — опционально).
    В сложном приложении бывает несколько форм для одной сущности, разные правила в зависимости от ролей пользователя, инлайн редактирование в гридах — делать валидацию форм — это значит дублировать код.
    Валидация сущностей — это бизнес логика, и подход к ней должен быть соответствующий.


    1. theWaR_13
      27.07.2016 22:32

      На данный момент я занимаюсь разработкой достаточно большого приложения. И вот взять даже такую простую вещь, как форма логина. Без валидаций пользователь может отправить данные на сервер даже не заполнив поля ввода. А учитывая тот факт, что нагрузки на сервер могут быть достаточно серьезными, зачем лишний раз его перегружать его бесполезными запросами? Да, возможно где-то код и будет дублироваться, но принцип DRY стоит использовать разумно, а не фанатично пытаясь избежать любого повторения.
      Кроме того, не все же делают какие-то большие приложения. Смысл данной статьи рассказать про один конкретный способ валидации форм, возможно, сэкономив кому то время.


      1. indestructable
        27.07.2016 23:25

        Вы, наверное, не совсем правильно меня поняли. Я не призываю отказаться от клиентской валидации, но она должна быть дополнением к серверной, в идеале использовать один и тот же код.


        1. stardust_kid
          28.07.2016 13:48

          В таких случаях, принято говорить: спасибо, Кэп.


  1. QtRoS
    28.07.2016 08:17

    Функция проверки могла бы выглядеть вот так:

    function isGoogleEmail(values, value) {
      return !value || value.indexOf('gmail.com') != -1
    }
    

    Лаконичность и краткость ценится всегда, независимо от фреймвока и языка, а писать 'if (x) return true else return false' — уровень университета.


    1. neoxack
      28.07.2016 11:55
      +1

      И читабельность ухудшается раз так в 5


    1. stardust_kid
      28.07.2016 13:53

      В даннном случае возвращаемое плохо читабельно. И реальной пользы, кроме демонстрации вашего умения использовать Булевы операторы, тут нет. Так что «уровень университета» — это про ваш пример.
      Но по сути данная проверка бесполезна, поскольку у Gmail есть еще несколько доменов кроме «gmail.com», и никто не знает какие они добавят завтра. А еще есть gmail на собственном домене.