Skip to content

📅 NGÀY 9: FORMS & INPUT HANDLING - Nền Tảng Controlled Components

🎯 Mục tiêu học tập (5 phút)

Sau bài học này, bạn sẽ:

  • [ ] Hiểu được cách form elements hoạt động trong React khác với HTML thuần
  • [ ] Nắm vững khái niệm Controlled Components (lý thuyết) và tại sao React khuyến nghị pattern này
  • [ ] Xử lý được form events và truy xuất giá trị input thông qua event.target
  • [ ] Phân biệt được uncontrolled vs controlled inputs (chuẩn bị cho useState ở Ngày 11)
  • [ ] Implement được form validation cơ bản với event handlers

🤔 Kiểm tra đầu vào (5 phút)

Trả lời 3 câu hỏi này để kích hoạt kiến thức nền:

  1. Event Handling (Ngày 5): Làm thế nào để lấy giá trị từ event.target.value?
  2. Conditional Rendering (Ngày 5): Làm sao hiển thị error message khi điều kiện nào đó xảy ra?
  3. Props (Ngày 4): Callback function được truyền qua props có thể nhận parameters không?
💡 Xem đáp án
  1. Trong event handler: const value = event.target.value
  2. Dùng && hoặc ternary: {error && <span>{error}</span>}
  3. Có! Ví dụ: onSubmit={(formData) => console.log(formData)}

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Hãy tưởng tượng bạn đang xây dựng form đăng ký như sau:

jsx
// ❌ HTML thuần - Cách cũ
<form>
  <input
    type='text'
    name='email'
  />
  <input
    type='password'
    name='password'
  />
  <button type='submit'>Đăng ký</button>
</form>

Những vấn đề khi dùng HTML form thuần trong React:

  1. Không kiểm soát được giá trị realtime → Không validate khi user đang gõ
  2. DOM quản lý state → React không biết giá trị hiện tại
  3. Khó implement UX tốt → Không disable button khi form invalid
  4. Không thể transform input → Ví dụ: format số điện thoại khi gõ
jsx
// ⚠️ Vấn đề cụ thể
function SignupForm() {
  // Làm sao biết email có hợp lệ TRƯỚC KHI user submit?
  // Làm sao disable button khi password < 8 ký tự?
  // Làm sao hiển thị strength meter cho password?

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        // ❌ Giá trị ở đâu? Phải query DOM!
        const formData = new FormData(e.target);
        console.log(formData.get('email')); // Cồng kềnh!
      }}
    >
      <input
        type='email'
        name='email'
      />
      <input
        type='password'
        name='password'
      />
      <button type='submit'>Đăng ký</button>
    </form>
  );
}

1.2 Giải Pháp - Controlled Components

React giải quyết bằng Controlled Components - React quản lý giá trị form:

jsx
// ✅ React Controlled Pattern (Lý thuyết)
function SignupForm() {
  // ⚠️ CHÚ Ý: Đây là GIẢI THÍCH PATTERN
  // useState sẽ học ở Ngày 11!

  // Concept: React lưu giá trị thay vì DOM
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');

  return (
    <form>
      {/* Value được React quản lý */}
      <input
        type='email'
        value={email} // ← React controls this
        onChange={(e) => setEmail(e.target.value)}
      />

      {/* Giờ có thể validate realtime */}
      {password.length > 0 && password.length < 8 && (
        <p>Mật khẩu phải ít nhất 8 ký tự</p>
      )}

      <input
        type='password'
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      {/* Disable button khi invalid */}
      <button
        type='submit'
        disabled={!email || password.length < 8}
      >
        Đăng ký
      </button>
    </form>
  );
}

🔑 Key Insight: Trong Controlled Component:

  • React là "single source of truth"
  • Input value luôn phản ánh React's state
  • Mọi thay đổi đều qua React (không có DOM mutations)

1.3 Mental Model

HTML FORM (Uncontrolled)
┌─────────────────────────────────────┐
│   DOM quản lý value                 │
│                                     │
│   User types → DOM updates          │
│                                     │
│   Submit → Read from DOM            │
└─────────────────────────────────────┘

      VS

REACT CONTROLLED FORM
┌─────────────────────────────────────┐
│   React quản lý value               │
│                                     │
│   User types → Event                │
│        ↓                            │
│   Update React state                │
│        ↓                            │
│   Re-render với value mới           │
│        ↓                            │
│   DOM reflects React state          │
└─────────────────────────────────────┘

🎯 Analogy: Controlled component giống như:

  • Uncontrolled: Bạn cho người lạ mượn xe → Không biết họ đi đâu cho đến khi trả xe
  • Controlled: Bạn làm tài xế → Biết chính xác đang ở đâu mọi lúc

1.4 Hiểu Lầm Phổ Biến

❌ Hiểu lầm 1: "Tôi cần useState để handle forms"

jsx
// ❌ SAI: Nghĩ phải có state mới handle được form
// Thực tế: Có thể handle events mà KHÔNG cần state!

function SearchBar() {
  // KHÔNG cần state nếu chỉ cần giá trị khi submit

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const query = e.target.elements.search.value;
        console.log('Search for:', query);
        // Gọi API, navigate, etc.
      }}
    >
      <input
        type='text'
        name='search' // ← Truy cập qua e.target.elements.search
        placeholder='Tìm kiếm...'
      />
      <button type='submit'>Tìm</button>
    </form>
  );
}

✅ ĐÚNG: Forms có thể hoạt động mà không cần state nếu:

  • Chỉ cần giá trị khi submit (không cần realtime validation)
  • Không cần disable/enable elements dựa trên giá trị
  • Không cần derived UI (ví dụ: character counter)

❌ Hiểu lầm 2: "onChange là event giống HTML"

jsx
// ❌ SAI: Nghĩ onChange trong React = onchange trong HTML
// HTML onchange: Chỉ fire khi blur
// React onChange: Fire sau mỗi keystroke!

function Demo() {
  return (
    <>
      {/* HTML thuần: onChange chỉ chạy khi blur */}
      <input
        type='text'
        onchange="console.log('HTML change')" // Chỉ khi blur!
      />

      {/* React: onChange chạy mỗi keystroke */}
      <input
        type='text'
        onChange={(e) => console.log('React change:', e.target.value)}
        // Gõ "hello" → 5 lần console.log!
      />
    </>
  );
}

✅ ĐÚNG: React's onChange thực chất là onInput của HTML!


❌ Hiểu lầm 3: "Controlled = Phải có value prop"

jsx
// ❌ SAI LẦM NGHIÊM TRỌNG: Controlled mà thiếu onChange
function BrokenInput() {
  return (
    <input
      type='text'
      value='Fixed value' // ← Controlled nhưng không có onChange
      // ❌ Input sẽ bị read-only!
    />
  );
}

// ⚠️ React warning:
// "You provided a `value` prop to a form field
//  without an `onChange` handler. This will render
//  a read-only field."

✅ ĐÚNG: Controlled component CẦN CẢ HAI:

  • value={something} để React control
  • onChange={handler} để update value

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Uncontrolled Form - Submit Handler Pattern ⭐

🎯 Use Case: Form đơn giản chỉ cần giá trị khi submit

jsx
/**
 * ✅ PATTERN: Uncontrolled với name attribute
 *
 * Khi nào dùng:
 * - Form đơn giản (1-3 fields)
 * - Không cần validation realtime
 * - Không cần disabled state dựa trên input
 *
 * Ưu điểm:
 * - Code ít hơn (không cần state)
 * - Performance tốt (ít re-renders)
 *
 * Nhược điểm:
 * - Không kiểm soát được giá trị
 * - Khó implement UX nâng cao
 */

function ContactForm() {
  // Hàm xử lý submit
  const handleSubmit = (event) => {
    event.preventDefault(); // ⚠️ BẮT BUỘC! Ngăn page reload

    // Cách 1: Dùng FormData API (Modern)
    const formData = new FormData(event.target);
    const data = {
      name: formData.get('name'),
      email: formData.get('email'),
      message: formData.get('message'),
    };

    console.log('Form submitted:', data);

    // Cách 2: Dùng event.target.elements
    const { name, email, message } = event.target.elements;
    console.log('Via elements:', {
      name: name.value,
      email: email.value,
      message: message.value,
    });

    // Reset form sau khi submit
    event.target.reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor='name'>Họ tên:</label>
        <input
          type='text'
          id='name'
          name='name' // ← KEY: name attribute để truy cập
          required // HTML validation
          placeholder='Nguyễn Văn A'
        />
      </div>

      <div>
        <label htmlFor='email'>Email:</label>
        <input
          type='email' // HTML tự validate email format
          id='email'
          name='email'
          required
          placeholder='example@email.com'
        />
      </div>

      <div>
        <label htmlFor='message'>Tin nhắn:</label>
        <textarea
          id='message'
          name='message'
          rows={4}
          required
          placeholder='Nội dung tin nhắn...'
        />
      </div>

      <button type='submit'>Gửi</button>
    </form>
  );
}

🔍 Key Learnings:

  1. event.preventDefault() - Ngăn form submit mặc định
  2. name attribute - Cách truy cập giá trị form
  3. FormData API - Modern way to read form values
  4. HTML validation - required, type="email" tự động validate

Demo 2: Event Handling Patterns - Real-world Scenarios ⭐⭐

🎯 Use Case: Xử lý nhiều loại form elements và events

jsx
/**
 * ✅ PATTERN: Comprehensive Event Handling
 *
 * Bao gồm:
 * - Text inputs, checkboxes, radio buttons
 * - Select dropdowns
 * - Event delegation
 * - Validation trước khi submit
 */

function RegistrationForm() {
  // Xử lý submit với validation
  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);

    // Extract data
    const data = {
      username: formData.get('username'),
      email: formData.get('email'),
      password: formData.get('password'),
      country: formData.get('country'),
      terms: formData.get('terms'), // checkbox: "on" hoặc null
      newsletter: formData.get('newsletter'), // "on" hoặc null
    };

    // Validation logic
    const errors = [];

    if (data.username.length < 3) {
      errors.push('Username phải ít nhất 3 ký tự');
    }

    if (data.password.length < 8) {
      errors.push('Mật khẩu phải ít nhất 8 ký tự');
    }

    if (!data.terms) {
      errors.push('Bạn phải đồng ý với điều khoản');
    }

    // Hiển thị errors
    if (errors.length > 0) {
      alert('Lỗi:\n' + errors.join('\n'));
      return;
    }

    // Submit success
    console.log('Registration data:', {
      ...data,
      terms: !!data.terms,
      newsletter: !!data.newsletter,
    });
  };

  // Xử lý change event cho preview
  const handleUsernameChange = (event) => {
    const value = event.target.value;
    console.log('Username changed:', value);

    // ⚠️ Lưu ý: Đây chỉ là log, không update UI
    // Để update UI cần state (Ngày 11)
  };

  const handlePasswordChange = (event) => {
    const value = event.target.value;

    // Đếm độ mạnh password (chỉ log, chưa hiển thị)
    let strength = 0;
    if (value.length >= 8) strength++;
    if (/[A-Z]/.test(value)) strength++;
    if (/[0-9]/.test(value)) strength++;
    if (/[^A-Za-z0-9]/.test(value)) strength++;

    console.log('Password strength:', strength + '/4');
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Text Input */}
      <div>
        <label htmlFor='username'>Username:</label>
        <input
          type='text'
          id='username'
          name='username'
          onChange={handleUsernameChange}
          placeholder='username123'
          minLength={3} // HTML validation
          required
        />
      </div>

      {/* Email Input */}
      <div>
        <label htmlFor='email'>Email:</label>
        <input
          type='email'
          id='email'
          name='email'
          placeholder='user@example.com'
          required
        />
      </div>

      {/* Password Input */}
      <div>
        <label htmlFor='password'>Password:</label>
        <input
          type='password'
          id='password'
          name='password'
          onChange={handlePasswordChange}
          minLength={8}
          required
        />
      </div>

      {/* Select Dropdown */}
      <div>
        <label htmlFor='country'>Quốc gia:</label>
        <select
          id='country'
          name='country'
          defaultValue='' // ← Giá trị mặc định
          required
        >
          <option
            value=''
            disabled
          >
            -- Chọn quốc gia --
          </option>
          <option value='vn'>Việt Nam</option>
          <option value='us'>United States</option>
          <option value='jp'>Japan</option>
          <option value='kr'>South Korea</option>
        </select>
      </div>

      {/* Checkbox */}
      <div>
        <label>
          <input
            type='checkbox'
            name='terms'
            required // HTML yêu cầu check
          />{' '}
          Tôi đồng ý với điều khoản sử dụng
        </label>
      </div>

      {/* Checkbox (optional) */}
      <div>
        <label>
          <input
            type='checkbox'
            name='newsletter'
            defaultChecked // ← Checked mặc định
          />{' '}
          Nhận bản tin qua email
        </label>
      </div>

      <button type='submit'>Đăng ký</button>
    </form>
  );
}

🔑 Key Patterns:

jsx
// ✅ Checkbox value
const isChecked = formData.get('checkbox'); // "on" hoặc null
const boolValue = !!formData.get('checkbox'); // true hoặc false

// ✅ Select default value
<select defaultValue="vn">  // Không dùng selected attribute

// ✅ Input events
onChange  // Mỗi keystroke (React khác HTML!)
onBlur    // Khi rời khỏi input
onFocus   // Khi focus vào input
onInput   // Giống onChange trong React

Demo 3: Edge Cases & Validation ⭐⭐⭐

🎯 Use Case: Handle các trường hợp đặc biệt và validate phức tạp

jsx
/**
 * ✅ ADVANCED: Edge Cases Handling
 *
 * Xử lý:
 * - Whitespace trimming
 * - Cross-field validation
 * - Custom validation rules
 * - Error display patterns
 */

function AdvancedForm() {
  const handleSubmit = (event) => {
    event.preventDefault();

    // Helper: Lấy giá trị và trim whitespace
    const getValue = (name) => {
      const value = event.target.elements[name].value;
      return value.trim(); // ← Xóa khoảng trắng đầu/cuối
    };

    // Extract và sanitize data
    const data = {
      fullName: getValue('fullName'),
      email: getValue('email'),
      phone: getValue('phone'),
      password: getValue('password'),
      confirmPassword: getValue('confirmPassword'),
      age: event.target.elements.age.value,
    };

    // Validation errors array
    const errors = [];

    // 1. Required fields
    if (!data.fullName) {
      errors.push('Họ tên không được để trống');
    }

    if (!data.email) {
      errors.push('Email không được để trống');
    }

    if (!data.password) {
      errors.push('Mật khẩu không được để trống');
    }

    // 2. Format validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (data.email && !emailRegex.test(data.email)) {
      errors.push('Email không hợp lệ');
    }

    const phoneRegex = /^[0-9]{10,11}$/;
    if (data.phone && !phoneRegex.test(data.phone)) {
      errors.push('Số điện thoại phải là 10-11 chữ số');
    }

    // 3. Length validation
    if (data.fullName && data.fullName.length < 3) {
      errors.push('Họ tên phải ít nhất 3 ký tự');
    }

    if (data.password && data.password.length < 8) {
      errors.push('Mật khẩu phải ít nhất 8 ký tự');
    }

    // 4. Cross-field validation
    if (data.password !== data.confirmPassword) {
      errors.push('Mật khẩu xác nhận không khớp');
    }

    // 5. Range validation
    const ageNum = parseInt(data.age, 10);
    if (isNaN(ageNum) || ageNum < 18 || ageNum > 100) {
      errors.push('Tuổi phải từ 18 đến 100');
    }

    // 6. Password strength
    const hasUpperCase = /[A-Z]/.test(data.password);
    const hasLowerCase = /[a-z]/.test(data.password);
    const hasNumber = /[0-9]/.test(data.password);
    const hasSpecial = /[^A-Za-z0-9]/.test(data.password);

    if (data.password && !(hasUpperCase && hasLowerCase && hasNumber)) {
      errors.push('Mật khẩu phải có chữ hoa, chữ thường và số');
    }

    // Display errors
    if (errors.length > 0) {
      // ⚠️ Production: Nên hiển thị trên UI, không dùng alert
      console.error('Form validation errors:', errors);
      alert('Lỗi:\n\n' + errors.join('\n'));

      // Focus vào field đầu tiên bị lỗi (advanced)
      if (!data.fullName) {
        event.target.elements.fullName.focus();
      } else if (!emailRegex.test(data.email)) {
        event.target.elements.email.focus();
      }

      return;
    }

    // Success
    console.log('✅ Form valid:', data);

    // Xóa sensitive data trước khi log
    const safeData = { ...data };
    delete safeData.password;
    delete safeData.confirmPassword;
    console.log('Safe to send:', safeData);
  };

  // Prevent paste vào confirm password (UX debatable)
  const handleConfirmPasswordPaste = (event) => {
    event.preventDefault();
    alert('Vui lòng nhập lại mật khẩu thay vì copy/paste');
  };

  // Format phone number khi gõ
  const handlePhoneInput = (event) => {
    let value = event.target.value;

    // Chỉ giữ lại số
    value = value.replace(/[^0-9]/g, '');

    // Limit 11 số
    if (value.length > 11) {
      value = value.slice(0, 11);
    }

    // ⚠️ Không thể set value vì không có state
    // Chỉ log để demo
    console.log('Phone formatted:', value);

    // ℹ️ Để format realtime cần Controlled Component (Ngày 11)
  };

  return (
    <form
      onSubmit={handleSubmit}
      noValidate
    >
      {/* noValidate: Tắt HTML validation để dùng custom */}

      <div>
        <label htmlFor='fullName'>
          Họ tên: <span style={{ color: 'red' }}>*</span>
        </label>
        <input
          type='text'
          id='fullName'
          name='fullName'
          placeholder='Nguyễn Văn A'
          autoComplete='name'
          // ❌ KHÔNG dùng: defaultValue (để user tự nhập)
        />
      </div>

      <div>
        <label htmlFor='email'>
          Email: <span style={{ color: 'red' }}>*</span>
        </label>
        <input
          type='email'
          id='email'
          name='email'
          placeholder='example@email.com'
          autoComplete='email'
        />
      </div>

      <div>
        <label htmlFor='phone'>Số điện thoại:</label>
        <input
          type='tel'
          id='phone'
          name='phone'
          placeholder='0901234567'
          onInput={handlePhoneInput}
          maxLength={11}
        />
      </div>

      <div>
        <label htmlFor='age'>
          Tuổi: <span style={{ color: 'red' }}>*</span>
        </label>
        <input
          type='number'
          id='age'
          name='age'
          min='18'
          max='100'
          defaultValue='25'
        />
      </div>

      <div>
        <label htmlFor='password'>
          Mật khẩu: <span style={{ color: 'red' }}>*</span>
        </label>
        <input
          type='password'
          id='password'
          name='password'
          placeholder='Ít nhất 8 ký tự'
          autoComplete='new-password'
        />
        <small style={{ color: '#666', display: 'block' }}>
          Phải có chữ hoa, chữ thường và số
        </small>
      </div>

      <div>
        <label htmlFor='confirmPassword'>
          Xác nhận mật khẩu: <span style={{ color: 'red' }}>*</span>
        </label>
        <input
          type='password'
          id='confirmPassword'
          name='confirmPassword'
          placeholder='Nhập lại mật khẩu'
          autoComplete='new-password'
          onPaste={handleConfirmPasswordPaste}
        />
      </div>

      <button type='submit'>Đăng ký</button>
      <button type='reset'>Xóa form</button>
    </form>
  );
}

🎯 Advanced Techniques:

jsx
// ✅ Sanitize input
const value = input.value.trim(); // Xóa whitespace
const cleaned = value.replace(/[^a-zA-Z0-9]/g, ''); // Chỉ giữ alphanumeric

// ✅ Cross-field validation
if (password !== confirmPassword) { /* error */ }

// ✅ Regex validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValid = emailRegex.test(email);

// ✅ Focus management
if (hasError) {
  inputElement.focus(); // Focus vào field lỗi
}

// ✅ Prevent paste
<input onPaste={(e) => e.preventDefault()} />

// ✅ Format while typing (giới hạn do không có state)
<input onInput={handleFormat} />

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Exercise 1: Login Form Cơ Bản (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Tạo form đăng nhập đơn giản
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: useState, useEffect, useReducer
 *
 * Requirements:
 * 1. Form có 2 fields: email và password
 * 2. Validate khi submit:
 *    - Email không được rỗng và phải hợp lệ
 *    - Password phải ít nhất 6 ký tự
 * 3. Hiển thị console.log khi login thành công
 * 4. Reset form sau khi submit thành công
 *
 * 💡 Gợi ý:
 * - Dùng event.preventDefault()
 * - Dùng FormData hoặc event.target.elements
 * - Dùng alert() hoặc console.error() cho errors
 */

// ❌ Cách SAI: Không validate
function LoginFormWrong() {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        // ❌ Submit trực tiếp mà không kiểm tra!
        console.log('Logged in');
      }}
    >
      <input
        type='email'
        name='email'
      />
      <input
        type='password'
        name='password'
      />
      <button type='submit'>Đăng nhập</button>
    </form>
  );
}

// ✅ Cách ĐÚNG: Có validation
function LoginFormCorrect() {
  const handleSubmit = (event) => {
    event.preventDefault();

    const email = event.target.elements.email.value.trim();
    const password = event.target.elements.password.value;

    // Validation
    const errors = [];

    if (!email) {
      errors.push('Email không được để trống');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.push('Email không hợp lệ');
    }

    if (password.length < 6) {
      errors.push('Mật khẩu phải ít nhất 6 ký tự');
    }

    if (errors.length > 0) {
      alert('Lỗi:\n' + errors.join('\n'));
      return;
    }

    // Success
    console.log('✅ Đăng nhập thành công:', { email });
    event.target.reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor='email'>Email:</label>
        <input
          type='email'
          id='email'
          name='email'
          placeholder='your@email.com'
          required
        />
      </div>

      <div>
        <label htmlFor='password'>Mật khẩu:</label>
        <input
          type='password'
          id='password'
          name='password'
          placeholder='Ít nhất 6 ký tự'
          required
        />
      </div>

      <button type='submit'>Đăng nhập</button>
    </form>
  );
}

// 🎯 NHIỆM VỤ CỦA BẠN:
function LoginForm() {
  // TODO: Implement handleSubmit function

  // TODO: Validate email (không rỗng + format hợp lệ)

  // TODO: Validate password (>= 6 chars)

  // TODO: Console.log khi success

  // TODO: Reset form sau khi submit

  return (
    <form /* TODO: onSubmit handler */>
      {/* TODO: Email input với label */}

      {/* TODO: Password input với label */}

      {/* TODO: Submit button */}
    </form>
  );
}

✅ Self-Check:

  • [ ] Form có email và password inputs?
  • [ ] Email được validate (empty + format)?
  • [ ] Password được validate (min length)?
  • [ ] Console.log hiển thị data khi success?
  • [ ] Form reset sau submit?
💡 Solution
jsx
/**
 * Login Form Cơ Bản - Uncontrolled pattern
 * Validate email và password khi submit
 * Không sử dụng state (useState)
 */
function LoginForm() {
  const handleSubmit = (event) => {
    event.preventDefault();

    // Lấy giá trị từ form
    const email = event.target.elements.email.value.trim();
    const password = event.target.elements.password.value;

    // Validation
    const errors = [];

    // Kiểm tra email
    if (!email) {
      errors.push('Email không được để trống');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.push('Email không hợp lệ');
    }

    // Kiểm tra password
    if (password.length < 6) {
      errors.push('Mật khẩu phải ít nhất 6 ký tự');
    }

    // Có lỗi → hiển thị và dừng
    if (errors.length > 0) {
      alert('Lỗi:\n' + errors.join('\n'));
      return;
    }

    // Thành công
    console.log('✅ Đăng nhập thành công:', { email });

    // Reset form
    event.target.reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor='email'>Email:</label>
        <input
          type='email'
          id='email'
          name='email'
          placeholder='your@email.com'
          required
        />
      </div>

      <div>
        <label htmlFor='password'>Mật khẩu:</label>
        <input
          type='password'
          id='password'
          name='password'
          placeholder='Ít nhất 6 ký tự'
          required
        />
      </div>

      <button type='submit'>Đăng nhập</button>
    </form>
  );
}

/* Ví dụ kết quả console khi submit thành công:
✅ Đăng nhập thành công: { email: "test@example.com" }
*/

⭐⭐ Exercise 2: Survey Form với Multiple Input Types (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Xử lý nhiều loại input khác nhau
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Tạo form khảo sát sản phẩm
 *
 * Requirements:
 * 1. Text input: Tên người dùng (required, min 3 chars)
 * 2. Email input: Email (required, valid format)
 * 3. Radio buttons: Độ tuổi (18-25, 26-35, 36-50, 50+)
 * 4. Checkboxes: Sản phẩm quan tâm (ít nhất 1)
 *    - Laptop
 *    - Smartphone
 *    - Tablet
 *    - Accessories
 * 5. Select: Tần suất mua hàng
 *    - Hàng tuần
 *    - Hàng tháng
 *    - Hàng quý
 *    - Hàng năm
 * 6. Textarea: Góp ý (optional, max 500 chars)
 *
 * 🤔 PHÂN TÍCH:
 * Approach A: Validate tất cả cùng lúc khi submit
 * Pros: Code đơn giản, ít event handlers
 * Cons: User chỉ biết lỗi sau khi click submit
 *
 * Approach B: Validate từng field khi blur
 * Pros: UX tốt hơn, realtime feedback
 * Cons: ⚠️ Cần state để lưu errors (chưa học!)
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 * - Với kiến thức hiện tại (không có state), chọn Approach A
 * - Document: "Approach A được chọn vì chưa học state management"
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
// Implement form theo requirements ở trên

✅ Self-Check:

  • [ ] Có đủ các loại input (text, email, radio, checkbox, select, textarea)?
  • [ ] Radio buttons dùng cùng name để chỉ chọn 1?
  • [ ] Checkboxes dùng getAll() để lấy nhiều giá trị?
  • [ ] Select có default value rỗng + disabled?
  • [ ] Validation kiểm tra ít nhất 1 checkbox được chọn?
  • [ ] Textarea có maxLength?
💡 Solution
jsx
/**
 * Survey Form với Multiple Input Types - Uncontrolled pattern
 * Xử lý nhiều loại input khác nhau và validate khi submit
 * Approach A được chọn vì chưa học state management
 */
function SurveyForm() {
  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);

    // Extract data
    const data = {
      name: formData.get('name')?.trim() || '',
      email: formData.get('email')?.trim() || '',
      ageGroup: formData.get('ageGroup'),
      interests: formData.getAll('interests'),
      frequency: formData.get('frequency'),
      feedback: formData.get('feedback')?.trim() || '',
    };

    // Validation
    const errors = [];

    // 1. Tên người dùng
    if (!data.name || data.name.length < 3) {
      errors.push('Tên phải ít nhất 3 ký tự');
    }

    // 2. Email
    if (!data.email) {
      errors.push('Email không được để trống');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
      errors.push('Email không hợp lệ');
    }

    // 3. Độ tuổi
    if (!data.ageGroup) {
      errors.push('Vui lòng chọn độ tuổi');
    }

    // 4. Sản phẩm quan tâm (ít nhất 1)
    if (data.interests.length === 0) {
      errors.push('Vui lòng chọn ít nhất 1 sản phẩm quan tâm');
    }

    // 5. Tần suất mua hàng
    if (!data.frequency) {
      errors.push('Vui lòng chọn tần suất mua hàng');
    }

    // 6. Góp ý (optional, max 500 ký tự)
    if (data.feedback && data.feedback.length > 500) {
      errors.push('Góp ý không được quá 500 ký tự');
    }

    // Hiển thị lỗi nếu có
    if (errors.length > 0) {
      alert('Lỗi:\n\n' + errors.join('\n'));
      return;
    }

    // Thành công
    console.log('✅ Survey submitted:', data);

    // Reset form
    event.target.reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 1. Tên người dùng */}
      <div>
        <label htmlFor='name'>Tên của bạn: *</label>
        <input
          type='text'
          id='name'
          name='name'
          placeholder='Nguyễn Văn A'
          required
        />
      </div>

      {/* 2. Email */}
      <div>
        <label htmlFor='email'>Email: *</label>
        <input
          type='email'
          id='email'
          name='email'
          placeholder='email@example.com'
          required
        />
      </div>

      {/* 3. Radio buttons - Độ tuổi */}
      <fieldset>
        <legend>Độ tuổi: *</legend>
        <label>
          <input
            type='radio'
            name='ageGroup'
            value='18-25'
            required
          />{' '}
          18-25
        </label>
        <label>
          <input
            type='radio'
            name='ageGroup'
            value='26-35'
          />{' '}
          26-35
        </label>
        <label>
          <input
            type='radio'
            name='ageGroup'
            value='36-50'
          />{' '}
          36-50
        </label>
        <label>
          <input
            type='radio'
            name='ageGroup'
            value='50+'
          />{' '}
          50+
        </label>
      </fieldset>

      {/* 4. Checkboxes - Sản phẩm quan tâm */}
      <fieldset>
        <legend>Sản phẩm quan tâm: * (chọn ít nhất 1)</legend>
        <label>
          <input
            type='checkbox'
            name='interests'
            value='laptop'
          />{' '}
          Laptop
        </label>
        <label>
          <input
            type='checkbox'
            name='interests'
            value='smartphone'
          />{' '}
          Smartphone
        </label>
        <label>
          <input
            type='checkbox'
            name='interests'
            value='tablet'
          />{' '}
          Tablet
        </label>
        <label>
          <input
            type='checkbox'
            name='interests'
            value='accessories'
          />{' '}
          Accessories
        </label>
      </fieldset>

      {/* 5. Select - Tần suất mua hàng */}
      <div>
        <label htmlFor='frequency'>Tần suất mua hàng: *</label>
        <select
          id='frequency'
          name='frequency'
          defaultValue=''
          required
        >
          <option
            value=''
            disabled
          >
            -- Chọn tần suất --
          </option>
          <option value='weekly'>Hàng tuần</option>
          <option value='monthly'>Hàng tháng</option>
          <option value='quarterly'>Hàng quý</option>
          <option value='yearly'>Hàng năm</option>
        </select>
      </div>

      {/* 6. Textarea - Góp ý */}
      <div>
        <label htmlFor='feedback'>Góp ý thêm:</label>
        <textarea
          id='feedback'
          name='feedback'
          rows={4}
          maxLength={500}
          placeholder='Chia sẻ ý kiến của bạn... (tối đa 500 ký tự)'
        />
      </div>

      <button type='submit'>Gửi khảo sát</button>
    </form>
  );
}

/* Ví dụ kết quả console khi submit thành công:
✅ Survey submitted: {
  name: "Nguyễn Văn A",
  email: "vana@example.com",
  ageGroup: "26-35",
  interests: ["laptop", "smartphone"],
  frequency: "monthly",
  feedback: "Sản phẩm rất tốt, giao hàng nhanh"
}
*/

⭐⭐⭐ Exercise 3: Dynamic Shipping Form (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Form với conditional fields
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là khách hàng, tôi muốn nhập địa chỉ giao hàng,
 *              với option địa chỉ thanh toán khác địa chỉ giao hàng"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Form có shipping address (họ tên, địa chỉ, thành phố, zip)
 * - [ ] Checkbox "Địa chỉ thanh toán khác địa chỉ giao hàng"
 * - [ ] Khi check: Hiển thị billing address form
 * - [ ] Khi uncheck: Ẩn billing form, dùng shipping address
 * - [ ] Validate tất cả required fields
 * - [ ] Console.log kết quả khi submit
 *
 * 🎨 Technical Constraints:
 * - KHÔNG dùng state (chưa học)
 * - Dùng CSS để show/hide billing form
 * - Validate cả shipping và billing addresses
 *
 * 🚨 Edge Cases cần handle:
 * - User check/uncheck checkbox nhiều lần
 * - Submit khi billing form đang ẩn
 * - Validate fields trong form đang ẩn
 *
 * 📝 Implementation Checklist:
 * - [ ] Shipping address form (4 fields)
 * - [ ] Billing checkbox với onChange handler
 * - [ ] Billing address form (initially hidden)
 * - [ ] CSS class để show/hide
 * - [ ] Validation logic
 * - [ ] Submit handler tổng hợp data
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
// Implement theo requirements

🎯 Challenges:

  1. Show/hide billing section WITHOUT state
  2. Clear billing fields khi ẩn
  3. Validate conditional fields
  4. Handle edge cases (check/uncheck nhiều lần)
💡 Solution
jsx
/**
 * Dynamic Shipping Form - Uncontrolled + DOM manipulation
 * Hiển thị/ẩn billing address dựa trên checkbox
 * Validate conditional fields mà không dùng state
 */
function ShippingForm() {
  const handleBillingToggle = (event) => {
    const billingSection = document.getElementById('billing-section');
    if (!billingSection) return;

    if (event.target.checked) {
      billingSection.style.display = 'block';
    } else {
      billingSection.style.display = 'none';
      // Xóa dữ liệu billing khi ẩn để tránh gửi nhầm dữ liệu cũ
      const inputs = billingSection.querySelectorAll('input');
      inputs.forEach((input) => {
        input.value = '';
      });
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);

    // Lấy dữ liệu shipping
    const shipping = {
      name: (formData.get('shipping_name') || '').trim(),
      address: (formData.get('shipping_address') || '').trim(),
      city: (formData.get('shipping_city') || '').trim(),
      zip: (formData.get('shipping_zip') || '').trim(),
    };

    // Kiểm tra xem có dùng billing riêng không
    const useSeparateBilling = !!formData.get('separate_billing');

    // Lấy dữ liệu billing (nếu có)
    let billing = shipping; // mặc định dùng shipping

    if (useSeparateBilling) {
      billing = {
        name: (formData.get('billing_name') || '').trim(),
        address: (formData.get('billing_address') || '').trim(),
        city: (formData.get('billing_city') || '').trim(),
        zip: (formData.get('billing_zip') || '').trim(),
      };
    }

    // Validation
    const errors = [];

    // Shipping luôn bắt buộc
    if (!shipping.name) errors.push('Shipping: Họ tên không được để trống');
    if (!shipping.address) errors.push('Shipping: Địa chỉ không được để trống');
    if (!shipping.city) errors.push('Shipping: Thành phố không được để trống');
    if (!shipping.zip) errors.push('Shipping: Mã ZIP không được để trống');

    // Billing chỉ validate khi được chọn (và hiển thị)
    if (useSeparateBilling) {
      if (!billing.name) errors.push('Billing: Họ tên không được để trống');
      if (!billing.address) errors.push('Billing: Địa chỉ không được để trống');
      if (!billing.city) errors.push('Billing: Thành phố không được để trống');
      if (!billing.zip) errors.push('Billing: Mã ZIP không được để trống');
    }

    if (errors.length > 0) {
      alert('Lỗi:\n\n' + errors.join('\n'));
      return;
    }

    // Thành công
    console.log('✅ Order submitted:', {
      shipping,
      billing,
      useSeparateBilling,
    });

    // Reset form (tùy chọn)
    event.target.reset();

    // Đảm bảo billing section ẩn lại sau reset
    const billingSection = document.getElementById('billing-section');
    if (billingSection) {
      billingSection.style.display = 'none';
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h3>Địa chỉ giao hàng</h3>

      <div>
        <label htmlFor='shipping_name'>Họ tên: *</label>
        <input
          type='text'
          id='shipping_name'
          name='shipping_name'
          required
        />
      </div>

      <div>
        <label htmlFor='shipping_address'>Địa chỉ: *</label>
        <input
          type='text'
          id='shipping_address'
          name='shipping_address'
          required
        />
      </div>

      <div>
        <label htmlFor='shipping_city'>Thành phố: *</label>
        <input
          type='text'
          id='shipping_city'
          name='shipping_city'
          required
        />
      </div>

      <div>
        <label htmlFor='shipping_zip'>Mã ZIP: *</label>
        <input
          type='text'
          id='shipping_zip'
          name='shipping_zip'
          pattern='[0-9]{5}'
          placeholder='Ví dụ: 700000'
          title='Mã ZIP gồm 5 chữ số'
          required
        />
      </div>

      <hr />

      <div>
        <label>
          <input
            type='checkbox'
            name='separate_billing'
            onChange={handleBillingToggle}
          />{' '}
          Địa chỉ thanh toán khác địa chỉ giao hàng
        </label>
      </div>

      {/* Billing section - ban đầu ẩn */}
      <div
        id='billing-section'
        style={{ display: 'none' }}
      >
        <h3>Địa chỉ thanh toán</h3>

        <div>
          <label htmlFor='billing_name'>Họ tên: *</label>
          <input
            type='text'
            id='billing_name'
            name='billing_name'
          />
        </div>

        <div>
          <label htmlFor='billing_address'>Địa chỉ: *</label>
          <input
            type='text'
            id='billing_address'
            name='billing_address'
          />
        </div>

        <div>
          <label htmlFor='billing_city'>Thành phố: *</label>
          <input
            type='text'
            id='billing_city'
            name='billing_city'
          />
        </div>

        <div>
          <label htmlFor='billing_zip'>Mã ZIP: *</label>
          <input
            type='text'
            id='billing_zip'
            name='billing_zip'
            pattern='[0-9]{5}'
            placeholder='Ví dụ: 700000'
            title='Mã ZIP gồm 5 chữ số'
          />
        </div>
      </div>

      <button type='submit'>Đặt hàng</button>
    </form>
  );
}

/* Ví dụ kết quả console khi submit thành công (billing khác shipping):

✅ Order submitted: {
  shipping: {
    name: "Nguyễn Văn A",
    address: "123 Đường Láng",
    city: "Hà Nội",
    zip: "100000"
  },
  billing: {
    name: "Công ty XYZ",
    address: "456 Nguyễn Huệ",
    city: "TP.HCM",
    zip: "700000"
  },
  useSeparateBilling: true
}

Ví dụ khi không check checkbox (dùng chung địa chỉ):

✅ Order submitted: {
  shipping: { ... },
  billing: { ...same as shipping... },
  useSeparateBilling: false
}
*/

⭐⭐⭐⭐ Exercise 4: Multi-Step Form Architecture (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Thiết kế form nhiều bước mà không dùng state
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Nhiệm vụ:
 * 1. So sánh ít nhất 3 approaches để làm multi-step form
 * 2. Document pros/cons mỗi approach
 * 3. Chọn approach phù hợp nhất (không cần state)
 * 4. Viết ADR (Architecture Decision Record)
 *
 * Approach Options:
 * A. Hidden sections với CSS (show/hide)
 * B. Multiple <form> elements
 * C. Single form với navigation buttons
 * D. URL-based steps (query params) - Advanced!
 *
 * ADR Template:
 * - Context: Multi-step registration form (3 steps)
 * - Decision: Approach đã chọn
 * - Rationale: Tại sao chọn approach này
 * - Consequences: Trade-offs accepted
 * - Alternatives Considered: Các options khác
 */

/**
 * 💻 PHASE 2: Implementation (30 phút)
 *
 * Requirements:
 * Step 1: Account Info
 * - Username (min 3 chars)
 * - Email (valid format)
 * - Password (min 8 chars)
 *
 * Step 2: Personal Info
 * - Full Name (min 3 chars)
 * - Date of Birth (18+ years old)
 * - Phone Number (10 digits)
 *
 * Step 3: Preferences
 * - Newsletter (checkbox)
 * - Interests (checkboxes - min 1)
 * - Bio (textarea, max 200 chars)
 *
 * Features:
 * - Validate each step before proceeding
 * - Show current step indicator (1/3, 2/3, 3/3)
 * - Back button (except Step 1)
 * - Next button (Steps 1-2)
 * - Submit button (Step 3)
 * - Preserve data khi navigate steps
 */

// Example Solution using Hidden Sections
function MultiStepForm() {
  // Step data stored in form fields (no state needed!)

  const showStep = (stepNumber) => {
    // Hide all steps
    document.querySelectorAll('.form-step').forEach((step) => {
      step.style.display = 'none';
    });

    // Show target step
    document.getElementById(`step-${stepNumber}`).style.display = 'block';

    // Update indicator
    document.getElementById('step-indicator').textContent =
      `Bước ${stepNumber}/3`;
  };

  const validateStep1 = () => {
    const username = document.getElementById('username').value.trim();
    const email = document.getElementById('email').value.trim();
    const password = document.getElementById('password').value;

    const errors = [];

    if (username.length < 3) {
      errors.push('Username phải ít nhất 3 ký tự');
    }

    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.push('Email không hợp lệ');
    }

    if (password.length < 8) {
      errors.push('Mật khẩu phải ít nhất 8 ký tự');
    }

    return errors;
  };

  const validateStep2 = () => {
    const fullName = document.getElementById('fullName').value.trim();
    const dob = document.getElementById('dob').value;
    const phone = document.getElementById('phone').value;

    const errors = [];

    if (fullName.length < 3) {
      errors.push('Họ tên phải ít nhất 3 ký tự');
    }

    // Check age >= 18
    const birthDate = new Date(dob);
    const today = new Date();
    const age = today.getFullYear() - birthDate.getFullYear();
    if (age < 18) {
      errors.push('Bạn phải từ 18 tuổi trở lên');
    }

    if (!/^[0-9]{10}$/.test(phone)) {
      errors.push('Số điện thoại phải là 10 chữ số');
    }

    return errors;
  };

  const handleNext1 = () => {
    const errors = validateStep1();

    if (errors.length > 0) {
      alert('Lỗi:\n' + errors.join('\n'));
      return;
    }

    showStep(2);
  };

  const handleNext2 = () => {
    const errors = validateStep2();

    if (errors.length > 0) {
      alert('Lỗi:\n' + errors.join('\n'));
      return;
    }

    showStep(3);
  };

  const handleBack2 = () => showStep(1);
  const handleBack3 = () => showStep(2);

  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);

    // Validate Step 3
    const interests = formData.getAll('interests');
    if (interests.length === 0) {
      alert('Vui lòng chọn ít nhất 1 sở thích');
      return;
    }

    // Collect all data
    const data = {
      username: formData.get('username'),
      email: formData.get('email'),
      password: formData.get('password'),
      fullName: formData.get('fullName'),
      dob: formData.get('dob'),
      phone: formData.get('phone'),
      newsletter: !!formData.get('newsletter'),
      interests: interests,
      bio: formData.get('bio'),
    };

    console.log('✅ Registration complete:', data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div
        id='step-indicator'
        style={{ fontWeight: 'bold', marginBottom: '20px' }}
      >
        Bước 1/3
      </div>

      {/* Step 1: Account Info */}
      <div
        id='step-1'
        className='form-step'
      >
        <h3>Thông tin tài khoản</h3>

        <div>
          <label htmlFor='username'>Username:</label>
          <input
            type='text'
            id='username'
            name='username'
            required
          />
        </div>

        <div>
          <label htmlFor='email'>Email:</label>
          <input
            type='email'
            id='email'
            name='email'
            required
          />
        </div>

        <div>
          <label htmlFor='password'>Mật khẩu:</label>
          <input
            type='password'
            id='password'
            name='password'
            required
          />
        </div>

        <button
          type='button'
          onClick={handleNext1}
        >
          Tiếp theo →
        </button>
      </div>

      {/* Step 2: Personal Info */}
      <div
        id='step-2'
        className='form-step'
        style={{ display: 'none' }}
      >
        <h3>Thông tin cá nhân</h3>

        <div>
          <label htmlFor='fullName'>Họ tên:</label>
          <input
            type='text'
            id='fullName'
            name='fullName'
            required
          />
        </div>

        <div>
          <label htmlFor='dob'>Ngày sinh:</label>
          <input
            type='date'
            id='dob'
            name='dob'
            required
          />
        </div>

        <div>
          <label htmlFor='phone'>Số điện thoại:</label>
          <input
            type='tel'
            id='phone'
            name='phone'
            pattern='[0-9]{10}'
            required
          />
        </div>

        <button
          type='button'
          onClick={handleBack2}
        >
          ← Quay lại
        </button>
        <button
          type='button'
          onClick={handleNext2}
        >
          Tiếp theo →
        </button>
      </div>

      {/* Step 3: Preferences */}
      <div
        id='step-3'
        className='form-step'
        style={{ display: 'none' }}
      >
        <h3>Sở thích</h3>

        <div>
          <label>
            <input
              type='checkbox'
              name='newsletter'
            />{' '}
            Nhận bản tin
          </label>
        </div>

        <fieldset>
          <legend>Sở thích: *</legend>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='tech'
            />{' '}
            Công nghệ
          </label>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='sports'
            />{' '}
            Thể thao
          </label>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='music'
            />{' '}
            Âm nhạc
          </label>
        </fieldset>

        <div>
          <label htmlFor='bio'>Giới thiệu bản thân:</label>
          <textarea
            id='bio'
            name='bio'
            rows={4}
            maxLength={200}
          />
        </div>

        <button
          type='button'
          onClick={handleBack3}
        >
          ← Quay lại
        </button>
        <button type='submit'>Hoàn tất đăng ký</button>
      </div>
    </form>
  );
}

// 🎯 NHIỆM VỤ CỦA BẠN:
/**
 * 1. Viết ADR document (20 phút)
 * 2. Implement solution (30 phút)
 * 3. Test all flows (10 phút):
 *    - Next/Back navigation
 *    - Validation mỗi step
 *    - Data preservation
 *    - Final submit
 */
💡 Solution
jsx
/**
 * Multi-Step Registration Form - Uncontrolled + DOM manipulation
 * Sử dụng approach Hidden sections với CSS show/hide
 * Không dùng state, dữ liệu được bảo toàn trong các input fields
 */

/*
 * ADR (Architecture Decision Record)
 *
 * Context:
 *   Xây dựng multi-step registration form với 3 bước:
 *   - Step 1: Account Info
 *   - Step 2: Personal Info
 *   - Step 3: Preferences
 *   Yêu cầu: Không được dùng state (useState/useReducer), cần validate từng bước,
 *   giữ dữ liệu khi chuyển bước, có nút Back/Next/Submit và indicator.
 *
 * Decision:
 *   Sử dụng Approach A - Hidden sections với CSS (show/hide)
 *
 * Rationale:
 *   - Đơn giản nhất trong các lựa chọn khi không dùng state
 *   - Dữ liệu tự động được bảo toàn trong DOM (input values)
 *   - Dễ validate từng bước bằng cách query DOM elements
 *   - Không cần quản lý routing hay nhiều form
 *   - Có thể điều khiển hiển thị hoàn toàn bằng JavaScript + CSS
 *
 * Consequences / Trade-offs accepted:
 *   - Phải dùng document.getElementById / querySelector → không "React way"
 *   - Code validation hơi dài dòng vì phải query từng field
 *   - Không tận dụng được React re-rendering cho UI updates
 *   - Khó scale nếu có nhiều bước hoặc logic phức tạp hơn
 *
 * Alternatives Considered:
 *   B. Multiple <form> elements
 *      → Không khả thi vì khó giữ dữ liệu giữa các form riêng biệt
 *   C. Single form với navigation buttons (đã chọn)
 *      → Đây chính là biến thể của A, được triển khai
 *   D. URL-based steps (query params)
 *      → Quá phức tạp, cần xử lý history, không cần thiết cho bài tập
 */

function MultiStepForm() {
  // Hiển thị bước cụ thể và cập nhật indicator
  const showStep = (stepNumber) => {
    document.querySelectorAll('.form-step').forEach((step) => {
      step.style.display = 'none';
    });

    const targetStep = document.getElementById(`step-${stepNumber}`);
    if (targetStep) {
      targetStep.style.display = 'block';
    }

    const indicator = document.getElementById('step-indicator');
    if (indicator) {
      indicator.textContent = `Bước ${stepNumber}/3`;
    }
  };

  // Validate Step 1: Account Info
  const validateStep1 = () => {
    const username = document.getElementById('username')?.value.trim() || '';
    const email = document.getElementById('email')?.value.trim() || '';
    const password = document.getElementById('password')?.value || '';

    const errors = [];

    if (username.length < 3) {
      errors.push('Username phải ít nhất 3 ký tự');
    }

    if (!email) {
      errors.push('Email không được để trống');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      errors.push('Email không hợp lệ');
    }

    if (password.length < 8) {
      errors.push('Mật khẩu phải ít nhất 8 ký tự');
    }

    return errors;
  };

  // Validate Step 2: Personal Info
  const validateStep2 = () => {
    const fullName = document.getElementById('fullName')?.value.trim() || '';
    const dob = document.getElementById('dob')?.value || '';
    const phone = document.getElementById('phone')?.value || '';

    const errors = [];

    if (fullName.length < 3) {
      errors.push('Họ tên phải ít nhất 3 ký tự');
    }

    if (dob) {
      const birthDate = new Date(dob);
      const today = new Date();
      let age = today.getFullYear() - birthDate.getFullYear();
      const m = today.getMonth() - birthDate.getMonth();
      if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
        age--;
      }
      if (age < 18) {
        errors.push('Bạn phải từ 18 tuổi trở lên');
      }
    } else {
      errors.push('Vui lòng nhập ngày sinh');
    }

    if (!/^[0-9]{10}$/.test(phone)) {
      errors.push('Số điện thoại phải đúng 10 chữ số');
    }

    return errors;
  };

  // Next từ Step 1
  const handleNext1 = () => {
    const errors = validateStep1();
    if (errors.length > 0) {
      alert('Lỗi ở bước 1:\n' + errors.join('\n'));
      return;
    }
    showStep(2);
  };

  // Next từ Step 2
  const handleNext2 = () => {
    const errors = validateStep2();
    if (errors.length > 0) {
      alert('Lỗi ở bước 2:\n' + errors.join('\n'));
      return;
    }
    showStep(3);
  };

  // Back buttons
  const handleBack2 = () => showStep(1);
  const handleBack3 = () => showStep(2);

  // Final submit - validate step 3
  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);
    const interests = formData.getAll('interests');

    if (interests.length === 0) {
      alert('Vui lòng chọn ít nhất 1 sở thích ở bước 3');
      return;
    }

    // Thu thập toàn bộ dữ liệu
    const data = {
      username: formData.get('username'),
      email: formData.get('email'),
      password: formData.get('password'),
      fullName: formData.get('fullName'),
      dob: formData.get('dob'),
      phone: formData.get('phone'),
      newsletter: !!formData.get('newsletter'),
      interests: interests,
      bio: formData.get('bio') || '',
    };

    console.log('✅ Registration complete:', data);

    // Optional: reset form sau khi thành công
    // event.target.reset();
    // showStep(1);
  };

  // Khởi tạo: hiển thị bước 1
  // (thường đặt trong useEffect, nhưng ở đây gọi trực tiếp)
  // Vì là uncontrolled, ta để JSX render trước rồi mới gọi showStep(1)
  // Nhưng trong React, tốt nhất nên gọi trong useEffect → ở đây ta giả lập bằng setTimeout ngắn
  setTimeout(() => {
    showStep(1);
  }, 0);

  return (
    <form onSubmit={handleSubmit}>
      <div
        id='step-indicator'
        style={{ fontWeight: 'bold', marginBottom: '24px', fontSize: '1.2rem' }}
      >
        Bước 1/3
      </div>

      {/* Step 1 */}
      <div
        id='step-1'
        className='form-step'
      >
        <h3>Thông tin tài khoản</h3>

        <div>
          <label htmlFor='username'>Username:</label>
          <input
            type='text'
            id='username'
            name='username'
            required
          />
        </div>

        <div>
          <label htmlFor='email'>Email:</label>
          <input
            type='email'
            id='email'
            name='email'
            required
          />
        </div>

        <div>
          <label htmlFor='password'>Mật khẩu:</label>
          <input
            type='password'
            id='password'
            name='password'
            required
          />
        </div>

        <div style={{ marginTop: '20px' }}>
          <button
            type='button'
            onClick={handleNext1}
          >
            Tiếp theo →
          </button>
        </div>
      </div>

      {/* Step 2 */}
      <div
        id='step-2'
        className='form-step'
        style={{ display: 'none' }}
      >
        <h3>Thông tin cá nhân</h3>

        <div>
          <label htmlFor='fullName'>Họ tên:</label>
          <input
            type='text'
            id='fullName'
            name='fullName'
            required
          />
        </div>

        <div>
          <label htmlFor='dob'>Ngày sinh:</label>
          <input
            type='date'
            id='dob'
            name='dob'
            required
          />
        </div>

        <div>
          <label htmlFor='phone'>Số điện thoại:</label>
          <input
            type='tel'
            id='phone'
            name='phone'
            pattern='[0-9]{10}'
            title='Số điện thoại phải có đúng 10 chữ số'
            required
          />
        </div>

        <div style={{ marginTop: '20px' }}>
          <button
            type='button'
            onClick={handleBack2}
          >
            ← Quay lại
          </button>
          <button
            type='button'
            onClick={handleNext2}
          >
            Tiếp theo →
          </button>
        </div>
      </div>

      {/* Step 3 */}
      <div
        id='step-3'
        className='form-step'
        style={{ display: 'none' }}
      >
        <h3>Sở thích</h3>

        <div>
          <label>
            <input
              type='checkbox'
              name='newsletter'
            />
            Nhận bản tin
          </label>
        </div>

        <fieldset>
          <legend>Sở thích: *</legend>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='tech'
            />
            Công nghệ
          </label>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='sports'
            />
            Thể thao
          </label>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='music'
            />
            Âm nhạc
          </label>
          <label>
            <input
              type='checkbox'
              name='interests'
              value='travel'
            />
            Du lịch
          </label>
        </fieldset>

        <div>
          <label htmlFor='bio'>Giới thiệu bản thân:</label>
          <textarea
            id='bio'
            name='bio'
            rows={4}
            maxLength={200}
            placeholder='Tối đa 200 ký tự'
          />
        </div>

        <div style={{ marginTop: '20px' }}>
          <button
            type='button'
            onClick={handleBack3}
          >
            ← Quay lại
          </button>
          <button type='submit'>Hoàn tất đăng ký</button>
        </div>
      </div>
    </form>
  );
}

/* Ví dụ kết quả console khi submit thành công:
✅ Registration complete: {
  username: "nguyenvana",
  email: "vana@example.com",
  password: "MatKhau123",
  fullName: "Nguyễn Văn A",
  dob: "1995-05-20",
  phone: "0912345678",
  newsletter: true,
  interests: ["tech", "music"],
  bio: "Yêu thích công nghệ và âm nhạc hiện đại."
}
*/

⭐⭐⭐⭐⭐ Exercise 5: Production-Ready Contact Form (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Form đạt chuẩn production
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Tạo contact form cho enterprise website với các yêu cầu:
 *
 * Functional Requirements:
 * - Name, Email, Phone, Subject, Message fields
 * - Department dropdown (Sales, Support, HR, Other)
 * - Priority radio (Low, Medium, High, Urgent)
 * - File attachment (optional, max 5MB, pdf/doc/docx only)
 * - Privacy policy checkbox (required)
 * - Form submission với validation đầy đủ
 * - Success/Error feedback
 * - Form reset sau submit thành công
 *
 * Non-Functional Requirements:
 * - Accessibility (WCAG 2.1 Level AA)
 * - Responsive design
 * - Loading state during submission
 * - Error messages inline & summary
 * - Client-side validation
 * - Security considerations
 *
 * 🏗️ Technical Design Doc:
 *
 * 1. Component Architecture
 *    - Single ContactForm component
 *    - Helper validation functions
 *    - Error display component (inline)
 *
 * 2. State Management Strategy
 *    - NO STATE (uncontrolled form)
 *    - DOM manipulation for error display
 *    - FormData API for value extraction
 *
 * 3. API Integration Points
 *    - Mock API endpoint (console.log)
 *    - Timeout để simulate network delay
 *
 * 4. Performance Considerations
 *    - Debounce file validation
 *    - Lazy load file reader
 *
 * 5. Error Handling Strategy
 *    - Inline errors per field
 *    - Error summary at top
 *    - Focus management
 *
 * ✅ Production Checklist:
 * - [ ] All inputs have labels (for screen readers)
 * - [ ] Required fields marked with *
 * - [ ] ARIA attributes (aria-required, aria-invalid)
 * - [ ] Error messages with role="alert"
 * - [ ] Keyboard navigation works
 * - [ ] Form submits with Enter key
 * - [ ] Loading state prevents double submit
 * - [ ] File upload validation (size, type)
 * - [ ] XSS prevention (sanitize input)
 * - [ ] Responsive layout (mobile-first)
 * - [ ] High contrast mode support
 * - [ ] Focus visible indicators
 *
 * 📝 Documentation:
 * - Inline comments giải thích logic
 * - JSDoc cho functions
 * - Usage examples
 */

/**
 * File upload validator
 * @param {File} file
 * @returns {string|null} Error message hoặc null nếu valid
 */
function validateFile(file) {
  if (!file) return null;

  const MAX_SIZE = 5 * 1024 * 1024; // 5MB
  const ALLOWED_TYPES = [
    'application/pdf',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  ];

  if (file.size > MAX_SIZE) {
    return `File quá lớn. Tối đa 5MB (file của bạn: ${(file.size / 1024 / 1024).toFixed(2)}MB)`;
  }

  if (!ALLOWED_TYPES.includes(file.type)) {
    return 'Chỉ chấp nhận file PDF, DOC, DOCX';
  }

  return null;
}

/**
 * Sanitize user input để prevent XSS
 * @param {string} str
 * @returns {string}
 */
function sanitizeInput(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

function ProductionContactForm() {
  // Track submission state
  let isSubmitting = false;

  /**
   * Display error message for specific field
   */
  const showFieldError = (fieldName, message) => {
    const errorId = `${fieldName}-error`;
    let errorEl = document.getElementById(errorId);

    if (!errorEl) {
      errorEl = document.createElement('div');
      errorEl.id = errorId;
      errorEl.className = 'field-error';
      errorEl.setAttribute('role', 'alert');

      const field = document.getElementById(fieldName);
      field.parentNode.appendChild(errorEl);
    }

    errorEl.textContent = message;
    errorEl.style.display = 'block';

    // Mark field as invalid for screen readers
    const field = document.getElementById(fieldName);
    field.setAttribute('aria-invalid', 'true');
  };

  /**
   * Clear all error messages
   */
  const clearErrors = () => {
    document.querySelectorAll('.field-error').forEach((el) => {
      el.style.display = 'none';
    });

    document.querySelectorAll('[aria-invalid]').forEach((el) => {
      el.removeAttribute('aria-invalid');
    });

    const summary = document.getElementById('error-summary');
    if (summary) {
      summary.style.display = 'none';
    }
  };

  /**
   * Display error summary
   */
  const showErrorSummary = (errors) => {
    let summary = document.getElementById('error-summary');

    if (!summary) {
      summary = document.createElement('div');
      summary.id = 'error-summary';
      summary.className = 'error-summary';
      summary.setAttribute('role', 'alert');
      summary.setAttribute('aria-live', 'assertive');

      const form = document.getElementById('contact-form');
      form.insertBefore(summary, form.firstChild);
    }

    summary.innerHTML = `
      <h3>Vui lòng sửa các lỗi sau:</h3>
      <ul>
        ${errors.map((err) => `<li>${sanitizeInput(err)}</li>`).join('')}
      </ul>
    `;
    summary.style.display = 'block';

    // Focus vào summary để screen reader đọc
    summary.focus();
  };

  /**
   * Validate all form fields
   */
  const validateForm = (formData, fileInput) => {
    const errors = [];

    // Extract data
    const data = {
      name: formData.get('name')?.trim(),
      email: formData.get('email')?.trim(),
      phone: formData.get('phone')?.trim(),
      department: formData.get('department'),
      priority: formData.get('priority'),
      subject: formData.get('subject')?.trim(),
      message: formData.get('message')?.trim(),
      privacy: formData.get('privacy'),
      file: fileInput.files[0],
    };

    // Clear previous errors
    clearErrors();

    // Validate name
    if (!data.name || data.name.length < 2) {
      errors.push('Tên phải ít nhất 2 ký tự');
      showFieldError('name', 'Tên phải ít nhất 2 ký tự');
    }

    // Validate email
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!data.email || !emailRegex.test(data.email)) {
      errors.push('Email không hợp lệ');
      showFieldError('email', 'Email không hợp lệ');
    }

    // Validate phone (optional but if provided must be valid)
    if (data.phone) {
      const phoneRegex = /^[0-9]{10,11}$/;
      if (!phoneRegex.test(data.phone)) {
        errors.push('Số điện thoại phải là 10-11 chữ số');
        showFieldError('phone', 'Số điện thoại phải là 10-11 chữ số');
      }
    }

    // Validate department
    if (!data.department) {
      errors.push('Vui lòng chọn phòng ban');
      showFieldError('department', 'Vui lòng chọn phòng ban');
    }

    // Validate priority
    if (!data.priority) {
      errors.push('Vui lòng chọn mức độ ưu tiên');
    }

    // Validate subject
    if (!data.subject || data.subject.length < 5) {
      errors.push('Tiêu đề phải ít nhất 5 ký tự');
      showFieldError('subject', 'Tiêu đề phải ít nhất 5 ký tự');
    }

    // Validate message
    if (!data.message || data.message.length < 20) {
      errors.push('Nội dung phải ít nhất 20 ký tự');
      showFieldError('message', 'Nội dung phải ít nhất 20 ký tự');
    }

    // Validate privacy
    if (!data.privacy) {
      errors.push('Bạn phải đồng ý với chính sách bảo mật');
      showFieldError('privacy', 'Bạn phải đồng ý với chính sách bảo mật');
    }

    // Validate file
    const fileError = validateFile(data.file);
    if (fileError) {
      errors.push(fileError);
      showFieldError('attachment', fileError);
    }

    return { valid: errors.length === 0, errors, data };
  };

  /**
   * Simulate API call
   */
  const submitToAPI = async (data) => {
    // Simulate network delay
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Simulate random success/failure (90% success)
    if (Math.random() > 0.1) {
      return { success: true };
    } else {
      throw new Error('Server error. Please try again.');
    }
  };

  /**
   * Handle form submission
   */
  const handleSubmit = async (event) => {
    event.preventDefault();

    // Prevent double submit
    if (isSubmitting) {
      return;
    }

    const form = event.target;
    const formData = new FormData(form);
    const fileInput = document.getElementById('attachment');

    // Validate
    const { valid, errors, data } = validateForm(formData, fileInput);

    if (!valid) {
      showErrorSummary(errors);

      // Focus first error field
      const firstErrorField = document.querySelector('[aria-invalid="true"]');
      if (firstErrorField) {
        firstErrorField.focus();
      }

      return;
    }

    // Show loading state
    isSubmitting = true;
    const submitBtn = form.querySelector('button[type="submit"]');
    const originalText = submitBtn.textContent;
    submitBtn.textContent = 'Đang gửi...';
    submitBtn.disabled = true;

    try {
      // Submit to API
      await submitToAPI(data);

      // Success feedback
      alert('✅ Gửi thành công! Chúng tôi sẽ liên hệ bạn sớm.');

      // Reset form
      form.reset();
      clearErrors();

      // Log success (in production: analytics tracking)
      console.log('✅ Contact form submitted:', {
        ...data,
        file: data.file
          ? {
              name: data.file.name,
              size: data.file.size,
              type: data.file.type,
            }
          : null,
      });
    } catch (error) {
      // Error feedback
      alert('❌ Có lỗi xảy ra. Vui lòng thử lại sau.');
      console.error('Submission error:', error);
    } finally {
      // Reset loading state
      isSubmitting = false;
      submitBtn.textContent = originalText;
      submitBtn.disabled = false;
    }
  };

  /**
   * Handle file input change
   */
  const handleFileChange = (event) => {
    const file = event.target.files[0];
    const error = validateFile(file);

    if (error) {
      showFieldError('attachment', error);
      event.target.value = ''; // Clear invalid file
    } else {
      clearErrors();
    }
  };

  return (
    <form
      id='contact-form'
      onSubmit={handleSubmit}
      noValidate // Use custom validation
      style={{
        maxWidth: '600px',
        margin: '0 auto',
        padding: '20px',
      }}
    >
      <h2>Liên hệ với chúng tôi</h2>
      <p>Điền thông tin bên dưới, chúng tôi sẽ liên hệ bạn sớm nhất.</p>

      {/* Name */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='name'>
          Họ tên:{' '}
          <span
            style={{ color: 'red' }}
            aria-label='required'
          >
            *
          </span>
        </label>
        <input
          type='text'
          id='name'
          name='name'
          aria-required='true'
          autoComplete='name'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Email */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='email'>
          Email:{' '}
          <span
            style={{ color: 'red' }}
            aria-label='required'
          >
            *
          </span>
        </label>
        <input
          type='email'
          id='email'
          name='email'
          aria-required='true'
          autoComplete='email'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Phone */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='phone'>Số điện thoại:</label>
        <input
          type='tel'
          id='phone'
          name='phone'
          autoComplete='tel'
          placeholder='0901234567'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Department */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='department'>
          Phòng ban:{' '}
          <span
            style={{ color: 'red' }}
            aria-label='required'
          >
            *
          </span>
        </label>
        <select
          id='department'
          name='department'
          aria-required='true'
          defaultValue=''
          style={{ width: '100%', padding: '8px' }}
        >
          <option
            value=''
            disabled
          >
            -- Chọn phòng ban --
          </option>
          <option value='sales'>Sales</option>
          <option value='support'>Support</option>
          <option value='hr'>HR</option>
          <option value='other'>Other</option>
        </select>
      </div>

      {/* Priority */}
      <fieldset
        style={{
          marginBottom: '20px',
          border: '1px solid #ccc',
          padding: '10px',
        }}
      >
        <legend>
          Mức độ ưu tiên:{' '}
          <span
            style={{ color: 'red' }}
            aria-label='required'
          >
            *
          </span>
        </legend>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          <input
            type='radio'
            name='priority'
            value='low'
          />{' '}
          Thấp
        </label>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          <input
            type='radio'
            name='priority'
            value='medium'
            defaultChecked
          />{' '}
          Trung bình
        </label>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          <input
            type='radio'
            name='priority'
            value='high'
          />{' '}
          Cao
        </label>
        <label style={{ display: 'block' }}>
          <input
            type='radio'
            name='priority'
            value='urgent'
          />{' '}
          Khẩn cấp
        </label>
      </fieldset>

      {/* Subject */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='subject'>
          Tiêu đề:{' '}
          <span
            style={{ color: 'red' }}
            aria-label='required'
          >
            *
          </span>
        </label>
        <input
          type='text'
          id='subject'
          name='subject'
          aria-required='true'
          placeholder='Tóm tắt nội dung liên hệ'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Message */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='message'>
          Nội dung:{' '}
          <span
            style={{ color: 'red' }}
            aria-label='required'
          >
            *
          </span>
        </label>
        <textarea
          id='message'
          name='message'
          aria-required='true'
          rows={6}
          placeholder='Mô tả chi tiết yêu cầu của bạn (ít nhất 20 ký tự)'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* File Attachment */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='attachment'>
          Đính kèm file: <small>(PDF, DOC, DOCX - Max 5MB)</small>
        </label>
        <input
          type='file'
          id='attachment'
          name='attachment'
          accept='.pdf,.doc,.docx'
          onChange={handleFileChange}
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Privacy Checkbox */}
      <div style={{ marginBottom: '20px' }}>
        <label>
          <input
            type='checkbox'
            id='privacy'
            name='privacy'
            aria-required='true'
          />{' '}
          <span>
            Tôi đồng ý với{' '}
            <a
              href='/privacy'
              target='_blank'
              rel='noopener noreferrer'
            >
              chính sách bảo mật
            </a>{' '}
            <span
              style={{ color: 'red' }}
              aria-label='required'
            >
              *
            </span>
          </span>
        </label>
      </div>

      {/* Submit Button */}
      <button
        type='submit'
        style={{
          backgroundColor: '#007bff',
          color: 'white',
          padding: '12px 24px',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          fontSize: '16px',
        }}
      >
        Gửi liên hệ
      </button>

      {/* CSS for error messages */}
      <style>{`
        .field-error {
          color: #d32f2f;
          font-size: 14px;
          margin-top: 4px;
          display: none;
        }
        
        .error-summary {
          background-color: #ffebee;
          border-left: 4px solid #d32f2f;
          padding: 16px;
          margin-bottom: 24px;
          display: none;
        }
        
        .error-summary h3 {
          color: #d32f2f;
          margin-top: 0;
        }
        
        .error-summary ul {
          margin: 8px 0 0 0;
          padding-left: 20px;
        }
        
        [aria-invalid="true"] {
          border-color: #d32f2f;
          border-width: 2px;
        }
        
        /* Focus styles for accessibility */
        input:focus,
        select:focus,
        textarea:focus,
        button:focus {
          outline: 2px solid #007bff;
          outline-offset: 2px;
        }
        
        /* High contrast mode support */
        @media (prefers-contrast: high) {
          input, select, textarea {
            border: 2px solid currentColor;
          }
        }
      `}</style>
    </form>
  );
}

// 🎯 NHIỆM VỤ CỦA BẠN:
/**
 * Implement production-ready contact form với:
 * - ✅ Full validation (client-side)
 * - ✅ Accessibility (WCAG 2.1 AA)
 * - ✅ Error handling (inline + summary)
 * - ✅ Loading states
 * - ✅ File upload validation
 * - ✅ Security (input sanitization)
 * - ✅ Responsive design
 * - ✅ Focus management
 * - ✅ Screen reader support
 */

🔍 Code Review Self-Checklist:

  • [ ] All labels have htmlFor attribute?
  • [ ] Required fields marked with aria-required="true"?
  • [ ] Error messages have role="alert"?
  • [ ] Focus moves to errors?
  • [ ] Keyboard navigation works?
  • [ ] File upload validated (size + type)?
  • [ ] Double submit prevented?
  • [ ] Input sanitized before display?
  • [ ] Loading state shown during submit?
  • [ ] Success/error feedback clear?
💡 Solution
jsx
/**
 * Production-Ready Contact Form (Uncontrolled + DOM manipulation)
 * Đạt chuẩn accessibility WCAG 2.1 AA, client-side validation,
 * error handling (inline + summary), loading state, file validation,
 * input sanitization, focus management, không sử dụng state
 */
function ProductionContactForm() {
  // Biến theo dõi trạng thái submit (không phải React state)
  let isSubmitting = false;

  /**
   * Hiển thị lỗi cho một field cụ thể
   * @param {string} fieldName - id của input
   * @param {string} message - thông báo lỗi
   */
  const showFieldError = (fieldName, message) => {
    const errorId = `${fieldName}-error`;
    let errorEl = document.getElementById(errorId);

    if (!errorEl) {
      errorEl = document.createElement('div');
      errorEl.id = errorId;
      errorEl.className = 'field-error';
      errorEl.setAttribute('role', 'alert');
      errorEl.setAttribute('aria-live', 'assertive');

      const field = document.getElementById(fieldName);
      if (field && field.parentNode) {
        field.parentNode.appendChild(errorEl);
      }
    }

    errorEl.textContent = message;
    errorEl.style.display = 'block';

    const field = document.getElementById(fieldName);
    if (field) {
      field.setAttribute('aria-invalid', 'true');
      field.setAttribute('aria-describedby', errorId);
    }
  };

  /**
   * Xóa toàn bộ lỗi hiển thị
   */
  const clearErrors = () => {
    document.querySelectorAll('.field-error').forEach((el) => {
      el.style.display = 'none';
      el.textContent = '';
    });

    document.querySelectorAll('[aria-invalid="true"]').forEach((el) => {
      el.removeAttribute('aria-invalid');
      el.removeAttribute('aria-describedby');
    });

    const summary = document.getElementById('error-summary');
    if (summary) {
      summary.style.display = 'none';
      summary.innerHTML = '';
    }
  };

  /**
   * Hiển thị tóm tắt lỗi ở đầu form
   * @param {string[]} errors
   */
  const showErrorSummary = (errors) => {
    let summary = document.getElementById('error-summary');

    if (!summary) {
      summary = document.createElement('div');
      summary.id = 'error-summary';
      summary.className = 'error-summary';
      summary.setAttribute('role', 'alert');
      summary.setAttribute('aria-live', 'assertive');

      const form = document.getElementById('contact-form');
      if (form) form.insertBefore(summary, form.firstChild);
    }

    summary.innerHTML = `
      <h3>Vui lòng sửa các lỗi sau:</h3>
      <ul>
        ${errors.map((err) => `<li>${sanitizeInput(err)}</li>`).join('')}
      </ul>
    `;
    summary.style.display = 'block';
    summary.focus(); // Hỗ trợ screen reader
  };

  /**
   * Validate toàn bộ form
   * @param {FormData} formData
   * @param {HTMLInputElement} fileInput
   * @returns {{valid: boolean, errors: string[], data: object}}
   */
  const validateForm = (formData, fileInput) => {
    const errors = [];

    const data = {
      name: (formData.get('name') || '').trim(),
      email: (formData.get('email') || '').trim(),
      phone: (formData.get('phone') || '').trim(),
      department: formData.get('department'),
      priority: formData.get('priority'),
      subject: (formData.get('subject') || '').trim(),
      message: (formData.get('message') || '').trim(),
      privacy: formData.get('privacy'),
      file: fileInput?.files?.[0] || null,
    };

    clearErrors();

    // Name
    if (!data.name || data.name.length < 2) {
      errors.push('Họ tên phải ít nhất 2 ký tự');
      showFieldError('name', 'Họ tên phải ít nhất 2 ký tự');
    }

    // Email
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!data.email || !emailRegex.test(data.email)) {
      errors.push('Email không hợp lệ');
      showFieldError('email', 'Email không hợp lệ');
    }

    // Phone (optional)
    if (data.phone && !/^[0-9]{10,11}$/.test(data.phone)) {
      errors.push('Số điện thoại phải là 10-11 chữ số');
      showFieldError('phone', 'Số điện thoại phải là 10-11 chữ số');
    }

    // Department
    if (!data.department) {
      errors.push('Vui lòng chọn phòng ban');
      showFieldError('department', 'Vui lòng chọn phòng ban');
    }

    // Priority
    if (!data.priority) {
      errors.push('Vui lòng chọn mức độ ưu tiên');
      // Không có id cho radio group → không show inline error cho group
    }

    // Subject
    if (!data.subject || data.subject.length < 5) {
      errors.push('Tiêu đề phải ít nhất 5 ký tự');
      showFieldError('subject', 'Tiêu đề phải ít nhất 5 ký tự');
    }

    // Message
    if (!data.message || data.message.length < 20) {
      errors.push('Nội dung phải ít nhất 20 ký tự');
      showFieldError('message', 'Nội dung phải ít nhất 20 ký tự');
    }

    // Privacy
    if (!data.privacy) {
      errors.push('Bạn phải đồng ý với chính sách bảo mật');
      showFieldError('privacy', 'Bạn phải đồng ý với chính sách bảo mật');
    }

    // File
    const fileError = validateFile(data.file);
    if (fileError) {
      errors.push(fileError);
      showFieldError('attachment', fileError);
    }

    return { valid: errors.length === 0, errors, data };
  };

  /**
   * Giả lập gọi API
   * @param {object} data
   */
  const submitToAPI = async (data) => {
    await new Promise((resolve) => setTimeout(resolve, 1800));

    // 92% thành công để test error hiếm
    if (Math.random() > 0.08) {
      return { success: true, message: 'Gửi thành công' };
    }
    throw new Error('Lỗi server tạm thời. Vui lòng thử lại sau.');
  };

  /**
   * Xử lý submit form
   */
  const handleSubmit = async (event) => {
    event.preventDefault();

    if (isSubmitting) return;

    const form = event.target;
    const formData = new FormData(form);
    const fileInput = document.getElementById('attachment');

    const { valid, errors, data } = validateForm(formData, fileInput);

    if (!valid) {
      showErrorSummary(errors);

      const firstInvalid = document.querySelector('[aria-invalid="true"]');
      if (firstInvalid) {
        firstInvalid.focus();
      }
      return;
    }

    isSubmitting = true;
    const submitBtn = form.querySelector('button[type="submit"]');
    const originalText = submitBtn.textContent;
    submitBtn.disabled = true;
    submitBtn.textContent = 'Đang gửi...';

    try {
      await submitToAPI(data);

      alert('✅ Gửi thành công! Chúng tôi sẽ phản hồi sớm nhất có thể.');

      form.reset();
      clearErrors();

      // Reset file input visual
      const fileLabel = document.querySelector('label[for="attachment"]');
      if (fileLabel)
        fileLabel.textContent = 'Đính kèm file: (PDF, DOC, DOCX - Max 5MB)';

      console.log('✅ Contact form submitted:', {
        ...data,
        file: data.file
          ? {
              name: data.file.name,
              size: `${(data.file.size / 1024 / 1024).toFixed(2)} MB`,
              type: data.file.type,
            }
          : null,
      });
    } catch (err) {
      alert(
        '❌ ' + (err.message || 'Có lỗi xảy ra khi gửi. Vui lòng thử lại.'),
      );
      console.error('Submission failed:', err);
    } finally {
      isSubmitting = false;
      submitBtn.disabled = false;
      submitBtn.textContent = originalText;
    }
  };

  /**
   * Validate file ngay khi chọn
   */
  const handleFileChange = (e) => {
    const file = e.target.files[0];
    const error = validateFile(file);

    if (error) {
      showFieldError('attachment', error);
      e.target.value = '';
    } else {
      // Xóa lỗi nếu có
      const errorEl = document.getElementById('attachment-error');
      if (errorEl) errorEl.style.display = 'none';
      document.getElementById('attachment')?.removeAttribute('aria-invalid');
    }
  };

  return (
    <form
      id='contact-form'
      onSubmit={handleSubmit}
      noValidate
      style={{
        maxWidth: '680px',
        margin: '0 auto',
        padding: '24px',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      <h2>Liên hệ với chúng tôi</h2>
      <p>
        Điền thông tin bên dưới, chúng tôi sẽ phản hồi trong vòng 24-48 giờ.
      </p>

      {/* Error Summary sẽ được tạo động */}

      {/* Name */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='name'>
          Họ tên{' '}
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </label>
        <input
          type='text'
          id='name'
          name='name'
          aria-required='true'
          autoComplete='name'
          required
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        />
      </div>

      {/* Email */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='email'>
          Email{' '}
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </label>
        <input
          type='email'
          id='email'
          name='email'
          aria-required='true'
          autoComplete='email'
          required
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        />
      </div>

      {/* Phone */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='phone'>Số điện thoại</label>
        <input
          type='tel'
          id='phone'
          name='phone'
          autoComplete='tel'
          placeholder='Ví dụ: 0901234567'
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        />
      </div>

      {/* Department */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='department'>
          Phòng ban{' '}
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </label>
        <select
          id='department'
          name='department'
          aria-required='true'
          required
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        >
          <option
            value=''
            disabled
            selected
          >
            -- Chọn phòng ban --
          </option>
          <option value='sales'>Sales</option>
          <option value='support'>Support</option>
          <option value='hr'>HR</option>
          <option value='other'>Other</option>
        </select>
      </div>

      {/* Priority */}
      <fieldset
        style={{
          marginBottom: '24px',
          border: '1px solid #ddd',
          padding: '16px',
        }}
      >
        <legend>
          Mức độ ưu tiên{' '}
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </legend>
        {['low', 'medium', 'high', 'urgent'].map((val, i) => (
          <label
            key={val}
            style={{ display: 'block', marginBottom: '8px' }}
          >
            <input
              type='radio'
              name='priority'
              value={val}
              defaultChecked={i === 1}
              required
            />{' '}
            {val === 'low'
              ? 'Thấp'
              : val === 'medium'
                ? 'Trung bình'
                : val === 'high'
                  ? 'Cao'
                  : 'Khẩn cấp'}
          </label>
        ))}
      </fieldset>

      {/* Subject */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='subject'>
          Tiêu đề{' '}
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </label>
        <input
          type='text'
          id='subject'
          name='subject'
          aria-required='true'
          placeholder='Tóm tắt nội dung liên hệ'
          required
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        />
      </div>

      {/* Message */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='message'>
          Nội dung{' '}
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </label>
        <textarea
          id='message'
          name='message'
          aria-required='true'
          rows={6}
          placeholder='Mô tả chi tiết yêu cầu của bạn (ít nhất 20 ký tự)'
          required
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        />
      </div>

      {/* File */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='attachment'>Đính kèm tài liệu (tùy chọn)</label>
        <input
          type='file'
          id='attachment'
          name='attachment'
          accept='.pdf,.doc,.docx'
          onChange={handleFileChange}
          style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
        />
      </div>

      {/* Privacy */}
      <div style={{ marginBottom: '28px' }}>
        <label>
          <input
            type='checkbox'
            id='privacy'
            name='privacy'
            aria-required='true'
            required
          />{' '}
          Tôi đã đọc và đồng ý với{' '}
          <a
            href='/privacy-policy'
            target='_blank'
            rel='noopener noreferrer'
          >
            Chính sách bảo mật
          </a>
          <span
            style={{ color: 'red' }}
            aria-label='bắt buộc'
          >
            *
          </span>
        </label>
      </div>

      {/* Submit */}
      <button
        type='submit'
        disabled={isSubmitting}
        style={{
          backgroundColor: isSubmitting ? '#6c757d' : '#0d6efd',
          color: 'white',
          padding: '12px 32px',
          border: 'none',
          borderRadius: '6px',
          fontSize: '16px',
          cursor: isSubmitting ? 'not-allowed' : 'pointer',
          minWidth: '160px',
        }}
      >
        {isSubmitting ? 'Đang gửi...' : 'Gửi thông tin'}
      </button>

      {/* Inline CSS */}
      <style>{`
        .field-error {
          color: #dc3545;
          font-size: 14px;
          margin-top: 6px;
          display: none;
        }
        .error-summary {
          background: #f8d7da;
          border-left: 5px solid #dc3545;
          padding: 16px 20px;
          margin-bottom: 28px;
          border-radius: 4px;
          display: none;
        }
        .error-summary h3 {
          margin: 0 0 12px 0;
          color: #721c24;
        }
        .error-summary ul {
          margin: 0;
          padding-left: 24px;
        }
        [aria-invalid="true"] {
          border: 2px solid #dc3545 !important;
        }
        input:focus, select:focus, textarea:focus {
          outline: 3px solid #0d6efd;
          outline-offset: 2px;
        }
        @media (prefers-contrast: high) {
          input, select, textarea, button {
            border: 2px solid currentColor !important;
          }
        }
        @media (max-width: 600px) {
          form { padding: 16px; }
          button { width: 100%; }
        }
      `}</style>
    </form>
  );
}

/* Ví dụ kết quả console khi submit thành công:
✅ Contact form submitted: {
  name: "Trần Thị B",
  email: "b.tran@company.vn",
  phone: "0918123456",
  department: "support",
  priority: "high",
  subject: "Hỗ trợ kỹ thuật khẩn cấp",
  message: "Máy chủ bị lỗi 502 liên tục từ 14h hôm nay...",
  privacy: "on",
  file: { name: "error-log.pdf", size: "2.34 MB", type: "application/pdf" }
}
*/

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh: Controlled vs Uncontrolled Forms

AspectUncontrolled FormControlled Form (Preview)
Value SourceDOM (HTML elements)React state
Access MethodFormData, event.target.elementsState variables
Update TriggerUser types directly to DOMUser types → Event → State update → Re-render
ValidationOn submit hoặc onBlurRealtime (mỗi keystroke)
Code Complexity✅ Đơn giản (ít code)⚠️ Phức tạp hơn (cần state)
Re-renders✅ Ít (chỉ khi submit)⚠️ Nhiều (mỗi keystroke)
Dynamic UI❌ Khó (cần DOM manipulation)✅ Dễ (dựa vào state)
Default ValuesdefaultValuevalue={state}
Reset Formform.reset()Set state về initial
Format Input❌ Khó (phải dùng onInput + DOM)✅ Dễ (format state)
Conditional Fields⚠️ CSS show/hide✅ Conditional rendering
Cross-field Validation✅ Dễ (có tất cả giá trị)✅ Dễ (có tất cả state)
Performance✅ Tốt hơn (ít renders)⚠️ Có thể chậm (nhiều renders)
Testing⚠️ Khó (cần query DOM)✅ Dễ (check state)

When to use Uncontrolled:

  • ✅ Simple forms (1-5 fields)
  • ✅ Chỉ cần data khi submit
  • ✅ Không cần realtime validation
  • ✅ Performance critical
  • ✅ Third-party form libraries

When to use Controlled: (Sẽ học Ngày 11)

  • ✅ Realtime validation
  • ✅ Format input while typing
  • ✅ Conditional form logic
  • ✅ Dynamic form fields
  • ✅ Complex UX requirements

Decision Tree: Chọn Form Pattern

START: Bạn cần build form?

├─> Form có <3 fields?
│   ├─> YES ──> Chỉ cần data khi submit?
│   │           ├─> YES ──> ✅ UNCONTROLLED (Simple)
│   │           └─> NO ───> ⚠️ Cần realtime feedback?
│   │                       ├─> YES ──> 🔜 CONTROLLED (Ngày 11)
│   │                       └─> NO ───> ✅ UNCONTROLLED
│   └─> NO

├─> Cần validate realtime (mỗi keystroke)?
│   ├─> YES ──> 🔜 CONTROLLED (Ngày 11)
│   └─> NO

├─> Cần format input while typing?
│   │   (Ví dụ: credit card 1234-5678-9012-3456)
│   ├─> YES ──> 🔜 CONTROLLED (Ngày 11)
│   └─> NO

├─> Cần disable button dựa vào input values?
│   ├─> YES ──> 🔜 CONTROLLED (Ngày 11)
│   └─> NO

├─> Form fields thay đổi động?
│   │   (Ví dụ: Add/remove fields)
│   ├─> YES ──> 🔜 CONTROLLED (Ngày 11)
│   └─> NO

└─> Default: ✅ UNCONTROLLED
    (Simple, performant, đủ cho most cases)

Pattern Combinations

jsx
// ✅ Hybrid Pattern: Uncontrolled + Manual Validation
function HybridForm() {
  const handleSubmit = (event) => {
    event.preventDefault();

    // Uncontrolled: Lấy data từ DOM
    const data = new FormData(event.target);

    // Manual validation
    if (validate(data)) {
      submitToAPI(data);
    }
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

// 🔜 Controlled Pattern (Ngày 11)
function ControlledForm() {
  const [email, setEmail] = useState('');
  const [isValid, setIsValid] = useState(false);

  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
    setIsValid(validateEmail(value));
  };

  return (
    <form>
      <input
        value={email}
        onChange={handleChange}
      />
      {!isValid && <span>Invalid email</span>}
    </form>
  );
}

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Form Submit Reloads Page ⚠️

jsx
// ❌ BUG: Page reloads sau khi submit
function BuggyForm1() {
  const handleSubmit = () => {
    // ❌ Thiếu event.preventDefault()!
    const data = new FormData(document.getElementById('myform'));
    console.log(data);
  };

  return (
    <form
      id='myform'
      onSubmit={handleSubmit}
    >
      <input name='email' />
      <button type='submit'>Submit</button>
    </form>
  );
}

// 🤔 QUESTIONS:
// 1. Tại sao page reload?
// 2. Console.log có chạy không?
// 3. Làm sao fix?

// ✅ SOLUTION:
function FixedForm1() {
  const handleSubmit = (event) => {
    event.preventDefault(); // ← KEY FIX!
    const data = new FormData(event.target);
    console.log(Object.fromEntries(data));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name='email' />
      <button type='submit'>Submit</button>
    </form>
  );
}

// 📚 EXPLANATION:
// - HTML form mặc định submit đến server (page reload)
// - event.preventDefault() ngăn behavior mặc định
// - Luôn LUÔN cần preventDefault trong React forms!

Bug 2: Cannot Read Checkbox Value ⚠️

jsx
// ❌ BUG: Checkbox value luôn là null
function BuggyForm2() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    const terms = formData.get('terms');
    console.log('Terms:', terms); // ❌ null hoặc "on"?

    if (terms === true) {
      // ❌ SAI! Không bao giờ true
      console.log('Accepted');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type='checkbox'
        name='terms'
      />
      <button type='submit'>Submit</button>
    </form>
  );
}

// 🤔 QUESTIONS:
// 1. Checkbox value là gì khi checked/unchecked?
// 2. Làm sao convert sang boolean?
// 3. Tại sao không dùng === true?

// ✅ SOLUTION:
function FixedForm2() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    const termsValue = formData.get('terms');
    // termsValue is "on" (checked) or null (unchecked)

    const termsAccepted = !!termsValue; // Convert to boolean
    // OR: termsValue !== null
    // OR: formData.has('terms')

    console.log('Terms accepted:', termsAccepted);

    if (termsAccepted) {
      console.log('✅ User accepted terms');
    } else {
      console.log('❌ User did NOT accept terms');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        <input
          type='checkbox'
          name='terms'
        />{' '}
        I accept terms
      </label>
      <button type='submit'>Submit</button>
    </form>
  );
}

// 📚 EXPLANATION:
// - Unchecked checkbox: formData.get() returns null
// - Checked checkbox: formData.get() returns "on" (string!)
// - Convert to boolean: !!value or value !== null
// - formData.has('key') is cleaner for existence check

Bug 3: Cannot Get Multiple Checkbox Values ⚠️

jsx
// ❌ BUG: Chỉ lấy được 1 checkbox value
function BuggyForm3() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    // ❌ SAI: get() chỉ trả về GIÁ TRỊ ĐẦU TIÊN!
    const interests = formData.get('interests');
    console.log('Interests:', interests); // Chỉ 1 value!
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        <input
          type='checkbox'
          name='interests'
          value='sports'
        />{' '}
        Sports
      </label>
      <label>
        <input
          type='checkbox'
          name='interests'
          value='music'
        />{' '}
        Music
      </label>
      <label>
        <input
          type='checkbox'
          name='interests'
          value='tech'
        />{' '}
        Tech
      </label>
      <button type='submit'>Submit</button>
    </form>
  );
}

// 🤔 QUESTIONS:
// 1. Tại sao chỉ lấy được 1 value?
// 2. Làm sao lấy tất cả checked values?
// 3. Nếu không có checkbox nào checked thì sao?

// ✅ SOLUTION:
function FixedForm3() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    // ✅ ĐÚNG: Dùng getAll() cho multiple values
    const interests = formData.getAll('interests');
    console.log('Interests:', interests); // Array of values

    if (interests.length === 0) {
      console.log('No interests selected');
    } else {
      console.log('Selected:', interests.join(', '));
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <fieldset>
        <legend>Select your interests:</legend>
        <label style={{ display: 'block' }}>
          <input
            type='checkbox'
            name='interests'
            value='sports'
          />{' '}
          Sports
        </label>
        <label style={{ display: 'block' }}>
          <input
            type='checkbox'
            name='interests'
            value='music'
          />{' '}
          Music
        </label>
        <label style={{ display: 'block' }}>
          <input
            type='checkbox'
            name='interests'
            value='tech'
          />{' '}
          Tech
        </label>
      </fieldset>
      <button type='submit'>Submit</button>
    </form>
  );
}

// 📚 EXPLANATION:
// - get(name): Returns first value only
// - getAll(name): Returns ARRAY of all values
// - Unchecked checkboxes: NOT included in array
// - Always use getAll() for multiple checkboxes with same name

✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

Đánh dấu ✅ những điều bạn đã hiểu:

Form Basics:

  • [ ] Hiểu sự khác biệt giữa HTML form và React form
  • [ ] Biết khi nào cần event.preventDefault()
  • [ ]Biết cách truy xuất form values bằng FormData API
  • [ ] Biết cách truy xuất form values bằng event.target.elements
  • [ ] Hiểu name attribute quan trọng như thế nào

Input Types:

  • [ ] Xử lý được text, email, password inputs
  • [ ] Xử lý được checkboxes (single & multiple)
  • [ ] Xử lý được radio buttons
  • [ ] Xử lý được select dropdowns
  • [ ] Xử lý được textarea
  • [ ] Xử lý được file uploads

Validation:

  • [ ] Validate required fields
  • [ ] Validate email format
  • [ ] Validate string length (min/max)
  • [ ] Cross-field validation (password confirm)
  • [ ] Regex validation
  • [ ] File validation (size, type)

Event Handling:

  • [ ] Hiểu sự khác biệt onChange React vs HTML
  • [ ] Dùng được onSubmit, onChange, onBlur
  • [ ] Prevent paste (onPaste)
  • [ ] Format input (onInput)

Best Practices:

  • [ ] Accessibility (labels, aria attributes)
  • [ ] Error handling (inline + summary)
  • [ ] Loading states
  • [ ] Security (input sanitization)
  • [ ] Form reset

Uncontrolled vs Controlled (Conceptual):

  • [ ] Hiểu khái niệm controlled component
  • [ ] Biết limitations của uncontrolled forms
  • [ ] Biết khi nào cần controlled (prepare for Ngày 11)

Code Review Checklist

Review code của bạn:

Functionality:

  • [ ] Form submit không reload page?
  • [ ] Validation chạy đúng?
  • [ ] Error messages hiển thị rõ ràng?
  • [ ] Form reset sau submit thành công?

Code Quality:

  • [ ] Event handlers có event.preventDefault()?
  • [ ] Dùng FormData.get() cho single value?
  • [ ] Dùng FormData.getAll() cho multiple values?
  • [ ] Convert checkbox value sang boolean?
  • [ ] Trim whitespace từ text inputs?

Accessibility:

  • [ ] All inputs có labels?
  • [ ] Labels có htmlFor attribute?
  • [ ] Required fields marked (aria-required)?
  • [ ] Error messages có role="alert"?

User Experience:

  • [ ] Placeholder text hữu ích?
  • [ ] Error messages dễ hiểu?
  • [ ] Submit button có loading state?
  • [ ] Focus management hợp lý?

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút): Event Registration Form

Tạo form đăng ký sự kiện với:

  • Event name (select dropdown: Workshop, Conference, Meetup)
  • Attendee name (text, required, min 3 chars)
  • Email (email, required, valid format)
  • Number of tickets (number, 1-10)
  • Dietary restrictions (checkboxes: Vegetarian, Vegan, Gluten-free, None)
  • Special requests (textarea, optional, max 300 chars)

Requirements:

  • Validate all fields
  • Calculate total price (ticket price varies by event type)
  • Display summary before submit
  • Console.log final data
💡 Solution
jsx
/**
 * Event Registration Form - Uncontrolled pattern
 * - Validate tất cả fields khi submit
 * - Tính tổng giá tiền dựa trên loại sự kiện
 * - Hiển thị summary trước khi submit thật (confirm dialog)
 * - Console.log dữ liệu cuối cùng khi hoàn tất
 */
function EventRegistrationForm() {
  const TICKET_PRICES = {
    workshop: 300000, // 300.000 VNĐ
    conference: 1200000, // 1.200.000 VNĐ
    meetup: 150000, // 150.000 VNĐ
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);

    // Lấy dữ liệu
    const data = {
      eventType: formData.get('eventType'),
      attendeeName: (formData.get('attendeeName') || '').trim(),
      email: (formData.get('email') || '').trim(),
      tickets: parseInt(formData.get('tickets') || '0', 10),
      restrictions: formData.getAll('restrictions'),
      requests: (formData.get('requests') || '').trim(),
    };

    // Validation
    const errors = [];

    // Event type
    if (!data.eventType) {
      errors.push('Vui lòng chọn loại sự kiện');
    }

    // Attendee name
    if (!data.attendeeName || data.attendeeName.length < 3) {
      errors.push('Tên người tham dự phải ít nhất 3 ký tự');
    }

    // Email
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!data.email) {
      errors.push('Email không được để trống');
    } else if (!emailRegex.test(data.email)) {
      errors.push('Email không hợp lệ');
    }

    // Number of tickets
    if (isNaN(data.tickets) || data.tickets < 1 || data.tickets > 10) {
      errors.push('Số lượng vé phải từ 1 đến 10');
    }

    // Dietary restrictions - không bắt buộc, nhưng nếu chọn "None" thì không nên chọn cái khác
    if (data.restrictions.includes('none') && data.restrictions.length > 1) {
      errors.push(
        'Nếu chọn "Không có hạn chế" thì không nên chọn hạn chế khác',
      );
    }

    // Special requests - optional, chỉ check độ dài
    if (data.requests.length > 300) {
      errors.push('Yêu cầu đặc biệt không được vượt quá 300 ký tự');
    }

    if (errors.length > 0) {
      alert('Vui lòng sửa các lỗi sau:\n\n' + errors.join('\n'));
      return;
    }

    // Tính giá
    const ticketPrice = TICKET_PRICES[data.eventType];
    const totalPrice = ticketPrice * data.tickets;

    // Tạo summary để confirm
    const restrictionsText =
      data.restrictions.length === 0
        ? 'Không'
        : data.restrictions
            .map((r) => {
              switch (r) {
                case 'vegetarian':
                  return 'Chay';
                case 'vegan':
                  return 'Thuần chay';
                case 'glutenfree':
                  return 'Không gluten';
                case 'none':
                  return 'Không có hạn chế';
                default:
                  return r;
              }
            })
            .join(', ');

    const summary = `
ĐĂNG KÝ SỰ KIỆN - XÁC NHẬN

Loại sự kiện: ${
      data.eventType === 'workshop'
        ? 'Workshop'
        : data.eventType === 'conference'
          ? 'Hội nghị'
          : 'Meetup'
    }

Tên người tham dự: ${data.attendeeName}
Email: ${data.email}
Số lượng vé: ${data.tickets}
Giá mỗi vé: ${ticketPrice.toLocaleString('vi-VN')} VNĐ
Tổng tiền: ${totalPrice.toLocaleString('vi-VN')} VNĐ

Hạn chế ăn uống: ${restrictionsText}

Yêu cầu đặc biệt:
${data.requests || 'Không có'}

Bạn có muốn gửi đăng ký này không?
    `;

    if (!confirm(summary)) {
      return; // User hủy
    }

    // Submit thành công
    const finalData = {
      ...data,
      totalPrice,
      ticketPrice,
      submittedAt: new Date().toISOString(),
    };

    console.log('✅ Đăng ký sự kiện thành công:', finalData);

    alert('Đăng ký thành công! Chúng tôi đã nhận được thông tin của bạn.');

    event.target.reset();
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ maxWidth: '540px', margin: '0 auto' }}
    >
      <h2>Đăng ký tham dự sự kiện</h2>

      {/* Event Type */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='eventType'>Loại sự kiện: *</label>
        <select
          id='eventType'
          name='eventType'
          required
          style={{ width: '100%', padding: '8px' }}
        >
          <option
            value=''
            disabled
            selected
          >
            -- Chọn loại sự kiện --
          </option>
          <option value='workshop'>Workshop</option>
          <option value='conference'>Conference</option>
          <option value='meetup'>Meetup</option>
        </select>
      </div>

      {/* Attendee Name */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='attendeeName'>Họ và tên: *</label>
        <input
          type='text'
          id='attendeeName'
          name='attendeeName'
          required
          minLength={3}
          placeholder='Ví dụ: Nguyễn Văn A'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Email */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='email'>Email: *</label>
        <input
          type='email'
          id='email'
          name='email'
          required
          placeholder='example@email.com'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Number of Tickets */}
      <div style={{ marginBottom: '20px' }}>
        <label htmlFor='tickets'>Số lượng vé (1-10): *</label>
        <input
          type='number'
          id='tickets'
          name='tickets'
          min='1'
          max='10'
          defaultValue='1'
          required
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      {/* Dietary Restrictions */}
      <fieldset style={{ marginBottom: '20px' }}>
        <legend>Hạn chế về ăn uống:</legend>
        <label style={{ display: 'block', margin: '6px 0' }}>
          <input
            type='checkbox'
            name='restrictions'
            value='vegetarian'
          />
          Chay (Vegetarian)
        </label>
        <label style={{ display: 'block', margin: '6px 0' }}>
          <input
            type='checkbox'
            name='restrictions'
            value='vegan'
          />
          Thuần chay (Vegan)
        </label>
        <label style={{ display: 'block', margin: '6px 0' }}>
          <input
            type='checkbox'
            name='restrictions'
            value='glutenfree'
          />
          Không gluten (Gluten-free)
        </label>
        <label style={{ display: 'block', margin: '6px 0' }}>
          <input
            type='checkbox'
            name='restrictions'
            value='none'
          />
          Không có hạn chế nào
        </label>
      </fieldset>

      {/* Special Requests */}
      <div style={{ marginBottom: '24px' }}>
        <label htmlFor='requests'>Yêu cầu đặc biệt (tùy chọn):</label>
        <textarea
          id='requests'
          name='requests'
          rows={4}
          maxLength={300}
          placeholder='Ví dụ: cần chỗ ngồi gần sân khấu, dị ứng hải sản...'
          style={{ width: '100%', padding: '8px' }}
        />
      </div>

      <button
        type='submit'
        style={{
          backgroundColor: '#28a745',
          color: 'white',
          padding: '12px 28px',
          border: 'none',
          borderRadius: '6px',
          fontSize: '16px',
          cursor: 'pointer',
        }}
      >
        Đăng ký ngay
      </button>
    </form>
  );
}

/* Ví dụ kết quả console khi submit thành công:
✅ Đăng ký sự kiện thành công: {
  eventType: "conference",
  attendeeName: "Trần Thị Bích",
  email: "bich.tran@example.com",
  tickets: 2,
  restrictions: ["vegetarian", "glutenfree"],
  requests: "Cần chỗ ngồi gần loa, không uống cà phê",
  totalPrice: 2400000,
  ticketPrice: 1200000,
  submittedAt: "2026-01-21T08:15:32.123Z"
}
*/

Nâng cao (60 phút): Job Application Form

Build form ứng tuyển công việc:

  • Personal info (name, email, phone)
  • Position applying (dropdown)
  • Experience level (radio: Junior/Mid/Senior)
  • Resume upload (PDF only, max 2MB)
  • Cover letter (textarea, 50-500 words)
  • References (checkbox: "I have 2+ references")
  • Portfolio URL (optional, must be valid URL if provided)

Advanced requirements:

  • Word count for cover letter
  • URL validation for portfolio
  • Conditional field: If Senior → "Years of experience" (number input)
  • Multi-step: Step 1 (Personal), Step 2 (Application), Step 3 (Review & Submit)
  • Accessibility: Full ARIA support
  • Error handling: Inline + summary
💡 Solution
jsx
/**
 * Job Application Form - Production-ready, uncontrolled, multi-step
 * Với conditional fields, validation đầy đủ, accessibility ARIA,
 * error inline + summary, file validation, word count display
 */
function JobApplicationForm() {
  // Track submission state (non-React)
  let isSubmitting = false;

  // Ticket prices or other constants if needed - not here

  // Show specific step
  const showStep = (stepNumber) => {
    document.querySelectorAll('.form-step').forEach((step) => {
      step.style.display = 'none';
    });
    const target = document.getElementById(`step-${stepNumber}`);
    if (target) target.style.display = 'block';

    const indicator = document.getElementById('step-indicator');
    if (indicator) indicator.textContent = `Bước ${stepNumber}/3`;
  };

  // Show/hide conditional field based on experience level
  const handleExperienceChange = (event) => {
    const yearsSection = document.getElementById('years-experience-section');
    if (yearsSection) {
      yearsSection.style.display =
        event.target.value === 'senior' ? 'block' : 'none';
      if (event.target.value !== 'senior') {
        const input = document.getElementById('yearsExperience');
        if (input) input.value = '';
      }
    }
  };

  // Update word count for cover letter
  const handleCoverLetterInput = (event) => {
    const text = event.target.value.trim();
    const words = text ? text.split(/\s+/).length : 0;
    const countEl = document.getElementById('cover-letter-word-count');
    if (countEl) {
      countEl.textContent = `Số từ: ${words} (50-500 từ)`;
      countEl.style.color = words < 50 || words > 500 ? '#dc3545' : '#198754';
    }
  };

  // Validate file resume
  const handleResumeChange = (event) => {
    const file = event.target.files[0];
    const errorEl = document.getElementById('resume-error');

    if (errorEl) errorEl.style.display = 'none';

    if (!file) return;

    const maxSize = 2 * 1024 * 1024; // 2MB
    if (file.size > maxSize) {
      showFieldError(
        'resume',
        `File quá lớn (tối đa 2MB). Kích thước: ${(file.size / 1024 / 1024).toFixed(2)}MB`,
      );
      event.target.value = '';
      return;
    }

    if (file.type !== 'application/pdf') {
      showFieldError('resume', 'Chỉ chấp nhận file PDF');
      event.target.value = '';
      return;
    }
  };

  // Validate step 1: Personal info
  const validateStep1 = () => {
    const name = document.getElementById('name')?.value.trim() || '';
    const email = document.getElementById('email')?.value.trim() || '';
    const phone = document.getElementById('phone')?.value.trim() || '';

    const errors = [];
    if (name.length < 3) errors.push('Họ tên phải ít nhất 3 ký tự');
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
      errors.push('Email không hợp lệ');
    if (phone && !/^[0-9]{10,11}$/.test(phone))
      errors.push('Số điện thoại phải 10-11 chữ số');

    return errors;
  };

  // Validate step 2: Application info
  const validateStep2 = () => {
    const position = document.getElementById('position')?.value || '';
    const experience =
      document.querySelector('input[name="experience"]:checked')?.value || '';
    const years = document.getElementById('yearsExperience')?.value || '';
    const portfolio = document.getElementById('portfolio')?.value.trim() || '';

    const errors = [];
    if (!position) errors.push('Vui lòng chọn vị trí ứng tuyển');
    if (!experience) errors.push('Vui lòng chọn mức độ kinh nghiệm');
    if (experience === 'senior' && (isNaN(years) || years < 5 || years > 50)) {
      errors.push('Số năm kinh nghiệm phải từ 5 đến 50');
    }
    if (portfolio && !/^https?:\/\/[^\s$.?#].[^\s]*$/.test(portfolio)) {
      errors.push('Portfolio URL không hợp lệ (phải bắt đầu bằng http/https)');
    }

    return errors;
  };

  // Next handlers with validation
  const handleNext1 = () => {
    clearErrors();
    const errors = validateStep1();
    if (errors.length > 0) {
      showErrorSummary(errors);
      return;
    }
    showStep(2);
  };

  const handleNext2 = () => {
    clearErrors();
    const errors = validateStep2();
    if (errors.length > 0) {
      showErrorSummary(errors);
      return;
    }
    // Generate review summary in step 3
    generateReviewSummary();
    showStep(3);
  };

  // Back handlers
  const handleBack2 = () => showStep(1);
  const handleBack3 = () => showStep(2);

  // Generate summary for step 3
  const generateReviewSummary = () => {
    const form = document.querySelector('form');
    if (!form) return;

    const formData = new FormData(form);

    const data = {
      name: formData.get('name')?.trim(),
      email: formData.get('email')?.trim(),
      phone: formData.get('phone')?.trim(),
      position: formData.get('position'),
      experience: formData.get('experience'),
      yearsExperience: formData.get('yearsExperience'),
      portfolio: formData.get('portfolio')?.trim(),
      coverLetter: formData.get('coverLetter')?.trim(),
      references: !!formData.get('references'),
      resume: document.getElementById('resume')?.files[0],
    };

    const positionMap = {
      developer: 'Developer',
      designer: 'Designer',
      manager: 'Manager',
    };

    const experienceMap = {
      junior: 'Junior',
      mid: 'Mid-level',
      senior: 'Senior',
    };

    const summaryEl = document.getElementById('review-summary');
    if (summaryEl) {
      summaryEl.innerHTML = `
        <h4>Thông tin cá nhân</h4>
        <p>Họ tên: ${sanitizeInput(data.name || 'N/A')}</p>
        <p>Email: ${sanitizeInput(data.email || 'N/A')}</p>
        <p>Số điện thoại: ${sanitizeInput(data.phone || 'N/A')}</p>

        <h4>Thông tin ứng tuyển</h4>
        <p>Vị trí: ${positionMap[data.position] || 'N/A'}</p>
        <p>Mức độ kinh nghiệm: ${experienceMap[data.experience] || 'N/A'}</p>
        ${data.experience === 'senior' ? `<p>Số năm kinh nghiệm: ${data.yearsExperience || 'N/A'}</p>` : ''}
        <p>Portfolio URL: ${sanitizeInput(data.portfolio || 'Không có')}</p>

        <h4>Tài liệu</h4>
        <p>Resume: ${data.resume ? sanitizeInput(data.resume.name) : 'Chưa upload'}</p>
        <p>Thư xin việc: ${data.coverLetter ? `${data.coverLetter.substring(0, 100)}... (${data.coverLetter.split(/\s+/).length} từ)` : 'Không có'}</p>
        <p>Tham chiếu: ${data.references ? 'Có (2+ references)' : 'Không'}</p>
      `;
    }
  };

  // Full validation for submit
  const validateAll = (formData, resumeInput) => {
    const errors = [...validateStep1(), ...validateStep2()];

    const data = {
      coverLetter: formData.get('coverLetter')?.trim() || '',
      references: !!formData.get('references'),
      resume: resumeInput.files[0],
    };

    const wordCount = data.coverLetter.split(/\s+/).filter((w) => w).length;
    if (wordCount < 50 || wordCount > 500) {
      errors.push(`Thư xin việc phải từ 50-500 từ (hiện tại: ${wordCount})`);
    }

    if (data.resume) {
      if (data.resume.type !== 'application/pdf')
        errors.push('Resume phải là file PDF');
      if (data.resume.size > 2 * 1024 * 1024) errors.push('Resume tối đa 2MB');
    } else {
      errors.push('Vui lòng upload resume');
    }

    // Show inline errors where possible
    if (errors.find((e) => e.includes('Thư xin việc')))
      showFieldError('coverLetter', 'Thư xin việc phải 50-500 từ');
    if (errors.find((e) => e.includes('Resume')))
      showFieldError('resume', 'Resume phải PDF, <2MB');

    return {
      valid: errors.length === 0,
      errors,
      data: { ...data /* add others if needed */ },
    };
  };

  // Submit handler
  const handleSubmit = async (event) => {
    event.preventDefault();
    if (isSubmitting) return;

    clearErrors();

    const form = event.target;
    const formData = new FormData(form);
    const resumeInput = document.getElementById('resume');

    const { valid, errors } = validateAll(formData, resumeInput);

    if (!valid) {
      showErrorSummary(errors);
      const firstInvalid = document.querySelector('[aria-invalid="true"]');
      if (firstInvalid) firstInvalid.focus();
      return;
    }

    isSubmitting = true;
    const submitBtn = form.querySelector('button[type="submit"]');
    const origText = submitBtn.textContent;
    submitBtn.textContent = 'Đang gửi...';
    submitBtn.disabled = true;

    try {
      // Simulate API
      await new Promise((r) => setTimeout(r, 1500));

      console.log('✅ Ứng tuyển thành công:', Object.fromEntries(formData));

      alert('Đăng ký ứng tuyển thành công!');
      form.reset();
      showStep(1);
    } catch (e) {
      alert('Lỗi: ' + e.message);
    } finally {
      isSubmitting = false;
      submitBtn.textContent = origText;
      submitBtn.disabled = false;
    }
  };

  // Helper functions from previous (sanitizeInput, showFieldError, clearErrors, showErrorSummary)

  /**
   * Sanitize to prevent XSS
   */
  function sanitizeInput(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
  }

  // Show field error (similar to prev)
  const showFieldError = (fieldId, message) => {
    const errorId = `${fieldId}-error`;
    let errorEl = document.getElementById(errorId);
    if (!errorEl) {
      errorEl = document.createElement('span');
      errorEl.id = errorId;
      errorEl.className = 'error-message';
      errorEl.setAttribute('role', 'alert');
      const field = document.getElementById(fieldId);
      if (field) field.parentNode.appendChild(errorEl);
    }
    errorEl.textContent = message;
    errorEl.style.display = 'block';

    const field = document.getElementById(fieldId);
    if (field) field.setAttribute('aria-invalid', 'true');
  };

  // Clear errors (similar)
  const clearErrors = () => {
    document
      .querySelectorAll('.error-message')
      .forEach((el) => (el.style.display = 'none'));
    document
      .querySelectorAll('[aria-invalid="true"]')
      .forEach((el) => el.removeAttribute('aria-invalid'));
    const summary = document.getElementById('error-summary');
    if (summary) summary.style.display = 'none';
  };

  // Show summary (similar)
  const showErrorSummary = (errors) => {
    let summary = document.getElementById('error-summary');
    if (!summary) {
      summary = document.createElement('div');
      summary.id = 'error-summary';
      summary.setAttribute('role', 'alert');
      const form = document.querySelector('form');
      form.insertBefore(summary, form.firstChild);
    }
    summary.innerHTML =
      '<h3>Lỗi:</h3><ul>' +
      errors.map((e) => `<li>${e}</li>`).join('') +
      '</ul>';
    summary.style.display = 'block';
    summary.focus();
  };

  // Initial setup
  setTimeout(() => showStep(1), 0);

  return (
    <form onSubmit={handleSubmit}>
      <div
        id='step-indicator'
        style={{ fontWeight: 'bold' }}
      >
        Bước 1/3
      </div>
      <div
        id='error-summary'
        style={{ display: 'none', color: 'red' }}
      ></div>

      {/* Step 1: Personal */}
      <div
        id='step-1'
        className='form-step'
      >
        <h3>Thông tin cá nhân</h3>
        <label htmlFor='name'>Họ tên *</label>
        <input
          id='name'
          name='name'
          aria-required='true'
        />

        <label htmlFor='email'>Email *</label>
        <input
          id='email'
          name='email'
          type='email'
          aria-required='true'
        />

        <label htmlFor='phone'>Số điện thoại</label>
        <input
          id='phone'
          name='phone'
          type='tel'
        />

        <button
          type='button'
          onClick={handleNext1}
        >
          Tiếp theo
        </button>
      </div>

      {/* Step 2: Application */}
      <div
        id='step-2'
        className='form-step'
        style={{ display: 'none' }}
      >
        <h3>Thông tin ứng tuyển</h3>

        <label htmlFor='position'>Vị trí ứng tuyển *</label>
        <select
          id='position'
          name='position'
          aria-required='true'
        >
          <option value=''>Chọn vị trí</option>
          <option value='developer'>Developer</option>
          <option value='designer'>Designer</option>
          <option value='manager'>Manager</option>
        </select>

        <fieldset aria-required='true'>
          <legend>Mức độ kinh nghiệm *</legend>
          <label>
            <input
              type='radio'
              name='experience'
              value='junior'
              onChange={handleExperienceChange}
            />{' '}
            Junior
          </label>
          <label>
            <input
              type='radio'
              name='experience'
              value='mid'
              onChange={handleExperienceChange}
            />{' '}
            Mid
          </label>
          <label>
            <input
              type='radio'
              name='experience'
              value='senior'
              onChange={handleExperienceChange}
            />{' '}
            Senior
          </label>
        </fieldset>

        <div
          id='years-experience-section'
          style={{ display: 'none' }}
        >
          <label htmlFor='yearsExperience'>Số năm kinh nghiệm *</label>
          <input
            id='yearsExperience'
            name='yearsExperience'
            type='number'
            min='5'
            max='50'
            aria-required='true'
          />
        </div>

        <label htmlFor='portfolio'>Portfolio URL (tùy chọn)</label>
        <input
          id='portfolio'
          name='portfolio'
          type='url'
        />

        <button
          type='button'
          onClick={handleBack2}
        >
          Quay lại
        </button>
        <button
          type='button'
          onClick={handleNext2}
        >
          Tiếp theo
        </button>
      </div>

      {/* Step 3: Review & Submit */}
      <div
        id='step-3'
        className='form-step'
        style={{ display: 'none' }}
      >
        <h3>Xem lại thông tin</h3>
        <div id='review-summary'></div>

        <button
          type='button'
          onClick={handleBack3}
        >
          Quay lại
        </button>
        <button type='submit'>Gửi ứng tuyển</button>
      </div>

      {/* Global styles */}
      <style>{`
        .form-step { margin: 20px 0; }
        label { display: block; margin-top: 10px; }
        input, select, textarea { width: 100%; margin-bottom: 10px; }
        .error-message { color: red; display: none; font-size: 0.9em; }
      `}</style>
    </form>
  );
}

/* Ví dụ console khi submit thành công:
✅ Ứng tuyển thành công: {
  name: "Nguyễn Văn A",
  email: "a@example.com",
  phone: "0123456789",
  position: "developer",
  experience: "senior",
  yearsExperience: "10",
  portfolio: "https://portfolio.com",
  resume: File { name: "resume.pdf", ... },
  coverLetter: "Long text here...",
  references: "on"
}
*/

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Forms

  2. MDN - FormData API

Đọc thêm

  1. Web.dev - Form Best Practices

  2. WCAG 2.1 - Forms


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền (Đã học)

  • Ngày 1-2: ES6+ (destructuring, spread operator)
  • Ngày 3: JSX syntax
  • Ngày 4: Props & Components
  • Ngày 5: Event handling, Conditional rendering
  • Ngày 6: Lists & Keys
  • Ngày 7: Component composition

Hướng tới (Sẽ học)

  • Ngày 11: useState - Controlled Components
  • Ngày 12: useReducer - Complex form state
  • Ngày 36-38: Advanced Form Management (React Hook Form, Formik)
  • Ngày 42: Form Testing
  • Ngày 44: Accessibility Testing

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Form Validation Strategies:

jsx
// ❌ Amateur: Validate chỉ khi submit
// ⚠️ Mid: Validate onBlur
// ✅ Senior: Progressive validation
//    - onChange: Format (e.g., phone number)
//    - onBlur: Validate & show error
//    - onSubmit: Final check

2. Error Handling:

jsx
// ❌ Amateur: alert() cho errors
// ⚠️ Mid: Inline errors
// ✅ Senior: Inline + Summary + Focus management + Screen reader

3. Performance:

jsx
// Uncontrolled form: ~0 re-renders (best)
// Controlled with debounce: ~10 re-renders (good)
// Controlled without debounce: ~100 re-renders (avoid!)

4. Security:

jsx
// ALWAYS sanitize trước khi:
// - Display to DOM (XSS prevention)
// - Send to server (SQL injection prevention)
// - Store to localStorage (XSS prevention)

Câu Hỏi Phỏng Vấn

Junior Level:

  1. Q: Sự khác biệt giữa controlled và uncontrolled components?

    A: Controlled: React quản lý value qua state. Uncontrolled: DOM quản lý value, React đọc khi cần.

  2. Q: event.preventDefault() làm gì?

    A: Ngăn form submit mặc định (page reload).

  3. Q: Làm sao lấy giá trị từ multiple checkboxes?

    A: formData.getAll('name') returns array.

Mid Level:

  1. Q: Khi nào dùng controlled vs uncontrolled?

A: Controlled khi cần realtime validation/formatting. Uncontrolled cho simple forms.

  1. Q: Làm sao validate file upload?

    A: Check file.size (max size) và file.type (MIME type).

  2. Q: Accessibility concerns cho forms?

    A: Labels, aria-required, aria-invalid, error announcements, keyboard navigation.

Senior Level:

  1. Q: Design multi-step form architecture

A: Options: Hidden sections, URL-based, Wizard component. Consider: State persistence, validation per step, back button UX.

  1. Q: Optimize form performance

    A: Uncontrolled for simple forms, debounce for controlled, memo for complex validation logic.

  2. Q: Security trong form handling

    A: Sanitize inputs, CSRF tokens, rate limiting, file upload restrictions, HTTPS only.


War Stories

Story 1: The Million-Dollar Typo

Dự án: E-commerce checkout form
Vấn đề: User gõ email sai → Không nhận order confirmation
Giải pháp:
- Typo detection (did you mean?)
- Email confirmation field
- Preview trước submit
Lesson: UX > Validation rules

Story 2: The Invisible Error

Dự án: Job application form
Vấn đề: Screen reader users không nghe thấy errors
Giải pháp:
- role="alert" cho errors
- aria-describedby link input → error
- Focus management
Lesson: Accessibility = Legal requirement

Story 3: The Performance Nightmare

Dự án: Survey với 50+ fields
Vấn đề: Controlled form re-renders 1000+ times/second
Giải pháp:
- Switch to uncontrolled
- Validate onBlur instead of onChange
- Debounce expensive validations
Lesson: Choose right tool for the job

🎯 PREVIEW NGÀY MAI

Ngày 10: ⚡ Mini Project 1 - Static Product Catalog

Ngày mai chúng ta sẽ tổng hợp TOÀN BỘ kiến thức từ Ngày 1-9:

  • Component hierarchy (Ngày 4, 7)
  • Props drilling (Ngày 4)
  • Lists & Keys (Ngày 6)
  • Events (Ngày 5)
  • Forms (Ngày 9) ← HÔM NAY!
  • Conditional rendering (Ngày 5)
  • Styling (Ngày 8)

Project: Build một product catalog với search, filter, và contact form!

Chuẩn bị:

  • Review lại concepts Ngày 1-9
  • Đảm bảo dev environment sẵn sàng
  • Nghỉ ngơi đủ - ngày mai intensive coding! 💪

🎉 Chúc mừng! Bạn đã hoàn thành Ngày 9!

Forms là nền tảng của hầu hết web apps. Kiến thức hôm nay sẽ được dùng LIÊN TỤC trong các ngày sau. Practice nhiều để thành thạo! 🚀

Personal tech knowledge base