Доброго времени суток, друзья!

В этом небольшом туториале я хочу продемонстировать вам пример клиент-серверной валидации формы.

Клиент будет реализован на React, сервер на Express.

Мы не будем изобретать велосипеды, а воспользуемся готовыми решениями: для валидации формы на стороне клиента будет использоваться react-hook-form (+: используются хуки, русский язык), а на стороне сервера — express-validator.

Для стилизации будет использоваться styled-components (CSS-in-JS или All-in-JS, учитывая JSX).

Исходный код примера находится здесь.

Поиграть с кодом можно здесь.

Без дальнейших предисловий.

Клиент


Создаем проект с помощью create-react-app:

yarn create react-app form-validation
# или
npm init react-app form-validation
# или
npx create-react-app form-validation

В дальнейшем для установки зависимостей и выполнения команд я буду использовать yarn.

Структура проекта после удаления лишних файлов:

public
  index.html
src
  App.js
  index.js
  styles.js
server.js
...

Устанавливаем зависимости:

# для клиента
yarn add styled-components react-hook-form

# для сервера (производственные зависимости)
yarn add express express-validator cors

# для сервера (зависимость для разработки)
yarn add -D nodemon

# для одновременного запуска серверов
yarn add concurrently

Поскольку styled-components не умеет импотировать шрифты, нам придется добавить их в public/index.html:

<head>
  ...
  <link rel="preconnect" href="https://fonts.gstatic.com" />
  <link
    href="https://fonts.googleapis.com/css2?family=Comfortaa&display=swap"
    rel="stylesheet"
  />
</head>

Наша форма будет состоять из трех полей: имя пользователя, его адрес электронной почты и пароль. Условия, которым должны удовлетворять данные:

  • Имя
    • от 2 до 10 символов
    • кириллица

  • Email
    • особых требований не предъявляется

  • Пароль
    • 8-12 символов
    • латиница: буквы в любом регистре, цифры, нижнее подчеркивание и дефис


Начнем со стилизации (src/styles.js; для подстветки синтаксиса я использую расширение для VSCode — vscode-styled-components):

// импорт инструментов
import styled, { createGlobalStyle } from 'styled-components'

// глобальные стили
const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    min-height: 100vh;
    display: grid;
    place-items: center;
    background-color: #1c1c1c;
    font-family: 'Comfortaa', cursive;
    font-size: 14px;
    letter-spacing: 1px;
    color: #f0f0f0;
  }
`

// заголовок
const StyledTitle = styled.h1`
  margin: 1em;
  color: orange;
`

// форма
const StyledForm = styled.form`
  margin: 0 auto;
  width: 320px;
  font-size: 1.2em;
  text-align: center;
`

// подпись
const Label = styled.label`
  margin: 0.5em;
  display: grid;
  grid-template-columns: 1fr 2fr;
  align-items: center;
  text-align: left;
`

// проект поля для ввода данных
const BaseInput = styled.input`
  padding: 0.5em 0.75em;
  font-family: inherit;
  font-size: 0.9em;
  letter-spacing: 1px;
  outline: none;
  border: none;
  border-radius: 4px;
`

// обычное поле
const RegularInput = styled(BaseInput)`
  background-color: #f0f0f0;
  box-shadow: inset 0 0 2px orange;

  &:focus {
    background-color: #1c1c1c;
    color: #f0f0f0;
    box-shadow: inset 0 0 4px yellow;
  }
`

// поле для отправки данных на сервер
const SubmitInput = styled(BaseInput)`
  margin: 1em 0.5em;
  background-image: linear-gradient(yellow, orange);
  cursor: pointer;

  &:active {
    box-shadow: inset 0 1px 3px #1c1c1c;
  }
`

// проект сообщения с текстом
const BaseText = styled.p`
  font-size: 1.1em;
  text-align: center;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
`

// сообщение об ошибке
const ErrorText = styled(BaseText)`
  font-size: ${(props) => (props.small ? '0.8em' : '1.1em')};
  color: red;
`

// сообщение об успехе
const SuccessText = styled(BaseText)`
  color: green;
`

// экспорт стилизованных компонентов
export {
  GlobalStyle,
  StyledTitle,
  StyledForm,
  Label,
  RegularInput,
  SubmitInput,
  ErrorText,
  SuccessText
}

Импортируем и подключаем глобальные стили в src/index.js:

import React from 'react'
import ReactDOM from 'react-dom'

// импортируем глобальные стили
import { GlobalStyle } from './styles'

import App from './App'

ReactDOM.render(
  <React.StrictMode>
    {/* подключаем глобальные стили */}
    <GlobalStyle />
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

Переходим к основному файлу клиента (src/App.js):

import { useState } from 'react'
// импорт хука для валидации формы
import { useForm } from 'react-hook-form'

// импорт стилизованных компонентов
import {
  StyledTitle,
  StyledForm,
  Label,
  RegularInput,
  SubmitInput,
  ErrorText,
  SuccessText
} from './styles'

// компонент заголовка
function Title() {
  return <StyledTitle>Валидация формы</StyledTitle>
}

// компонент формы
function Form() {
  // инициализируем начальное состояние
  const [result, setResult] = useState({
    message: '',
    success: false
  })

  // извлекаем средства валидации:
  // регистрация проверяемого поля
  // ошибки и обработка отправки формы
  const { register, errors, handleSubmit } = useForm()

  // общие валидаторы
  const validators = {
    required: 'Не может быть пустым'
  }

  // функция отправки формы
  async function onSubmit(values) {
    console.log(values)

    const response = await fetch('http://localhost:5000/server', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(values)
    })

    const result = await response.json()

    // обновляем состояние
    setResult({
      message: result,
      success: response.ok
    })
  }

  // нажатие кнопки сброса полей в исходное состояние приводит к перезагрузке страницы
  function onClick() {
    window.location.reload()
  }

  return (
    <>
      <StyledForm onSubmit={handleSubmit(onSubmit)}>
        <Label>
          Имя:
          <RegularInput
            type='text'
            name='name'
            // поля являются неуправляемыми
            // это повышает производительность
            ref={register({
              ...validators,
              minLength: {
                value: 2,
                message: 'Не менее двух букв'
              },
              maxLength: {
                value: 10,
                message: 'Не более десяти букв'
              },
              pattern: {
                value: /[А-ЯЁ]{2,10}/i,
                message: 'Только киррилица'
              }
            })}
            defaultValue='Иван'
          />
        </Label>
        {/* ошибки */}
        <ErrorText small>{errors.name && errors.name.message}</ErrorText>

        <Label>
          Email:
          <RegularInput
            type='email'
            name='email'
            ref={register({
              ...validators,
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: 'Неправильный адрес электронной почты'
              }
            })}
            defaultValue='email@example.com'
          />
        </Label>
        <ErrorText small>{errors.email && errors.email.message}</ErrorText>

        <Label>
          Пароль:
          <RegularInput
            type='password'
            name='password'
            ref={register({
              ...validators,
              pattern: {
                value: /^[A-Z0-9_-]{8,12}$/i,
                message:
                  'От 8 до 12 символов: латиница, цифры, нижнее подчеркивание и дефис'
              }
            })}
            defaultValue='password'
          />
        </Label>
        <ErrorText small>
          {errors.password && errors.password.message}
        </ErrorText>

        <SubmitInput type='submit' defaultValue='Отправить' />

        {/* обратите внимание на атрибут "as", он позволяет превратить "инпут" в кнопку с аналогичными стилями */}
        <SubmitInput as='button' onClick={onClick}>
          Сбросить
        </SubmitInput>
      </StyledForm>

      {/* результат отправки формы */}
      {result.success ? (
        <SuccessText>{result.message}</SuccessText>
      ) : (
        <ErrorText>{result.message}</ErrorText>
      )}
    </>
  )
}

export default function App() {
  return (
    <>
      <Title />
      <Form />
    </>
  )
}

Метод register() хука useForm() поддерживает все атрибуты тега input. Полный список таких атрибутов. В случае с именем, мы могли бы ограничиться регулярным выражением.

Запускаем сервер для клиента с помощью yarn start и тестируем форму:



Замечательно. Валидация на стороне клиента работает, как ожидается. Но ее всегда можно отключить. Поэтому нужна валидация на сервере.

Сервер


Приступаем к реализации сервера (server.js):

const express = require('express')
// body читает тело запроса
// validationResult - результат валидации
const { body, validationResult } = require('express-validator')
const cors = require('cors')

const app = express()
const PORT = process.env.PORT || 5000

app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: false }))

// валидаторы
const validators = [
  body('name').trim().notEmpty().isAlpha('ru-RU').escape(),
  body('email').normalizeEmail().isEmail(),
  // кастомный валидатор
  body('password').custom((value) => {
    const regex = /^[A-Z0-9_-]{8,12}$/i

    if (!regex.test(value)) throw new Error('Пароль не соответствует шаблону')

    return true
  })
]

// валидаторы передаются в качестве middleware
app.post('/server', validators, (req, res) => {
  // извлекаем массив с ошибками из результата валидации
  const { errors } = validationResult(req)

  console.log(errors)

  // если массив с ошибками не является пустым
  if (errors.length) {
    res.status(400).json('Регистрация провалилась')
  } else {
    res.status(201).json('Регистрация прошла успешно')
  }
})

app.listen(PORT, () => {
  console.log(`Сервер готов. Порт: ${PORT}`)
})

Полный список доступных валидаторов можно посмотреть здесь.

Добавим в package.json парочку скриптов — «server» для запуска сервера и «dev» для одновременного запуска серверов:

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "server": "nodemon server",
  "dev": "concurrently \"yarn server\" \"yarn start\""
}

Выполняем yarn dev и тестируем отправку формы:





Прекрасно. Кажется, у нас все получилось.

Мы с вами рассмотрели очень простой вариант клиент-серверной валидации формы. Вместе с тем, более сложные варианты предполагают лишь увеличение количества валидаторов, общие принципы остаются такими же. Также стоит отметить, что валидацию формы на стороне клиента вполне можно реализовать средствами HTML (GitHub, CodeSandbox).

Благодарю за внимание и хорошего дня.