Статьи

Мышление вне DOM: составные валидаторы и сбор данных

В первой части этого мини-сериала мы обсудили проблему, общую для многих баз кода JavaScript: тесно связанный код. Затем я познакомил вас с преимуществами разделения ортогональных проблем. В качестве подтверждения концепции мы начали разработку системы проверки форм, которая не ограничивается формами и даже может работать вне DOM.

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

Составленные валидаторы

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

Это возможно с текущим дизайном:

var rules = [
  pattern('email', /@/, 'Your email is missing an @'),
  pattern('email', /^\S+@/, 'Please enter the username in your email address',
  // ...
];

Хотя это будет работать, для адреса электронной почты может появиться несколько сообщений об ошибках. Это также требует, чтобы мы вручную повторяли каждый шаг для каждого поля, имеющего семантику электронной почты. Даже если мы еще не обсуждали визуализацию сообщений об ошибках, было бы неплохо иметь абстракцию для группировки нескольких валидаторов таким образом, чтобы он отображал только результат первого нарушенного правила. Как оказалось, это точная семантика оператора && Введите and Этот валидатор примет несколько валидаторов в качестве аргументов и применяет их все, пока не найдет ошибочный:

 function and() {
  var rules = arguments;

  return function (data) {
    var result, l = rules.length;

    for (var i = 0; i < l; ++i) {
      result = rules[i](data);
      if (result) {
        return result;
      }
    }
  };
}

Теперь мы можем выразить нашу валидатор электронной почты таким образом, чтобы за один раз всплыло только одно сообщение об ошибке:

 var rules = [and(
  pattern('email', /@/, 'Your email is missing an @'),
  pattern('email', /^\S+@/, 'Please enter the username in your email address',
  // ...
)];

Затем это можно кодифицировать как отдельный валидатор:

 function email(id, messages) {
  return and(
    pattern('email', /@/, messages.missingAt),
    pattern('email', /^\S+@/, messages.missingUser)
    // ...
  );
}

Пока мы говорим об адресах электронной почты, люди продолжают делать одну ошибку, когда я живу, — вводить адреса Hotmail и Gmail с нашим национальным доменом верхнего уровня (например, «…@hotmail.no»). Было бы очень полезно иметь возможность предупреждать пользователя, когда это происходит. Чтобы сформулировать это по-другому: иногда мы хотим выполнять определенные проверки только при соблюдении определенных критериев. Чтобы решить эту проблему, мы введем функцию when

 function when(pred, rule) {
  return function (data) {
    if (pred(data)) {
      return rule(data);
    }
  };
}

Как вы можете видеть, whenrequired Вы вызываете его с помощью предиката (функция, которая будет получать проверяемые данные) и валидатора. Если функция предиката возвращает true В противном случае, when

Предикат, который нам нужен для решения нашей проблемы с Hotmail, проверяет соответствие значения шаблону:

 function matches(id, re) {
  return function (data) {
    return re.test(data[id]);
  };
}

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

 function email(id, messages) {
  return and(
    pattern(id, /@/, messages.missingAt),
    pattern(id, /^\S+@/, messages.missingUser),
    pattern(id, /@\S+$/, messages.missingDomain),
    pattern(id, /@\S+\.\S+$/, messages.missingTLD),
    when(matches(id, /@hotmail\.[^\.]+$/),
      pattern(id, /@hotmail\.com$/, messages.almostHotmail)
    ),
    when(matches(id, /@gmail\.[^\.]+$/),
      pattern(id, /@gmail\.com$/, messages.almostGmail)
    )
  );
}

Это можно использовать так:

 email('email', {
  missingAt: 'Missing @',
  missingUser: 'You need something in front of the @',
  missingDomain: 'You need something after the @',
  missingTLD: 'Did you forget .com or something similar?',
  almostHotmail: 'Did you mean hotmail<strong>.com</strong>?',
  almostGmail: 'Did you mean gmail<strong>.com</strong>?'
});

Если вы хотите поиграть с этой функцией, я создал CodePen специально для вас.

Извлечение данных

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

 <form action="/doit" novalidate>
  <label for="email">
    Email
    <input type="email" name="email" id="email" value="[email protected]">
  </label>
  <label for="password">
    Password
    <input type="password" name="password" id="password">
  </label>
  <label class="faded hide-lt-pad">
    <input type="checkbox" name="remember" value="1" checked>
    Remember me
  </label>
  <button type="submit">Login</button>
</form>

В это:

 {
  email: '[email protected]',
  password: '',
  remember: '1'
}

Реализация этого в шагах с тестами довольно проста, но для этого потребуются элементы DOM. Ниже приведен пример того, как выглядят эти тесты:

 describe('extractData', function () {
  it('fetches data out of a form', function () {
    var form = document.createElement('form');
    var input = document.createElement('input');
    input.type = 'text';
    input.name = 'phoneNumber';
    input.value = '+47 998 87 766';
    form.appendChild(input);

    assert.deepEqual(extractData(form), {'phoneNumber': '+47 998 87 766'});
  });
});

Это не так уж и плохо, и с помощью еще одной небольшой абстракции мы можем немного ее улучшить:

 it('fetches data out of a form', function () {
  var form = document.createElement('form');
  addElement(
    form,
    'input',
    {type: 'text', name: 'phoneNumber', value: '+47 998 87 766'}
  );

  assert.deepEqual(extractData(form), {'phoneNumber': '+47 998 87 766'});
});

Извлечение данных — это выбор всех элементов inputselecttextareaname Для извлечения правильного значения из флажков и переключателей требуется специальная обработка. Основная функция выглядит так:

 function extractData(form) {
  return getInputs(form).reduce(function (data, el) {
    var val = getValue[el.tagName.toLowerCase()](el);
    if (val) { data[el.name] = val.trim(); }
    return data;
  }, {});
};

Как видно из этого фрагмента, extractData()getInputs() Цель этой вспомогательной функции — получить массив элементов DOM формы, переданной в качестве аргумента. В этой статье я не собираюсь рассказывать об этом, потому что эта функция опирается на другие небольшие функции, и я хочу избежать эффекта «Начало». Однако, если вы хотите копать больше, вы можете взглянуть на созданный мной репозиторий GitHub, который содержит все файлы из предыдущей и этой частей.

Давайте теперь посмотрим, как мы можем сообщить об ошибках.

Отчет об ошибках

Чтобы сообщать об ошибках, мы можем разработать функцию, которая принимает форму и массив ошибок. Однако, есть одна проблема, которую нужно решить: чтобы избежать дублирования ошибок в DOM, функция должна либо сохранять состояние, поэтому она знает, какие ошибки она уже вызвала, или должна предполагать, что каждая ошибка в форме может быть стерта, когда новый набор предоставлен. Какое решение подходит, будет зависеть от ваших конкретных случаев использования.

Я не буду вдаваться в детали реализации рендеринга, но предложу следующее упрощенное решение:

 function renderErrors(form, errors) {
  removeErrors(form);
  errors.forEach(function (error) {
    renderError(form, error);
  });
}

Чтобы отобразить ошибку, мы находим вход, к которому она относится, и вставляем элемент прямо перед ней. Мы только делаем первую ошибку. Это очень простая стратегия рендеринга, но она хорошо работает:

 function renderError(form, error) {
  var input = form.querySelector("[name=" + error.id + "]");
  var el = document.createElement("div");
  el.className = "error js-validation-error";
  el.innerHTML = error.messages[0];
  input.parentNode.insertBefore(el, input);
}

В приведенном выше коде вы можете видеть, что я назначаю элементу два класса: errorjs-validation-error Первый предназначен только для стилизации. Последний предназначен в качестве внутреннего механизма, используемого следующей removeErrors()

 function removeErrors(form) {
  var errors = form.querySelectorAll(".js-validation-error");

  for (var i = 0, l = errors.length; i < l; ++i) {
    errors[i].parentNode.removeChild(errors[i]);
  }
}

Базовая демонстрация системы отчетов об ошибках, которую мы создали в этом разделе, показана в этом CodePen .

Все вместе

Теперь у нас есть (одна версия) все части: чтение из DOM, проверка чистых данных и рендеринг результатов проверки обратно в DOM. Все, что нам сейчас нужно, это высокоуровневый интерфейс, чтобы связать их всех вместе:

 validateForm(myForm, [
  required("login", "Please choose a login"),
  email("email", i18n.validation.emailFormat),
  confirmation("password", "password-confirmation", "Passwords don't match")
], {
  success: function (e) {
    alert("Congratulations, it's all correct!");
  }
});

Как и при рендеринге, эта высокоуровневая разводка может быть как глупо простой, так и довольно сложной. В проекте, где большая часть этого кода возникла, функция validateForm() Если бы были ошибки проверки, он бы входил в своего рода «умный режим проверки в реальном времени»: исправленные ошибки удалялись бы как можно быстрее (например, при keyupblur Эта модель находила хороший баланс между мгновенной обратной связью и нытьем (никто не любит слышать, что «ваша электронная почта неверна», прежде чем они даже закончили печатать)

Теперь, когда я завершил описание этой последней части, я приглашаю вас взглянуть на демонстрацию, включенную в репозиторий GitHub . Он включает в себя весь код, который мы обсудили, а также полные тестовые примеры.

Вывод

Сила этой модели заключается в том, что внешние механизмы ввода / вывода полностью отделены от реализации «правил», которая действительно является сердцем библиотеки. Эта модель может быть легко использована для других видов проверки данных. Механизм правил также может быть расширен за счет включения информации об успешном исправлении ошибок (например, возвращая что-то вроде {id: 'name', ok: true} Возможно, также имеет смысл разрешить механизму правил работать с асинхронными операциями.

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