# Forms

Magic provides a complete form handling system: `MagicFormData` for centralized form state, Wind form widgets for UI, `FormRequest` classes for structured validation and authorization, and the `ValidatesRequests` mixin for controller-side error management.

- [Introduction](#introduction)
- [MagicFormData](#magicformdata)
    - [Creating Form Data](#creating-form-data)
    - [Accessing Values](#accessing-values)
    - [Setting Values](#setting-values)
- [MagicForm Widget](#magicform-widget)
- [Form Inputs](#form-inputs)
    - [WFormInput](#wforminput)
    - [WFormCheckbox](#wformcheckbox)
    - [WFormSelect](#wformselect)
- [Form Validation](#form-validation)
- [Custom FormRequest Classes](#custom-form-request-classes)
    - [Validating Requests in Controllers](#validating-requests-in-controllers)
- [Submitting Forms](#submitting-forms)
- [Form Processing](#form-processing)
- [Error Management](#error-management)
- [Form Introspection](#form-introspection)
- [MagicFormObject](#magicformobject)
- [Complete Example](#complete-example)

## Introduction

Magic provides a powerful form handling system that combines the simplicity of Laravel's request handling with Flutter's form widgets. Forms are managed through `MagicFormData`, which centralizes form state, validation, and data extraction.

> [!TIP]
> You can generate a validated form request stub using the Magic CLI: `dart run magic:artisan make:request` (e.g., `dart run magic:artisan make:request StoreOrder`). The generated file at `lib/app/requests/store_order_request.dart` provides a structured place for your validation rules.

## MagicFormData

### Creating Form Data

Create a form data instance in your stateful view:

```dart
class LoginView extends MagicStatefulView<AuthController> {
  const LoginView({super.key});
  
  @override
  State<LoginView> createState() => _LoginViewState();
}

class _LoginViewState extends MagicStatefulViewState<AuthController, LoginView> {
  // Define form fields with initial values
  // String values create TextEditingControllers
  // bool values create ValueNotifiers
  late final form = MagicFormData({
    'email': '',
    'password': '',
    'remember_me': false,
  }, controller: controller);

  @override
  void onClose() => form.dispose();  // Always dispose
}
```

### Accessing Values

```dart
// Get string value
String email = form.get('email');

// Get typed value
bool? rememberMe = form.value<bool>('remember_me');
int? age = form.value<int>('age');

// Get TextEditingController for text fields
TextEditingController emailController = form['email'];
```

### Setting Values

```dart
// Set text field value
form.set('email', 'john@example.com');

// Set non-text field value (bool, list, etc.)
form.setValue('remember_me', true);
form.setValue('tags', ['flutter', 'magic']);
```

> [!NOTE]
> `set(field, value)` is for text fields (backed by `TextEditingController`).
> `setValue(field, value)` is for non-text fields (backed by `ValueNotifier`).

## MagicForm Widget

Wrap your form fields with `MagicForm` for automatic state management:

```dart
@override
Widget build(BuildContext context) {
  return MagicForm(
    formData: form,
    child: WDiv(
      className: 'flex flex-col gap-4',
      children: [
        WFormInput(
          controller: form['email'],
          label: trans('attributes.email'),
          validator: rules([Required(), Email()], field: 'email'),
        ),
        WFormInput(
          controller: form['password'],
          type: InputType.password,
          label: trans('attributes.password'),
          validator: rules([Required(), Min(8)], field: 'password'),
        ),
        WButton(
          onTap: _submit,
          child: WText(trans('auth.login')),
        ),
      ],
    ),
  );
}
```

`MagicForm` provides:
- Form key for validation
- Auto-validation mode when errors exist
- Automatic error state binding
- Server-side error display

## Form Inputs

### WFormInput

The primary text input widget with Wind UI styling:

```dart
WFormInput(
  controller: form['email'],
  label: trans('attributes.email'),
  placeholder: trans('fields.email_placeholder'),
  type: InputType.email,  // text, password, email, number, multiline
  validator: rules([Required(), Email()], field: 'email'),
  className: '''
    w-full bg-slate-900 border border-gray-700 rounded-lg p-3 text-white
    focus:border-primary focus:ring-2 focus:ring-primary/30
    error:border-red-500 error:ring-red-200
  ''',
  labelClassName: 'text-sm font-medium text-gray-300 mb-1',
  placeholderClassName: 'text-gray-400',
)
```

#### Input Types

| Type | Description |
|------|-------------|
| `InputType.text` | Standard text input (default) |
| `InputType.password` | Obscured password input |
| `InputType.email` | Email keyboard |
| `InputType.number` | Numeric keyboard |
| `InputType.phone` | Phone number keyboard |
| `InputType.multiline` | Multi-line text area |

### WFormCheckbox

Checkbox with validation support:

```dart
WFormCheckbox(
  value: form.value<bool>('accept_terms'),
  onChanged: (v) => form.setValue('accept_terms', v),
  label: WText(trans('auth.accept_terms')),
  validator: rules([Accepted()], field: 'accept_terms'),
  className: 'w-5 h-5 checked:bg-primary error:border-red-500',
)
```

### WFormSelect

Dropdown select with search support:

```dart
WFormSelect<String>(
  value: form.get('country'),
  options: [
    SelectOption(value: 'us', label: 'United States'),
    SelectOption(value: 'gb', label: 'United Kingdom'),
    SelectOption(value: 'tr', label: 'Turkey'),
  ],
  onChange: (v) => form.setValue('country', v),
  label: trans('attributes.country'),
  searchable: true,
  placeholder: trans('fields.select_country'),
  validator: rules([Required()], field: 'country'),
  className: 'w-full bg-slate-900 border border-gray-700 rounded-lg',
)
```

## Form Validation

### Client-Side Validation

Use the `rules()` helper for client-side validation:

```dart
WFormInput(
  controller: form['email'],
  validator: rules([Required(), Email()], field: 'email'),
)

WFormInput(
  controller: form['password_confirmation'],
  validator: rules([
    Required(),
    Same('password', valueGetter: () => form['password'].text),
  ], field: 'password_confirmation'),
)
```

To name a field in its messages, call `FormValidator.rules` with `attribute`. It wins over the global `attributes.<key>` and matches what a `FormRequest.attributes()` entry shows after submit:

```dart
WFormInput(
  validator: FormValidator.rules([Required()], field: 'url', attribute: 'Server address'),
)
```

### Server-Side Errors

Server-side validation errors are automatically displayed when using `ValidatesRequests` mixin in your controller:

```dart
class AuthController extends MagicController with ValidatesRequests {
  Future<void> register(Map<String, dynamic> data) async {
    clearErrors();  // Clear previous errors
    
    final response = await Http.post('/register', data: data);
    
    if (!response.successful) {
      handleApiError(response);  // Auto-populates field errors
    }
  }
}
```

## Custom FormRequest Classes

`FormRequest` collapses the authorize-prepare-validate ceremony into a single, reusable class. Subclass it, declare `rules()`, and optionally override `authorize()` for access checks and `prepared()` for input normalization.

```dart
import 'package:magic/magic.dart';

class StoreOrderRequest extends FormRequest {
  @override
  bool authorize() => Gate.allows('order.create');

  @override
  Map<String, dynamic> prepared(Map<String, dynamic> data) => {
    ...data,
    'status': 'pending',
  };

  @override
  Map<String, List<Rule>> rules() => {
    'product_id': [Required()],
    'quantity': [Required(), Min(1)],
    'note': [Max(500)],
  };
}
```

Pass the raw form data to `validate()` in the controller. It runs `authorize` first, then `prepared`, then the rules, and returns the cleaned data filtered to the keys declared in `rules()`:

```dart
class OrderController extends MagicController with MagicStateMixin<Order>, ValidatesRequests {
  Future<void> store(Map<String, dynamic> data) async {
    setLoading();
    clearErrors();

    try {
      final payload = StoreOrderRequest().validate(data);
      final response = await Http.post('/orders', data: payload);

      if (!response.successful) {
        handleApiError(response);
        return;
      }

      setSuccess(Order.fromMap(response.data));
      MagicRoute.to('/orders');
    } on AuthorizationException {
      setError('You are not authorized to create orders.');
    } on ValidationException catch (e) {
      validationErrors = Map.from(e.errors);
      notifyListeners();
      setEmpty();
    }
  }
}
```

Override `attributes()` to name a field for `:attribute` in this request's messages, ahead of the global `attributes.<key>` translation:

```dart
@override
Map<String, String> attributes() => {'product_id': 'Product'};
```

**Failure modes:**
- `authorize()` returns `false`: throws `AuthorizationException`
- A rule in `rules()` fails: throws `ValidationException` with a field-keyed error map

### Validating Requests in Controllers

The `ValidatesRequests` mixin adds Laravel-style validation directly to a controller. Import it from the public barrel `package:magic/magic.dart`; its implementation lives under `lib/src/concerns/` (not `lib/src/http/`), but app code never imports from `lib/src/` directly.

> [!IMPORTANT]
> Import `ValidatesRequests` from `package:magic/magic.dart`. The mixin lives in `lib/src/concerns/validates_requests.dart`. Do not import from `lib/src/http/`; the mixin is not there.

Apply the mixin with the `with` keyword on any `MagicController` subclass:

```dart
import 'package:magic/magic.dart';

class ProfileController extends MagicController with ValidatesRequests {
  Future<void> update(Map<String, dynamic> data) async {
    clearErrors(); // Clear previous field errors before re-submitting

    try {
      final validated = validate(data, {
        'name': [Required(), Min(2), Max(100)],
        'email': [Required(), Email()],
      });

      final response = await Http.put('/profile', data: validated);

      if (!response.successful) {
        handleApiError(response); // Populates validationErrors from 422 response
        return;
      }

      // Success path
    } on ValidationException {
      // validationErrors is already populated; notifyListeners() was called
      // The MagicForm widget switches to AutovalidateMode.always automatically
    }
  }
}
```

**How errors surface in `MagicFormData`:**

When the controller bound to a `MagicFormData` instance implements `ValidatesRequests`, field errors flow to the form automatically:

- `handleApiError(response)` calls `setErrorsFromResponse(response)`, which parses the Laravel `{"errors": {"field": ["msg"]}}` format and populates `validationErrors`.
- `MagicForm` detects `hasRelevantErrors` on the form data and switches `AutovalidateMode` to `always`, so server-side errors display inline next to each field.
- `MagicFormData` attaches listeners to each field controller so `clearFieldError(field)` fires as soon as the user types, removing stale errors one field at a time.

**Available mixin members:**

| Member | Description |
|--------|-------------|
| `validate(data, rules)` | Run rules; returns validated data or throws `ValidationException` |
| `setErrorsFromResponse(response)` | Populate field errors from a 422 `MagicResponse` |
| `handleApiError(response, {fallback})` | Combined handler: sets field errors for 422, generic error otherwise |
| `clearErrors()` | Remove all validation errors and notify listeners |
| `clearFieldError(field)` | Remove a single field's error and notify listeners |
| `hasError(field)` | Returns `true` if the field has an error |
| `getError(field)` | Returns the error message for a field, or `null` |
| `firstError` | The first error message in `validationErrors`, or `null` |
| `hasErrors` | `true` if any validation errors are present |
| `validationErrors` | The raw `Map<String, String>` of current errors |

## Submitting Forms

### Using validated()

The `validated()` method validates the form and returns data if valid:

```dart
void _submit() {
  final data = form.validated();
  
  if (data.isNotEmpty) {
    // Form is valid, submit to controller
    controller.register(data);
  }
  // If empty, validation failed (errors are shown automatically)
}
```

### Using validate()

For more control, use `validate()` separately:

```dart
void _submit() {
  if (form.validate()) {
    // Form is valid
    final data = {
      'email': form.get('email'),
      'password': form.get('password'),
      'remember_me': form.value<bool>('remember_me'),
    };
    controller.login(data);
  }
}
```

## Form Processing

The `process()` method wraps an async action with automatic loading state management. It sets `isProcessing` to `true` before execution and `false` after, regardless of success or failure.

### Basic Usage

```dart
void _submit() async {
  final data = form.validated();
  if (data.isEmpty) return;

  await form.process(() => controller.register(data));
}
```

### Preventing Double Submissions

`process()` throws a `StateError` if the form is already processing, preventing concurrent submissions:

```dart
void _submit() async {
  final data = form.validated();
  if (data.isEmpty) return;

  try {
    await form.process(() => controller.register(data));
  } on StateError {
    // Already processing, ignore
  }
}
```

### Processing-Aware UI

Use `isProcessing` for simple checks, or `processingListenable` for granular rebuilds:

```dart
// Simple: check in build
WButton(
  isLoading: form.isProcessing,
  onTap: _submit,
  child: WText(trans('common.save')),
)

// Efficient: rebuild only the button when processing state changes
MagicBuilder<bool>(
  listenable: form.processingListenable,
  builder: (isProcessing) => WButton(
    isLoading: isProcessing,
    onTap: _submit,
    child: WText(trans('common.save')),
  ),
)
```

> [!TIP]
> Prefer `form.isProcessing` over `controller.isLoading` when you need form-scoped loading state. This avoids full-page rebuilds and keeps loading indicators tied to the specific form being submitted.

## Error Management

### Clearing All Errors

Use `clearErrors()` on the controller to remove all validation errors at once:

```dart
class AuthController extends MagicController with ValidatesRequests {
  Future<void> register(Map<String, dynamic> data) async {
    clearErrors();  // Clear previous errors before new submission

    final response = await Http.post('/register', data: data);

    if (!response.successful) {
      handleApiError(response);
    }
  }
}
```

### Clearing a Single Field Error

Use `clearFieldError(field)` to remove the error for a specific field. This is called automatically when the user types in a text field or changes a non-text field value, thanks to `MagicFormData`'s built-in listeners:

```dart
// Automatic: MagicFormData auto-clears field errors on input change.
// No manual wiring needed for fields registered in MagicFormData.

// Manual: clear a specific field error explicitly
controller.clearFieldError('email');
```

> [!NOTE]
> `clearErrors()` and `clearFieldError()` live on the `ValidatesRequests` mixin, not on `MagicFormData`. They are accessed through the controller.

## Form Introspection

### fieldNames

The `fieldNames` getter returns a `Set<String>` of all registered field names (both text and non-text fields):

```dart
final form = MagicFormData({
  'name': '',
  'email': '',
  'accept_terms': false,
});

print(form.fieldNames); // {'name', 'email', 'accept_terms'}
```

### hasRelevantErrors

The `hasRelevantErrors` getter checks if the controller has validation errors that match fields in this form. This prevents cross-form error leakage when multiple forms share a controller:

```dart
if (form.hasRelevantErrors) {
  // At least one of this form's fields has a server-side error
}
```

## MagicFormObject

The pattern above (`MagicFormData` in a `State`, `ValidatesRequests` on the controller) is the right shape for a form that shares its controller with the rest of the screen. When a form is its own unit of work, unrelated to the screen's read state, `MagicFormObject` collapses `MagicFormData`, `ValidatesRequests`, `CollapsesIndexedErrorKeys`, and `RunsActions` into one class: a Livewire-style Form object.

```dart
class MonitorFormObject extends MagicFormObject {
  MonitorFormObject({this.editing});

  final Monitor? editing;

  @override
  Map<String, dynamic> get initial => {
    'name': editing?.name ?? '',
    'url': editing?.url ?? '',
  };

  @override
  FormRequest get request =>
      editing == null ? const StoreMonitorRequest() : const UpdateMonitorRequest();

  @override
  Future<bool> persist(Map<String, dynamic> validated) async {
    final monitor = editing ?? Monitor();
    monitor.fill(validated, strict: true);
    return monitor.save();
  }
}
```

Create ONE instance per `State` and dispose it from that `State`'s `onClose`:

```dart
class _MonitorFormViewState extends MagicStatefulViewState<MonitorFormController, MonitorFormView> {
  late final form = MonitorFormObject(editing: widget.editing);

  @override
  void onClose() => form.dispose();

  void _submit() => form.submit();
}
```

`submit()` clears stale errors, validates the current `data` against `request` (painting a failing client rule onto `data`'s fields and stopping before `persist` ever runs), then routes `persist` through `data.process` so `data.isProcessing` covers the write and a server-side `ValidationException` from `persist` is caught the same way a client-side failure is. Never register a `MagicFormObject` via `Magic.put`/`findOrPut`: it is scoped to a single screen's lifetime, and `onClose` asserts (in debug) that it never ended up in the container. See `doc/basics/actions.md` for the `RunsActions` half of the mix.

```bash
dart run magic:artisan make:form Monitor                          # -> MonitorFormObject
dart run magic:artisan make:form Monitor --request=StoreMonitorRequest
```

## Complete Example

```dart
class RegisterView extends MagicStatefulView<AuthController> {
  const RegisterView({super.key});
  
  @override
  State<RegisterView> createState() => _RegisterViewState();
}

class _RegisterViewState extends MagicStatefulViewState<AuthController, RegisterView> {
  late final form = MagicFormData({
    'name': '',
    'email': '',
    'password': '',
    'password_confirmation': '',
    'accept_terms': false,
  }, controller: controller);

  @override
  void onClose() => form.dispose();

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) => _buildForm(),
    );
  }
  
  Widget _buildForm() {
    return WDiv(
      className: 'max-w-md mx-auto p-6 bg-slate-900 rounded-xl',
      child: MagicForm(
        formData: form,
        child: WDiv(
          className: 'flex flex-col gap-4',
          children: [
            WText(trans('auth.register_title'), 
              className: 'text-2xl font-bold text-white text-center'),
            
            WFormInput(
              controller: form['name'],
              label: trans('attributes.name'),
              validator: rules([Required(), Min(2)], field: 'name'),
              className: _inputClass,
            ),
            
            WFormInput(
              controller: form['email'],
              type: InputType.email,
              label: trans('attributes.email'),
              validator: rules([Required(), Email()], field: 'email'),
              className: _inputClass,
            ),
            
            WFormInput(
              controller: form['password'],
              type: InputType.password,
              label: trans('attributes.password'),
              validator: rules([Required(), Min(8)], field: 'password'),
              className: _inputClass,
            ),
            
            WFormInput(
              controller: form['password_confirmation'],
              type: InputType.password,
              label: trans('attributes.password_confirmation'),
              validator: rules([
                Required(),
                Same('password', valueGetter: () => form['password'].text),
              ], field: 'password_confirmation'),
              className: _inputClass,
            ),
            
            WFormCheckbox(
              value: form.value<bool>('accept_terms'),
              onChanged: (v) => setState(() => form.setValue('accept_terms', v)),
              label: WText(trans('auth.accept_terms'), className: 'text-gray-300'),
              validator: rules([Accepted()], field: 'accept_terms'),
            ),
            
            SizedBox(height: 8),
            
            WButton(
              isLoading: controller.isLoading,
              onTap: _submit,
              className: 'w-full bg-primary p-4 rounded-lg',
              child: WText(trans('auth.register'), className: 'text-white text-center'),
            ),
          ],
        ),
      ),
    );
  }
  
  String get _inputClass => '''
    w-full bg-slate-800 border border-gray-700 rounded-lg p-3 text-white
    focus:border-primary error:border-red-500
  ''';
  
  void _submit() {
    final data = form.validated();
    if (data.isNotEmpty) {
      controller.register(data);
    }
  }
}
```
