Xác thực dữ liệu đầu vào sau khi tương tác

Vấn đề

Việc hiển thị lỗi xác thực ngay khi người dùng tập trung vào một trường và bắt đầu nhập là quá sớm và gây mất tập trung. Ví dụ: khi người dùng nhập địa chỉ email (ví dụ: "user@gm") hoặc mật khẩu có yêu cầu phức tạp, trường này về mặt kỹ thuật sẽ không hợp lệ cho đến khi hoàn tất. Kiểu :invalid chuẩn dẫn đến trạng thái lỗi xuất hiện ngay lập tức, khiến người dùng cảm thấy khó chịu.

Giải pháp

Lớp giả :user-invalid cho phép bạn trì hoãn trạng thái lỗi cho đến khi người dùng "cam kết" với một giá trị (bằng cách làm mờ trường) hoặc cố gắng gửi biểu mẫu. Điều này đảm bảo thông tin phản hồi xác thực chỉ được cung cấp sau khi người dùng hoàn tất việc tương tác với trường.

Chiến lược triển khai

  1. Ràng buộc HTML: NÊN sử dụng các thuộc tính HTML5 tiêu chuẩn như type="email", pattern và required để kích hoạt logic xác thực tích hợp sẵn của trình duyệt.
  2. Phản hồi bằng hình ảnh: NÊN sử dụng :user-invalid để chỉ áp dụng kiểu lỗi sau khi tương tác.
  3. Củng cố tích cực: NÊN sử dụng :user-valid để đưa ra chỉ báo "thành công" màu xanh lục sau khi đáp ứng các yêu cầu.
  4. Khôi phục linh hoạt: Ngay khi người dùng sửa dữ liệu đầu vào thành định dạng hợp lệ, :user-invalid sẽ ngừng so khớp và xoá trạng thái lỗi ngay lập tức.

Hướng dẫn triển khai

Trường hợp sử dụng 1: Xác thực email

BẮT BUỘC: Dựa vào các thuộc tính HTML5 tiêu chuẩn cho các trường email. Thông báo lỗi bị ẩn theo mặc định và chỉ xuất hiện khi trình duyệt xác định rằng người dùng đã rời khỏi trường ở trạng thái không hợp lệ.

<form>
  <div class="field">
    <label for="email">Email Address</label>
    <!-- MANDATORY: Place format hints above the input so autocomplete popovers don't cover them during editing -->
    <span id="email-hint" class="hint">Format: you@example.com</span>
    <!-- DO: Use standard HTML validation attributes like type="email" and required -->
    <input
      type="email"
      id="email"
      name="email"
      required
      autocomplete="email"
      aria-describedby="email-hint"
      aria-errormessage="email-error"
    >
    <div id="email-error" class="error-msg">
      <span aria-hidden="true">❌</span> Please enter a valid email address.
    </div>
  </div>
</form>
.hint {
  display: block;
  color: #5f6368;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

.error-msg {
  display: none;
  color: #d93025;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/*
  DO: Only show error styles after user interaction.
  Use multiple indicators (border/background shift + icon/text) to avoid color-only states.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Reveal the error message using the adjacent sibling selector */
input:user-invalid + .error-msg {
  display: block;
}

/* DO: Provide a clear success indication on :user-valid */
input:user-valid {
  border-color: #188038;
}

Trường hợp sử dụng 2: Độ phức tạp của mật khẩu

BẮT BUỘC: Xác định quy tắc về độ phức tạp bằng cách sử dụng mẫu Regex Lookahead trong thuộc tính pattern. Danh sách quy tắc xuất hiện phía trên dữ liệu đầu vào để hướng dẫn người dùng và được làm nổi bật nếu có lỗi.

<form>
  <div class="field">
    <label for="password">New Password</label>
    <!-- MANDATORY: Place hints and rules above the input so mobile keyboards do not obscure them -->
    <ul id="password-rules" class="rules-list">
      <li>At least 8 characters</li>
      <li>One uppercase letter</li>
      <li>One number</li>
      <li>One special character</li>
    </ul>
    <!-- DO: Use pattern and minlength for complex password validation
         DO: Match all constraints with lookaheads via pattern attribute
     -->
    <input
      type="password"
      id="password"
      autocomplete="new-password"
      required
      pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,}"
      minlength="8"
      aria-describedby="password-rules"
    >
  </div>
</form>
/* DO: State the default styling as neutral */
.rules-list { 
  color: #5f6368; 
  margin-bottom: 0.5rem;
}

/* DO: Show invalid state (After interaction): Error */
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Highlight rules list when error is shown using the modern :has() selector */
.field:has(input:user-invalid) .rules-list {
  color: #d93025;
  font-weight: 600;
}

/* DO: Add success indications for :user-valid state */
input:user-valid {
  border-color: #188038;
}

/* DO: Hide rules once satisfied */
.field:has(input:user-valid) .rules-list {
  display: none;
}

Dự phòng và hỗ trợ trình duyệt

Dự phòng và hỗ trợ trình duyệt cho :user-valid và :user-invalid

CSS cho quảng cáo dự phòng

input:user-invalid,
input.user-invalid-fallback {
  border-color: #d93025;
  background-color: #fce8e6;
}

input:user-invalid + .error-msg,
input.user-invalid-fallback + .error-msg {
  display: block;
}

Dự phòng JavaScript

Sử dụng một tiện ích có thể tái sử dụng để theo dõi trạng thái tương tác bằng WeakMap. Điều này giúp tránh làm ô nhiễm DOM bằng các lớp hoặc thuộc tính dữ liệu "bẩn".

const UserInvalidFallback = (() => {
  const dirtyState = new WeakMap();

  const updateState = (input) => {
    const isValid = input.checkValidity();

    // Update both visual and ARIA state
    input.classList.toggle('user-invalid-fallback', !isValid);
    input.classList.toggle('user-valid-fallback', isValid);

    if (!isValid) {
      input.setAttribute('aria-invalid', 'true');
    } else {
      input.removeAttribute('aria-invalid');
    }
  };

  const handleEvent = (event) => {
    const input = event.target;

    if (event.type === 'reset') {
      const controls = input.elements || [];
      for (const control of controls) {
        dirtyState.delete(control);
        control.classList.remove('user-invalid-fallback');
        control.classList.remove('user-valid-fallback');
        control.removeAttribute('aria-invalid');
      }
      return;
    }

    if (!input.checkValidity) return;

    if (event.type === 'input' || event.type === 'change') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasInteracted = true;
      dirtyState.set(input, state);
      if (state.hasBlurred) {
        updateState(input);
      }
    } else if (event.type === 'blur') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasBlurred = true;
      dirtyState.set(input, state);
      if (state.hasInteracted) {
        updateState(input);
      }
    }
  };

  const init = (root = document) => {
    if (CSS.supports('selector(:user-invalid)')) return;

    root.addEventListener('blur', handleEvent, true); // Capture phase
    root.addEventListener('input', handleEvent);
    root.addEventListener('change', handleEvent);
    root.addEventListener('reset', handleEvent, true); // Capture resets
  };

  return { init };
})();

// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);

Những điều cần xem xét khác

  1. Hỗ trợ tiếp cận:
    • BẮT BUỘC: Sử dụng aria-describedby để liên kết danh sách quy tắc với dữ liệu đầu vào.
    • KHÔNG NÊN: Ẩn hoàn toàn danh sách quy tắc cho đến khi thông tin nhập vào hợp lệ; người dùng cần biết nên nhập gì!
  2. Giới hạn thuộc tính mẫu: BẮT BUỘC: Thuộc tính pattern thực hiện so khớp đầy đủ (^...$ ngầm định). Đảm bảo biểu thức chính quy mật khẩu của bạn tính đến toàn bộ chuỗi.
  3. Mức độ nghiêm ngặt của quy trình xác thực: Xin lưu ý rằng quy trình xác thực type="email" mặc định của trình duyệt khá dễ dãi (ví dụ: user@localserver có thể vượt qua). Nếu cần xác thực nghiêm ngặt hơn, bạn có thể phải sử dụng một thư viện xác thực mạnh mẽ hơn hoặc một hàm xác thực tuỳ chỉnh cùng với type="email".
  4. Quản lý tiêu điểm: BẮT BUỘC: Nếu người dùng gửi biểu mẫu có trường không hợp lệ, thì trình duyệt sẽ tự động lấy tiêu điểm vào trường không hợp lệ đầu tiên. Kiểu :user-invalid của bạn sẽ được áp dụng ngay lập tức vì một lần gửi được tính là một lượt tương tác.
  5. Trải nghiệm ARIA nhất quán: :user-invalid gốc không tự động đồng bộ hoá với các thuộc tính ARIA. Thêm JavaScript sau để đồng bộ hoá aria-invalid với trạng thái hình ảnh:
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
  el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};

// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
  if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});