Валидация форм в React

Если вы когда-нибудь пользовались фреймворком 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, там еще очень много всего интересного. Надеюсь, что статья поможет вам при создании вашего приложения.

Спасибо за внимание!
Share post

Similar posts

Comments 17

    0
    Советую также взглянуть на 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);
    

      0
      Зачем передавать в в функцию все поля с данными для проверки конкретного поля?
        0
        Честно говоря, я тоже не совсем понимаю, зачем автор компонента это сделал. Возможно, существуют какие-то кейсы, когда это необходимо.
          +4
          Первое что приходит на ум — в зависимости от другого поля (например, чекбокса), проверять заполнено ли поле.
            +2
            пример:
            dropdown-box «Откуда узнали про наш проект?» Последняя опция — «свой вариант». Если ее выбираешь, появляется текстовое поле. Это текстовое поле нужно проверить на заполненность только если в dropdown выбрано «свой вариант».
              0
              Еще кейс — «дата начала не может быть позже даты окончания [чего-либо]» и ее более сложные вариации.
                0
                Та кейсов можно много придумать, так какая разница плодить в одном методе ифы, или сделать отдельно конкретный метод и передавать туда те поля, которые нужны для валидации?
                0
                Просто люди не знают, зачем нужен паттерн Mediator.
                0
                Пользуюсь https://validatejs.org/ Либа не зависит ни от фреймворка, ни от платформы.
                  0
                  А мне больше нравится redux-form… и её material-ная версия
                    +1

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

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

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

                          0
                          В таких случаях, принято говорить: спасибо, Кэп.
                      0
                      Функция проверки могла бы выглядеть вот так:
                      function isGoogleEmail(values, value) {
                        return !value || value.indexOf('gmail.com') != -1
                      }
                      

                      Лаконичность и краткость ценится всегда, независимо от фреймвока и языка, а писать 'if (x) return true else return false' — уровень университета.
                        +1
                        И читабельность ухудшается раз так в 5
                          0
                          В даннном случае возвращаемое плохо читабельно. И реальной пользы, кроме демонстрации вашего умения использовать Булевы операторы, тут нет. Так что «уровень университета» — это про ваш пример.
                          Но по сути данная проверка бесполезна, поскольку у Gmail есть еще несколько доменов кроме «gmail.com», и никто не знает какие они добавят завтра. А еще есть gmail на собственном домене.

                        Only users with full accounts can post comments. Log in, please.