neat_form 1.1.0-preview.2 copy "neat_form: ^1.1.0-preview.2" to clipboard
neat_form: ^1.1.0-preview.2 copied to clipboard

A clean, lightweight, type-safe form state management and validation library for Flutter & Dart.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:neat_form/neat_form.dart';

void main() {
  runApp(const NeatFormShowcaseApp());
}

class NeatFormShowcaseApp extends StatelessWidget {
  const NeatFormShowcaseApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'neat_form Showcase',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: const Color(0xFF4F46E5),
        brightness: Brightness.light,
        inputDecorationTheme: InputDecorationTheme(
          filled: true,
          fillColor: Colors.grey.shade50,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
            borderSide: BorderSide(color: Colors.grey.shade300),
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
            borderSide: BorderSide(color: Colors.grey.shade300),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
            borderSide: const BorderSide(color: Color(0xFF4F46E5), width: 2),
          ),
        ),
      ),
      home: const FormShowcaseHomePage(),
    );
  }
}

class FormShowcaseHomePage extends StatelessWidget {
  const FormShowcaseHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Row(
            children: [
              Icon(Icons.assignment_turned_in_rounded, color: Colors.white),
              SizedBox(width: 8),
              Text(
                'neat_form Showcase',
                style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
              ),
            ],
          ),
          flexibleSpace: Container(
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                colors: [Color(0xFF4F46E5), Color(0xFF7C3AED)],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
            ),
          ),
          bottom: const TabBar(
            indicatorColor: Colors.white,
            indicatorWeight: 3,
            labelColor: Colors.white,
            unselectedLabelColor: Colors.white70,
            labelStyle: TextStyle(fontWeight: FontWeight.bold),
            tabs: [
              Tab(icon: Icon(Icons.person_add_rounded), text: 'Tài khoản & Auth'),
              Tab(icon: Icon(Icons.credit_card_rounded), text: 'Fintech & Thẻ'),
              Tab(icon: Icon(Icons.hotel_rounded), text: 'Booking & E-Com'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            AuthRegistrationTab(),
            FintechPaymentTab(),
            BookingCrossFieldTab(),
          ],
        ),
      ),
    );
  }
}

// ==========================================
// TAB 1: AUTH & REGISTRATION FORM
// ==========================================

enum AuthFormKey {
  fullName,
  email,
  username,
  password,
  confirmPassword,
  termsAccepted,
}

class AuthRegistrationTab extends StatefulWidget {
  const AuthRegistrationTab({super.key});

  @override
  State<AuthRegistrationTab> createState() => _AuthRegistrationTabState();
}

class _AuthRegistrationTabState extends State<AuthRegistrationTab> {
  late final NeatFormController<AuthFormKey> _form;
  Timer? _usernameDebounce;

  @override
  void initState() {
    super.initState();
    _form = NeatFormController<AuthFormKey>(
      initialFields: {
        AuthFormKey.fullName: const NeatFieldState<String>(value: ''),
        AuthFormKey.email: const NeatFieldState<String>(value: ''),
        AuthFormKey.username: const NeatFieldState<String>(value: ''),
        AuthFormKey.password: const NeatFieldState<String>(value: ''),
        AuthFormKey.confirmPassword: const NeatFieldState<String>(value: ''),
        AuthFormKey.termsAccepted: const NeatFieldState<bool>(value: false),
      },
      validators: {
        AuthFormKey.fullName: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập họ và tên'),
          NeatValidators.latinOnly(message: 'Họ tên chỉ được chứa chữ cái không dấu'),
          NeatValidators.noSpecialChars(message: 'Không được chứa ký tự đặc biệt'),
        ]),
        AuthFormKey.email: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập email'),
          NeatValidators.email(message: 'Email không đúng định dạng'),
        ]),
        AuthFormKey.username: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập tên đăng nhập'),
          NeatValidators.minLength(4, message: 'Tên đăng nhập tối thiểu 4 ký tự'),
          NeatValidators.noSpaces(message: 'Tên đăng nhập không được chứa khoảng trắng'),
        ]),
        AuthFormKey.password: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập mật khẩu'),
          NeatValidators.minLength(8, message: 'Mật khẩu tối thiểu 8 ký tự'),
          NeatValidators.passwordStrength(
            message: 'Mật khẩu cần ít nhất 1 chữ hoa, 1 chữ thường, 1 số và 1 ký tự đặc biệt',
          ),
        ]),
        AuthFormKey.confirmPassword: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng xác nhận mật khẩu'),
          NeatValidators.match(
            () => _form.getField<String>(AuthFormKey.password).value,
            message: 'Mật khẩu xác nhận không khớp',
          ),
        ]),
        AuthFormKey.termsAccepted: NeatValidators.mustBeTrue(
          message: 'Bạn phải đồng ý với Điều khoản dịch vụ',
        ),
      },
    );
  }

  @override
  void dispose() {
    _usernameDebounce?.cancel();
    _form.dispose();
    super.dispose();
  }

  void _onUsernameChanged(String val) {
    _form.setField(AuthFormKey.username, val);
    _usernameDebounce?.cancel();
    if (val.length >= 4) {
      _usernameDebounce = Timer(const Duration(milliseconds: 400), () {
        _form.validateFieldAsync<String>(
          AuthFormKey.username,
          (username) async {
            await Future<void>.delayed(const Duration(milliseconds: 600));
            if (username?.toLowerCase() == 'admin' || username?.toLowerCase() == 'root') {
              return const NeatValidationError(
                'username_taken',
                message: 'Tên đăng nhập này đã có người sử dụng!',
              );
            }
            return null;
          },
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _form,
      builder: (context, _) {
        final name = _form.getField<String>(AuthFormKey.fullName);
        final email = _form.getField<String>(AuthFormKey.email);
        final username = _form.getField<String>(AuthFormKey.username);
        final password = _form.getField<String>(AuthFormKey.password);
        final confirm = _form.getField<String>(AuthFormKey.confirmPassword);
        final terms = _form.getField<bool>(AuthFormKey.termsAccepted);

        return ListView(
          padding: const EdgeInsets.all(20),
          children: [
            _buildSectionHeader('Đăng ký tài khoản & Bảo mật', Icons.lock_outline),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Họ và tên (In trên thẻ/Vé máy bay)',
                hintText: 'NGUYEN VAN A',
                prefixIcon: const Icon(Icons.badge_outlined),
                errorText: name.isErrorVisible ? name.error?.message : null,
              ),
              onChanged: (val) => _form.setField(AuthFormKey.fullName, val),
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Email',
                prefixIcon: const Icon(Icons.email_outlined),
                errorText: email.isErrorVisible ? email.error?.message : null,
              ),
              keyboardType: TextInputType.emailAddress,
              onChanged: (val) => _form.setField(AuthFormKey.email, val),
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Tên đăng nhập (Kiểm tra trùng async)',
                hintText: 'Thử nhập "admin"',
                prefixIcon: const Icon(Icons.alternate_email_rounded),
                suffixIcon: username.isValidating
                    ? const Padding(
                        padding: EdgeInsets.all(12),
                        child: SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        ),
                      )
                    : null,
                errorText: username.isErrorVisible ? username.error?.message : null,
              ),
              onChanged: _onUsernameChanged,
            ),
            const SizedBox(height: 16),
            TextField(
              obscureText: true,
              decoration: InputDecoration(
                labelText: 'Mật khẩu bảo mật',
                prefixIcon: const Icon(Icons.key_outlined),
                errorText: password.isErrorVisible ? password.error?.message : null,
              ),
              onChanged: (val) => _form.setField(AuthFormKey.password, val),
            ),
            const SizedBox(height: 16),
            TextField(
              obscureText: true,
              decoration: InputDecoration(
                labelText: 'Xác nhận mật khẩu',
                prefixIcon: const Icon(Icons.lock_reset_rounded),
                errorText: confirm.isErrorVisible ? confirm.error?.message : null,
              ),
              onChanged: (val) => _form.setField(AuthFormKey.confirmPassword, val),
            ),
            const SizedBox(height: 12),
            CheckboxListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Tôi đồng ý với Điều khoản dịch vụ & Chính sách bảo mật'),
              value: terms.value,
              subtitle: terms.isErrorVisible
                  ? Text(
                      terms.error?.message ?? '',
                      style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
                    )
                  : null,
              onChanged: (val) => _form.setField(AuthFormKey.termsAccepted, val ?? false),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFF4F46E5),
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              ),
              onPressed: _form.submissionStatus.isSubmitting
                  ? null
                  : () async {
                      final success = await _form.submitForm(
                        onSubmit: (values) async {
                          await Future<void>.delayed(const Duration(seconds: 1));
                          if (context.mounted) {
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(
                                content: Text('🎉 Đăng ký tài khoản thành công!'),
                                backgroundColor: Colors.green,
                              ),
                            );
                          }
                        },
                      );
                      if (!success && context.mounted) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(
                            content: Text('⚠️ Vui lòng kiểm tra lại các trường báo lỗi'),
                            backgroundColor: Colors.redAccent,
                          ),
                        );
                      }
                    },
              child: _form.submissionStatus.isSubmitting
                  ? const CircularProgressIndicator(color: Colors.white)
                  : const Text('Đăng Ký Ngay', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            ),
          ],
        );
      },
    );
  }
}

// ==========================================
// TAB 2: FINTECH & CREDIT CARD PAYMENT FORM
// ==========================================

enum FintechKey {
  cardNumber,
  cardExpiry,
  cvv,
  amount,
}

class FintechPaymentTab extends StatefulWidget {
  const FintechPaymentTab({super.key});

  @override
  State<FintechPaymentTab> createState() => _FintechPaymentTabState();
}

class _FintechPaymentTabState extends State<FintechPaymentTab> {
  late final NeatFormController<FintechKey> _form;

  @override
  void initState() {
    super.initState();
    _form = NeatFormController<FintechKey>(
      initialFields: {
        FintechKey.cardNumber: const NeatFieldState<String>(value: ''),
        FintechKey.cardExpiry: const NeatFieldState<String>(value: ''),
        FintechKey.cvv: const NeatFieldState<String>(value: ''),
        FintechKey.amount: const NeatFieldState<num?>(value: null),
      },
      validators: {
        FintechKey.cardNumber: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập số thẻ'),
          NeatValidators.creditCard(message: 'Số thẻ không hợp lệ theo thuật toán Luhn'),
        ]),
        FintechKey.cardExpiry: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập ngày hết hạn (MM/YY)'),
          NeatValidators.pattern(
            RegExp(r'^(0[1-9]|1[0-2])\/?([0-9]{2})$'),
            message: 'Định dạng hạn thẻ phải là MM/YY (ví dụ: 12/28)',
          ),
        ]),
        FintechKey.cvv: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập mã CVV'),
          NeatValidators.exactLength(3, message: 'Mã CVV phải có đúng 3 chữ số'),
          NeatValidators.numeric(message: 'CVV chỉ gồm các chữ số'),
        ]),
        FintechKey.amount: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng nhập số tiền thanh toán'),
          NeatValidators.positive(message: 'Số tiền giao dịch phải lớn hơn 0'),
          NeatValidators.decimalPrecision(2, message: 'Số tiền tối đa 2 chữ số thập phân'),
          NeatValidators.maxValue(100000000, message: 'Hạn mức giao dịch tối đa 100.000.000đ'),
        ]),
      },
    );
  }

  @override
  void dispose() {
    _form.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _form,
      builder: (context, _) {
        final card = _form.getField<String>(FintechKey.cardNumber);
        final expiry = _form.getField<String>(FintechKey.cardExpiry);
        final cvv = _form.getField<String>(FintechKey.cvv);
        final amount = _form.getField<num?>(FintechKey.amount);

        return ListView(
          padding: const EdgeInsets.all(20),
          children: [
            _buildSectionHeader('Thanh toán thẻ & Fintech', Icons.payment_rounded),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Số thẻ tín dụng / Ghi nợ (Luhn Algorithm)',
                hintText: '4532 0151 1283 0366',
                prefixIcon: const Icon(Icons.credit_card_outlined),
                errorText: card.isErrorVisible ? card.error?.message : null,
              ),
              keyboardType: TextInputType.number,
              onChanged: (val) => _form.setField(FintechKey.cardNumber, val),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    decoration: InputDecoration(
                      labelText: 'Hạn thẻ (MM/YY)',
                      hintText: '12/28',
                      prefixIcon: const Icon(Icons.calendar_month_outlined),
                      errorText: expiry.isErrorVisible ? expiry.error?.message : null,
                    ),
                    onChanged: (val) => _form.setField(FintechKey.cardExpiry, val),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: TextField(
                    decoration: InputDecoration(
                      labelText: 'CVV / CVC',
                      hintText: '123',
                      prefixIcon: const Icon(Icons.security_rounded),
                      errorText: cvv.isErrorVisible ? cvv.error?.message : null,
                    ),
                    keyboardType: TextInputType.number,
                    onChanged: (val) => _form.setField(FintechKey.cvv, val),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Số tiền thanh toán (VNĐ)',
                hintText: '500000',
                prefixIcon: const Icon(Icons.monetization_on_outlined),
                errorText: amount.isErrorVisible ? amount.error?.message : null,
              ),
              keyboardType: const TextInputType.numberWithOptions(decimal: true),
              onChanged: (val) => _form.setField(FintechKey.amount, num.tryParse(val)),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFF059669),
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              ),
              onPressed: _form.submissionStatus.isSubmitting
                  ? null
                  : () async {
                      final success = await _form.submitForm(
                        onSubmit: (values) async {
                          await Future<void>.delayed(const Duration(seconds: 1));
                          if (context.mounted) {
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(
                                content: Text('💳 Thanh toán thành công!'),
                                backgroundColor: Color(0xFF059669),
                              ),
                            );
                          }
                        },
                      );
                      if (!success && context.mounted) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(
                            content: Text('⚠️ Thông tin thẻ hoặc số tiền không hợp lệ'),
                            backgroundColor: Colors.redAccent,
                          ),
                        );
                      }
                    },
              child: _form.submissionStatus.isSubmitting
                  ? const CircularProgressIndicator(color: Colors.white)
                  : const Text('Xác Nhận Thanh Toán', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            ),
          ],
        );
      },
    );
  }
}

// ==========================================
// TAB 3: BOOKING & CROSS-FIELD FORM
// ==========================================

enum BookingKey {
  checkInDate,
  checkOutDate,
  guestCount,
  promoCode,
  requestVAT,
  companyTaxId,
  tags,
}

class BookingCrossFieldTab extends StatefulWidget {
  const BookingCrossFieldTab({super.key});

  @override
  State<BookingCrossFieldTab> createState() => _BookingCrossFieldTabState();
}

class _BookingCrossFieldTabState extends State<BookingCrossFieldTab> {
  late final NeatFormController<BookingKey> _form;

  @override
  void initState() {
    super.initState();
    _form = NeatFormController<BookingKey>(
      initialFields: {
        BookingKey.checkInDate: const NeatFieldState<DateTime?>(value: null),
        BookingKey.checkOutDate: const NeatFieldState<DateTime?>(value: null),
        BookingKey.guestCount: const NeatFieldState<int>(value: 1),
        BookingKey.promoCode: const NeatFieldState<String>(value: '', isOptional: true),
        BookingKey.requestVAT: const NeatFieldState<bool>(value: false),
        BookingKey.companyTaxId: const NeatFieldState<String>(value: '', isOptional: true),
        BookingKey.tags: const NeatFieldState<List<String>>(value: []),
      },
      validators: {
        BookingKey.checkInDate: NeatValidators.required(message: 'Vui lòng chọn ngày nhận phòng'),
        BookingKey.checkOutDate: NeatValidators.combine([
          NeatValidators.required(message: 'Vui lòng chọn ngày trả phòng'),
          (val) {
            final checkIn = _form.getField<DateTime?>(BookingKey.checkInDate).value;
            final checkOut = val as DateTime?;
            if (checkIn != null && checkOut != null) {
              if (!checkOut.isAfter(checkIn)) {
                return const NeatValidationError(
                  'checkout_before_checkin',
                  message: 'Ngày trả phòng phải sau ngày nhận phòng ít nhất 1 ngày',
                );
              }
            }
            return null;
          },
        ]),
        BookingKey.guestCount: NeatValidators.combine([
          NeatValidators.minValue(1, message: 'Tối thiểu 1 khách'),
          NeatValidators.maxValue(10, message: 'Tối đa 10 khách'),
        ]),
        BookingKey.companyTaxId: NeatValidators.when(
          () => _form.getField<bool>(BookingKey.requestVAT).value,
          NeatValidators.combine([
            NeatValidators.required(message: 'Mã số thuế bắt buộc khi yêu cầu xuất VAT'),
            NeatValidators.exactLength(10, message: 'Mã số thuế doanh nghiệp phải đúng 10 chữ số'),
            NeatValidators.numeric(message: 'Mã số thuế chỉ gồm các chữ số'),
          ]),
        ),
        BookingKey.tags: NeatValidators.combine([
          NeatValidators.minItems(1, message: 'Vui lòng chọn ít nhất 1 tiện ích yêu cầu'),
          NeatValidators.uniqueItems(message: 'Không được chọn trùng lặp tiện ích'),
        ]),
      },
    );
  }

  @override
  void dispose() {
    _form.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _form,
      builder: (context, _) {
        final checkIn = _form.getField<DateTime?>(BookingKey.checkInDate);
        final checkOut = _form.getField<DateTime?>(BookingKey.checkOutDate);
        final guests = _form.getField<int>(BookingKey.guestCount);
        final vat = _form.getField<bool>(BookingKey.requestVAT);
        final taxId = _form.getField<String>(BookingKey.companyTaxId);
        final tags = _form.getField<List<String>>(BookingKey.tags);

        return ListView(
          padding: const EdgeInsets.all(20),
          children: [
            _buildSectionHeader('Đặt phòng khách sạn & Cross-field', Icons.hotel_class_rounded),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ListTile(
                    contentPadding: EdgeInsets.zero,
                    title: const Text('Ngày nhận phòng', style: TextStyle(fontWeight: FontWeight.bold)),
                    subtitle: Text(
                      checkIn.value != null ? '${checkIn.value!.day}/${checkIn.value!.month}/${checkIn.value!.year}' : 'Chọn ngày',
                    ),
                    trailing: const Icon(Icons.calendar_today_rounded),
                    onTap: () async {
                      final picked = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now().add(const Duration(days: 1)),
                        firstDate: DateTime.now(),
                        lastDate: DateTime.now().add(const Duration(days: 365)),
                      );
                      if (picked != null) {
                        _form.setAndValidateField(BookingKey.checkInDate, picked);
                      }
                    },
                  ),
                ),
                Expanded(
                  child: ListTile(
                    contentPadding: EdgeInsets.zero,
                    title: const Text('Ngày trả phòng', style: TextStyle(fontWeight: FontWeight.bold)),
                    subtitle: Text(
                      checkOut.value != null ? '${checkOut.value!.day}/${checkOut.value!.month}/${checkOut.value!.year}' : 'Chọn ngày',
                    ),
                    trailing: const Icon(Icons.event_available_rounded),
                    onTap: () async {
                      final picked = await showDatePicker(
                        context: context,
                        initialDate: DateTime.now().add(const Duration(days: 2)),
                        firstDate: DateTime.now(),
                        lastDate: DateTime.now().add(const Duration(days: 365)),
                      );
                      if (picked != null) {
                        _form.setAndValidateField(BookingKey.checkOutDate, picked);
                      }
                    },
                  ),
                ),
              ],
            ),
            if (checkOut.isErrorVisible)
              Text(
                checkOut.error?.message ?? '',
                style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
              ),
            const SizedBox(height: 16),
            TextField(
              decoration: InputDecoration(
                labelText: 'Số lượng khách',
                prefixIcon: const Icon(Icons.group_outlined),
                errorText: guests.isErrorVisible ? guests.error?.message : null,
              ),
              keyboardType: TextInputType.number,
              controller: TextEditingController(text: guests.value.toString()),
              onChanged: (val) => _form.setField(BookingKey.guestCount, int.tryParse(val) ?? 1),
            ),
            const SizedBox(height: 16),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Yêu cầu xuất hóa đơn công ty (VAT)'),
              value: vat.value,
              onChanged: (val) {
                _form.setField(BookingKey.requestVAT, val);
                _form.validateField(BookingKey.companyTaxId);
              },
            ),
            if (vat.value) ...[
              const SizedBox(height: 8),
              TextField(
                decoration: InputDecoration(
                  labelText: 'Mã số thuế doanh nghiệp (10 số)',
                  prefixIcon: const Icon(Icons.receipt_long_rounded),
                  errorText: taxId.isErrorVisible ? taxId.error?.message : null,
                ),
                keyboardType: TextInputType.number,
                onChanged: (val) => _form.setField(BookingKey.companyTaxId, val),
              ),
            ],
            const SizedBox(height: 16),
            const Text('Tiện ích yêu cầu thêm (Chọn ít nhất 1):', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: ['Bữa sáng Buffet', 'Đưa đón sân bay', 'Phòng view biển', 'Late Check-out'].map((item) {
                final isSelected = tags.value.contains(item);
                return FilterChip(
                  label: Text(item),
                  selected: isSelected,
                  onSelected: (selected) {
                    final current = List<String>.from(tags.value);
                    if (selected) {
                      current.add(item);
                    } else {
                      current.remove(item);
                    }
                    _form.setAndValidateField(BookingKey.tags, current);
                  },
                );
              }).toList(),
            ),
            if (tags.isErrorVisible)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: Text(
                  tags.error?.message ?? '',
                  style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
                ),
              ),
            const SizedBox(height: 24),
            ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFF7C3AED),
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              ),
              onPressed: _form.submissionStatus.isSubmitting
                  ? null
                  : () async {
                      final success = await _form.submitForm(
                        onSubmit: (values) async {
                          await Future<void>.delayed(const Duration(seconds: 1));
                          if (context.mounted) {
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(
                                content: Text('🏨 Đặt phòng thành công!'),
                                backgroundColor: Color(0xFF7C3AED),
                              ),
                            );
                          }
                        },
                      );
                      if (!success && context.mounted) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(
                            content: Text('⚠️ Vui lòng hoàn tất thông tin đặt phòng'),
                            backgroundColor: Colors.redAccent,
                          ),
                        );
                      }
                    },
              child: _form.submissionStatus.isSubmitting
                  ? const CircularProgressIndicator(color: Colors.white)
                  : const Text('Hoàn Tất Đặt Phòng', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            ),
          ],
        );
      },
    );
  }
}

Widget _buildSectionHeader(String title, IconData icon) {
  return Row(
    children: [
      Icon(icon, color: const Color(0xFF4F46E5), size: 24),
      const SizedBox(width: 8),
      Text(
        title,
        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
    ],
  );
}
0
likes
0
points
739
downloads

Publisher

unverified uploader

Weekly Downloads

A clean, lightweight, type-safe form state management and validation library for Flutter & Dart.

Repository (GitHub)
View/report issues

Topics

#form #validation #form-validation #state-management #riverpod

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on neat_form