Этот пост из моего блога.

Вы также можете проверить видео на YouTube.

Создать html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Form Validation</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <h1>Form Validation</h1>
  </header>
  <div class="container">
    <form class="form" data-form>
      <div class="form__group" data-formgroup>
        <label for="name">Full name</label>
        <input 
          type="text" id="name" name="name"
          data-validate data-required data-required-message="Name is required"
        >
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group" data-formgroup>
        <label for="email">Email</label>
        <input 
          type="text" name="email" id="email"
          data-validate data-required 
          data-email data-email-message="Email is not valid"
        >
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group" data-formgroup>
        <label for="password">Password</label>
        <input 
          type="password" name="password" id="password"
          data-validate data-required 
          data-minlength="6" data-maxlength="16"
          data-match-with="confirmPassword"
        >
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group" data-formgroup>
        <label for="confirmPassword">Confirm Password</label>
        <input 
          type="password" name="confirmPassword" id="confirmPassword"
          data-validate data-required 
          data-match="password" data-match-message="Passwords must be equal"
        >
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group form__group--radio" data-formgroup>
        <p>Gender</p>
        <label>
          <input type="radio" name="gender" value="female" 
            data-validate data-error-message="Gender is required">
          <span>Female</span>
        </label>
        <label>
          <input type="radio" name="gender" value="male" data-validate>
          <span>Male</span>
        </label>
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group" data-formgroup>
        <label for="difficulty">Difficulty</label>
        <select name="difficulty" id="difficulty" data-validate data-required>
          <option value="">Select difficulty</option>
          <option value="easy">Easy</option>
          <option value="medium">Medium</option>
          <option value="hard">Hard</option>
        </select>
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group" data-formgroup>
        <label for="image">Image</label>
        <input type="file" name="image" id="image"
          data-validate data-required data-maxfilesize="1024"
          data-allowed-types="jpg,png" data-allowed-types-message="Only jpg and png formats allowed"
        >
        <div class="form__error" data-formerror></div>
      </div>
      <div class="form__group">
        <label for="description">Description</label>
        <textarea name="description" id="description"></textarea>
      </div>
      <div class="form__group form__group--checkbox" data-formgroup>
        <label>
          <input type="checkbox" name="terms" id="terms"
            data-validate data-error-message="You must accept our Terms and Conditions"
          >
          <span>Terms and Conditions</span>
        </label>
        <div class="form__error" data-formerror></div>
      </div>
      <button type="submit" class="btn">Submit</button>
    </form>
  </div>
  <script src="form.js"></script>
</body>
</html>

Для проверки формы добавьте атрибут data-form к элементу формы. Затем добавьте data-formgroup в каждый div-оболочку. Каждый div имеет элемент метки, элемент ввода и div для сообщения об ошибке.

На этом этапе ничего не будет проверено, потому что нам нужно добавить еще несколько атрибутов к элементу ввода. Первый — проверка данных, он указывает javascript включить этот элемент в процесс проверки. Теперь нам нужно добавить атрибуты, чтобы сообщить js, какую проверку ему нужно проверить.

данные-требуются, данные-требуются-сообщение

Этот сообщит js, что требуется входное значение. Каждый атрибут также имеет соответствующий атрибут для сообщения об ошибке. Чтобы добавить его, просто добавьте -message к атрибуту имени атрибута. Например, для обязательных данных мы можем добавить data-required-message="This field is required". Если не будет добавлено сообщение об ошибке по умолчанию, оно будет добавлено в javascript.

минимальная длина данных, минимальная длина данных-сообщение

Это используется, чтобы сообщить js, что длина входного значения не может быть меньше числа, которое вы указали в этом атрибуте, например, data-minlength="5". Для сообщения об ошибке используйте data-minlength-message. Опять же, этот атрибут является необязательным, если он не установлен, будет использоваться сообщение об ошибке по умолчанию.

данные-maxlength, данные-maxlength-сообщение

Этот проверяет, превышает ли длина входного значения число, которое вы указали в атрибуте.

электронная почта с данными, электронная почта с данными

data-email проверяет, является ли введенное значение действительным адресом электронной почты.

совпадение данных, совпадение данных с, сообщение-соответствие данных

Эти атрибуты используются в паролях. Например, когда у вас есть регистрационная форма, вы, вероятно, добавите поле подтверждения пароля, чтобы пользователь мог повторить пароль.

В поле исходного пароля вы можете добавить атрибут data-match-with, и значение этого атрибута должно быть значением атрибута имени ввода подтверждения пароля. И при вводе подтверждения пароля вы должны добавить атрибут match-with со значением атрибута имени исходного ввода пароля. А для сообщения об ошибке вы можете добавить ошибку совпадения данных к вводу подтверждения пароля.

Радио-кнопки

Если вы хотите проверять переключатели, вы должны добавить проверку данных к каждому переключателю в группе, а для сообщения об ошибке добавить сообщение об ошибке данных к первому переключателю в группе.

флажок

Чтобы проверить флажок, добавьте атрибут проверки данных в флажок, а для сообщения об ошибке вы можете добавить сообщение об ошибке данных.

ПРИМЕЧАНИЕ: для переключателей и флажков вам не нужно добавлять атрибут data-required.

ввод файла, data-maxfilesize, data-allowed-types

Для входных файлов вы можете добавить атрибут data-maxfilesize, он будет проверять, больше ли размер файла, чем значение этого атрибута, и в этом он покажет ошибку. Для ошибки вы можете использовать data-maxfilesize-message.

И если вы хотите проверить тип файла, вы можете добавить атрибут data-allowed-types. Значением этого атрибута является расширение файла. Чтобы добавить несколько значений, разделите их запятой и без пробела после запятой. Например: data-allowed-types="jpg,png,pdf". Для сообщения об ошибке используйте атрибут data-allowed-types-message.

И это все, что касается атрибутов. Еще одна вещь: каждый блок ошибок должен иметь атрибут data-formerror.

Создать CSS

@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
:root {
  --primary-color: #009e6c;
  --error-color: #e70f0f;
}
* {
  box-sizing: border-box;
  margin: 0;
}
body {
  font-family: 'Roboto', sans-serif;
  font-size: 16px;
  line-height: 1.5;
}
header {
  background-color: var(--primary-color);
  color: #fff;
  text-align: center;
  padding: 50px 0;
  margin-bottom: 50px;
}
.container {
  max-width: 600px;
  margin: 0 auto;
  padding-bottom: 50px;
}
.form {
  border: 1px solid #eee;
  padding: 40px;
}
.form__group {
  margin-bottom: 20px;
}
.form__group label,
.form__group p {
  display: block;
  font-size: 14px;
  margin-bottom: 5px;
}
.form__group--radio label,
.form__group--checkbox label {
  display: inline-flex;
  align-items: center;
  margin-right: 15px;
}
.form__group--radio label span,
.form__group--checkbox label span {
  margin-left: 5px;
}
.form__group input[type="text"],
.form__group input[type="file"],
.form__group input[type="password"],
.form__group select,
.form__group textarea {
  display: block;
  width: 100%;
  padding: 10px;
  font-size: 14px;
  border: 1px solid #eee;
  outline: 0;
  transition: box-shadow .3s ease;
}
.form__group input[type="text"]:focus,
.form__group input[type="file"]:focus,
.form__group input[type="password"]:focus,
.form__group select:focus,
.form__group textarea:focus {
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.form__group input[type="text"].error,
.form__group input[type="file"].error,
.form__group input[type="password"].error,
.form__group select.error,
.form__group textarea.error {
  border-color: var(--error-color);
}
.form__error {
  color: var(--error-color);
  font-size: 12px;
  padding: 5px 0;
  display: none;
}
.btn {
  display: inline-flex;
  align-items: center;
  padding: 10px 20px;
  background-color: var(--primary-color);
  color: #fff;
  border: 0;
  outline: 0;
  cursor: pointer;
}

CSS очень прост. Я только что добавил несколько стилей для стилизации формы. Самое главное — скрыть ошибочный div по умолчанию и добавить некоторый класс к элементу ввода, когда ошибка видна, в данном примере это класс ошибки.

JS-часть

Первое, что мы должны сделать в js, — это получить все элементы формы с атрибутом data-form, проверить, существуют ли они на странице, а затем прокрутить их.

// Get elements
const forms = document.querySelectorAll('form[data-form]');
// Check if form elements exist
if(forms.length > 0) {
  // Loop through forms
  for(let form of forms) {
    // Get all inputs with data-validate attribute
    const inputs = form.querySelectorAll('[data-validate]');
    // Submit form
    form.addEventListener('submit', submitForm.bind(form, inputs));
    
    // Loop through inputs
    inputs.forEach(input => {
      // Add input event to all inputs and listen to inputChange function
      input.addEventListener('input', inputChange);
    });
  }
}

После этого для каждой формы мы можем получить элементы формы, которые необходимо проверить, мы можем получить их по атрибуту [data-validate]. Затем мы можем добавить прослушиватель событий для события ввода, чтобы проверять каждый ввод при изменении значения ввода. А также добавьте прослушиватель событий для типа события отправки в форме, чтобы при нажатии кнопки отправки мы также могли проверять входные данные.

Теперь нужные нам функции:

// Input change
function inputChange() {
  const input = this;
  validateInput(input);
}
// Validate input
function validateInput(input) {
  // Get the value and error element
  const value = input.value;
  const errorEl = input.closest('[data-formgroup]').querySelector('[data-formerror]');
  // Declare error variable and assign null by default
  let error = null;
  // Check in input has data-required attribute and if the value is empty, and if the input is not radio or checkbox
  if((input.type !== 'radio' || input.type !== 'checkbox') && input.dataset.required !== undefined && value === '') {
    error = input.dataset.requiredMessage ? input.dataset.requiredMessage : 'This field is required';
    input.classList.add('error');
  }
  // Check if input is checkbox and it is not checked
  if(input.type === 'checkbox' && !input.checked) {
    error = input.dataset.errorMessage ? input.dataset.errorMessage : 'This field is required';
  }
  // Check if input is radio
  if(input.type === 'radio') {
    // Get all radio inputs in a group
    const radios = input.closest('[data-formgroup]').querySelectorAll('input[type="radio"]');
    let isChecked = false;
    let errorMsg = '';
    // Loop through radios and check if any radio is checked and if it is checked set isChecked to true
    radios.forEach(radio => {
      if(radio.checked) {
        isChecked = true;
      }
      if(radio.dataset.errorMessage) {
        errorMsg = input.dataset.errorMessage;
      }
    });
    if(!isChecked) {
      error = errorMsg !== '' ? errorMsg : 'This field is required';
    }
  }
  // Check if input has data-minlength attribute and if value length is smaller than this attribute, if so show the error
  if(!error && input.dataset.minlength !== undefined && value.length < +input.dataset.minlength) {
    error = 
      input.dataset.minlengthMessage ? input.dataset.minlengthMessage : `Please enter at least ${input.dataset.minlength} characters`;
    input.classList.add('error');
  }
  // Check if input has data-maxlength attribute and if value length is greater than this attribute, if so show the error
  if(!error && input.dataset.maxlength !== undefined && value.length > +input.dataset.maxlength) {
    error = 
      input.dataset.maxlengthMessage ? input.dataset.maxlengthMessage : `Only ${input.dataset.maxlength} characters allowed`;
    input.classList.add('error');
  }
  // Check if input has data-email attribute and if email is not valid
  if(!error && input.dataset.email !== undefined && !validateEmail(value)) {
    error = 
      input.dataset.emailMessage ? input.dataset.emailMessage : 'Invalid email address';
    input.classList.add('error');
  }
  // Check if input has data-match attribute and if value is not equal to the value of the element with name attribute equal to this data-match attribute
  if(!error && input.dataset.match !== undefined && value !== input.closest('[data-form]').querySelector(`[name="${input.dataset.match}"]`).value) {
    error = 
      input.dataset.matchMessage ? input.dataset.matchMessage : 'Fields are not the same';
    input.classList.add('error');
  }
  // Check if input has data-match-with attribute
  if(input.dataset.matchWith !== undefined) {
    // Get the input that has a name attribute equal to value of data-match-with attribute
    const inputToMatch = input.closest('[data-form]').querySelector(`[name="${input.dataset.matchWith}"]`);
    // Get the error element of that input
    const inputToMatchError = inputToMatch.closest('[data-formgroup]').querySelector('[data-formerror]');
    // If values are equal remove error class from input and hide error element
    if(value === inputToMatch.value) {
      inputToMatch.classList.remove('error');
      inputToMatchError.style.display = 'none';
    }else { // Add error class to input and show error element
      inputToMatch.classList.add('error');
      inputToMatchError.style.display = 'block';
      inputToMatchError.innerText = inputToMatch.dataset.matchMessage || 'Fields are not the same';
    }
  }
  // Check if input is file input and if has data-maxfilesize attribute and if file size is greater than the value of this data-maxfilesize attribute 
  if(!error && input.type === 'file' && input.dataset.maxfilesize !== undefined && input.files[0].size > +input.dataset.maxfilesize * 1024) {
    error = 
      input.dataset.maxfilesizeMessage ? input.dataset.maxfilesizeMessage : 'File is too large';
    input.classList.add('error');
  }
  // Check if input is file input and if it has data-allowed-types attribute and if file type is not equal to one of the values in data-allowed-type attribute
  if(!error && input.type === 'file' && input.dataset.allowedTypes !== undefined && !input.dataset.allowedTypes.includes(input.files[0].type.split('/')[1])) {
    error = 
      input.dataset.allowedTypesMessage ? input.dataset.allowedTypesMessage : 'Invalid file type';
    input.classList.add('error');
  }
  
  // If there is no error remove error class from the input, remove message from error element and hide it
  if(!error) {
    input.classList.remove('error');
    errorEl.innerText = '';
    errorEl.style.display = 'none';
  } else { // If there is error set error message and show error element
    errorEl.innerText = error;
    errorEl.style.display = 'block';
  }
  return error;
}
// Submit form - on submit btn click
function submitForm(inputs, e) {
  e.preventDefault();
  const errors = [];
  
  inputs.forEach(input => {
    const error = validateInput(input);
    if(error) {
      errors.push(error);
    }
  });
  if(errors.length === 0) {
    console.log('form can be submitted...');
  }
}
// Validate email
function validateEmail(email) {
  var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(String(email).toLowerCase());
}

Прежде всего, нам нужна функция inputChange. В этой функции мы получаем ввод по этому ключевому слову, а затем мы можем просто вызвать функцию validateInput и передать ввод в качестве аргумента.

Для submitForm мы делаем то же самое, за исключением того, что мы передаем все входные данные с помощью bind для циклического перебора входных данных и вызываем validateInput для каждого входного элемента. Мы также можем создать массив ошибок, и если validateInput возвращает ошибку, а не ноль, мы добавляем эту ошибку в массив. Таким образом, после forEach мы можем проверить, пуст ли массив ошибок, и только в этом случае мы можем отправить форму.

Теперь самая важная функция, validateInput.

Сначала мы получаем значение ввода. А затем элемент error div и установите для переменной error значение null по умолчанию.

// Get the value and error element
  const value = input.value;
  const errorEl = input.closest('[data-formgroup]').querySelector('[data-formerror]');
  // Declare error variable and assign null by default
  let error = null;

В первом операторе if мы проверяем, не является ли тип ввода радио или флажком, имеет ли ввод атрибут data-required и является ли он пустым. В этом случае мы устанавливаем сообщение об ошибке, которое было передано в атрибут data-required-message, или мы используем сообщение об ошибке по умолчанию, если на входе нет атрибута data-required-message, и мы добавляем к входу класс ошибки.

// Check in input has data-required attribute and if the value is empty, and if the input is not radio or checkbox
  if((input.type !== 'radio' || input.type !== 'checkbox') && input.dataset.required !== undefined && value === '') {
    error = input.dataset.requiredMessage ? input.dataset.requiredMessage : 'This field is required';
    input.classList.add('error');
  }

Во втором операторе if мы проверяем, является ли тип ввода флажком и не установлен ли флажок. Если это правда, мы устанавливаем переменную ошибки в значение атрибута data-error-message или используем сообщение об ошибке по умолчанию.

// Check if input is checkbox and it is not checked
  if(input.type === 'checkbox' && !input.checked) {
    error = input.dataset.errorMessage ? input.dataset.errorMessage : 'This field is required';
  }

В следующем операторе if мы проверяем, является ли тип ввода радио. Если это так, мы получаем все переключатели в этой группе, устанавливаем для переменной isChecked значение false, а для errorMsg — пустую строку.

Затем мы перебираем переключатели и устанавливаем для isChecked значение true, если радио проверено, и устанавливаем для errorMsg сообщение об ошибке (только если у радио есть атрибут data-error-message). После forEach мы проверяем, является ли isChecked ложным, поэтому, если ни один из переключателей не отмечен, мы устанавливаем ошибку в errorMsg или сообщение по умолчанию.

// Check if input is radio
  if(input.type === 'radio') {
    // Get all radio inputs in a group
    const radios = input.closest('[data-formgroup]').querySelectorAll('input[type="radio"]');
    let isChecked = false;
    let errorMsg = '';
    // Loop through radios and check if any radio is checked and if it is checked set isChecked to true
    radios.forEach(radio => {
      if(radio.checked) {
        isChecked = true;
      }
      if(radio.dataset.errorMessage) {
        errorMsg = input.dataset.errorMessage;
      }
    });
    if(!isChecked) {
      error = errorMsg !== '' ? errorMsg : 'This field is required';
    }
  }

ПРИМЕЧАНИЕ: в следующих операторах if мы всегда сначала проверяем, не установлена ​​ли ошибка, потому что если это так, мы не хотим показывать несколько сообщений об ошибках.

Следующий оператор if предназначен для атрибута data-minlength. Здесь мы проверяем, добавлен ли к входным данным атрибут data-minlength и меньше ли длина входного значения, чем значение атрибута data-minlength. В этом случае показать сообщение об ошибке и добавить класс ошибки на вход.

// Check if input has data-minlength attribute and if value length is smaller than this attribute, if so show the error
  if(!error && input.dataset.minlength !== undefined && value.length < +input.dataset.minlength) {
    error = 
      input.dataset.minlengthMessage ? input.dataset.minlengthMessage : `Please enter at least ${input.dataset.minlength} characters`;
    input.classList.add('error');
  }

Следующий — для атрибута data-maxlength. Если этот атрибут находится на входе и если длина значения больше, чем значение этого атрибута, нам нужно показать сообщение об ошибке.

// Check if input has data-maxlength attribute and if value length is greater than this attribute, if so show the error
  if(!error && input.dataset.maxlength !== undefined && value.length > +input.dataset.maxlength) {
    error = 
      input.dataset.maxlengthMessage ? input.dataset.maxlengthMessage : `Only ${input.dataset.maxlength} characters allowed`;
    input.classList.add('error');
  }

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

// Check if input has data-email attribute and if email is not valid
  if(!error && input.dataset.email !== undefined && !validateEmail(value)) {
    error = 
      input.dataset.emailMessage ? input.dataset.emailMessage : 'Invalid email address';
    input.classList.add('error');
  }

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

// Check if input has data-match attribute and if value is not equal to the value of the element with name attribute equal to this data-match attribute
  if(!error && input.dataset.match !== undefined && value !== input.closest('[data-form]').querySelector(`[name="${input.dataset.match}"]`).value) {
    error = 
      input.dataset.matchMessage ? input.dataset.matchMessage : 'Fields are not the same';
    input.classList.add('error');
  }

Выше оператор if предназначен для поля подтверждения пароля. И в исходное поле пароля мы должны добавить атрибут data-match-with. Теперь мы можем проверить, установлен ли атрибут data-match-with, и если это так, мы сначала получаем ввод со значением атрибута name, равным значению атрибута data-match-with, а также получаем блок ошибок этого ввода. Теперь мы можем проверить, равны ли значения этих входов, если они равны, мы можем удалить сообщение об ошибке и класс ошибки, иначе мы добавим сообщение об ошибке и класс ошибки в inputToMatch.

// Check if input has data-match-with attribute
  if(input.dataset.matchWith !== undefined) {
    // Get the input that has a name attribute equal to value of data-match-with attribute
    const inputToMatch = input.closest('[data-form]').querySelector(`[name="${input.dataset.matchWith}"]`);
    // Get the error element of that input
    const inputToMatchError = inputToMatch.closest('[data-formgroup]').querySelector('[data-formerror]');
    // If values are equal remove error class from input and hide error element
    if(value === inputToMatch.value) {
      inputToMatch.classList.remove('error');
      inputToMatchError.style.display = 'none';
    }else { // Add error class to input and show error element
      inputToMatch.classList.add('error');
      inputToMatchError.style.display = 'block';
      inputToMatchError.innerText = inputToMatch.dataset.matchMessage || 'Fields are not the same';
    }
  }

В следующем операторе if мы проверяем, является ли тип ввода файлом, и имеет ли он атрибут data-maxfilesize и если размер выбранного файла больше, чем значение атрибута data-maxfilesize, в этом случае мы показываем ошибку.

// Check if input is file input and if has data-maxfilesize attribute and if file size is greater than the value of this data-maxfilesize attribute 
  if(!error && input.type === 'file' && input.dataset.maxfilesize !== undefined && input.files[0].size > +input.dataset.maxfilesize * 1024) {
    error = 
      input.dataset.maxfilesizeMessage ? input.dataset.maxfilesizeMessage : 'File is too large';
    input.classList.add('error');
  }

Следующий оператор if также предназначен для ввода файла. Нам нужно проверить, является ли тип ввода файлом, имеет ли ввод атрибут data-allowed-types и не включает ли значение атрибута data-allowed-types значение типа ввода. Я разбиваю значение типа ввода, потому что по умолчанию это возвращает что-то вроде «image/png», а меня интересует только «png». Если все это верно, мы можем показать сообщение об ошибке.

// Check if input is file input and if it has data-allowed-types attribute and if file type is not equal to one of the values in data-allowed-type attribute
  if(!error && input.type === 'file' && input.dataset.allowedTypes !== undefined && !input.dataset.allowedTypes.includes(input.files[0].type.split('/')[1])) {
    error = 
      input.dataset.allowedTypesMessage ? input.dataset.allowedTypesMessage : 'Invalid file type';
    input.classList.add('error');
  }

Последнее, что нужно сделать, это проверить, установлена ​​​​ли ошибка, если это не так, мы удаляем класс ошибки и скрываем сообщение об ошибке, в противном случае мы устанавливаем содержимое блока ошибок в сообщение об ошибке, а затем показываем ошибку. Нам не нужно добавлять здесь класс ошибок, потому что мы делаем это в других операторах is. И в конце функции validateInput мы возвращаем ошибку (нулевое значение или сообщение об ошибке), чтобы использовать это значение в функции submitForm.

И это все, что нам нужно сделать. Как видите, это на самом деле не сложно. И вы можете легко добавить новые валидаторы внутри (новые операторы if).