# Validation - [Introduction](#introduction) - [Quick Start](#quick-start) - [Defining Validation Rules](#defining-validation-rules) - [Available Rules](#available-rules) - [Server-Side Validation](#server-side-validation) - [Custom Rules](#custom-rules) - [Error Styling](#error-styling) - [Localization](#localization) ## Introduction Magic provides a powerful validation system that integrates seamlessly with Flutter forms. Define your validation rules in the View using the `rules()` helper, and your Controller receives only clean, pre-validated data. ### Key Features | Feature | Description | |---------|-------------| | **MagicForm Widget** | Automatic form state management and error handling | | **rules() Helper** | Concise rule definition with auto-injected controller | | **Server-Side Errors** | Automatic display of API 422 validation errors | | **Error Styling** | Wind UI's `error:` state prefix for styling | | **Localization** | Error messages resolved via `trans()` helper | ## Quick Start Use `MagicFormData` for Laravel-style form handling: ```dart class RegisterView extends MagicStatefulView { const RegisterView({super.key}); @override State createState() => _RegisterViewState(); } class _RegisterViewState extends MagicStatefulViewState { // Define form fields - types inferred from initial values 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 MagicForm( formData: form, child: WDiv( className: 'flex flex-col gap-4', children: [ WFormInput( controller: form['name'], label: trans('attributes.name'), validator: rules([Required(), Min(2)], field: 'name'), ), WFormInput( controller: form['email'], label: trans('attributes.email'), type: InputType.email, validator: rules([Required(), Email()], field: 'email'), ), WFormInput( controller: form['password'], label: trans('attributes.password'), type: InputType.password, validator: rules([Required(), Min(8)], field: 'password'), ), WFormInput( controller: form['password_confirmation'], label: trans('attributes.password_confirmation'), type: InputType.password, validator: rules([ Required(), Same('password', valueGetter: () => form['password'].text), ], field: 'password_confirmation'), ), WFormCheckbox( value: form.value('accept_terms'), onChanged: (v) => form.setValue('accept_terms', v), label: WText(trans('auth.accept_terms')), validator: rules([Accepted()], field: 'accept_terms'), ), WButton( isLoading: controller.isLoading, onTap: () { final data = form.validated(); if (data.isNotEmpty) { controller.register(data); } }, className: 'w-full bg-primary p-4 rounded-lg', child: WText(trans('auth.register'), className: 'text-white text-center'), ), ], ), ); } } ``` ## Defining Validation Rules ### The rules() Helper The `rules()` helper is available in all `MagicStatefulViewState` subclasses: ```dart // Full syntax validator: FormValidator.rules([Required()], field: 'email', controller: controller) // Shorthand (controller auto-injected) validator: rules([Required(), Email()], field: 'email') ``` ### Multiple Rules Combine multiple rules in an array: ```dart validator: rules([ Required(), Email(), Max(255), ], field: 'email') ``` Rules are evaluated in order. If any rule fails, validation stops and the error is displayed. ## Available Rules | Rule | Description | Example | |------|-------------|---------| | `Required()` | Field must not be empty | `[Required()]` | | `Email()` | Valid email format | `[Email()]` | | `Min(n)` | Minimum length/value | `[Min(8)]` | | `Max(n)` | Maximum length/value | `[Max(255)]` | | `Confirmed()` | Must match `{field}_confirmation` | `[Confirmed()]` | | `Same('field')` | Must match another field | `[Same('password', valueGetter: ...)]` | | `Accepted()` | Must be true/1/"yes"/"on" | `[Accepted()]` | ### Same Rule with ValueGetter For password confirmation, use the `valueGetter` parameter: ```dart WFormInput( controller: form['password_confirmation'], validator: rules([ Required(), Same('password', valueGetter: () => form['password'].text), ], field: 'password_confirmation'), ) ``` ## Server-Side Validation Magic automatically handles Laravel-style 422 validation errors from your API. ### Controller Setup Add the `ValidatesRequests` mixin to your controller: ```dart class AuthController extends MagicController with MagicStateMixin, ValidatesRequests { Future register(Map data) async { setLoading(); clearErrors(); // Clear previous validation errors final response = await Http.post('/register', data: data); if (response.successful) { setSuccess(true); MagicRoute.to('/dashboard'); } else { // Automatically populates field errors from 422 response handleApiError(response, fallback: 'Registration failed'); } } } ``` ### handleApiError Method The `handleApiError()` method handles different error types: | Error Type | Behavior | |------------|----------| | 422 Validation | Sets field-level errors, form shows errors | | 401 Unauthorized | Shows unauthorized message | | 500+ Server Error | Shows fallback error message | ### ValidatesRequests Methods | Method | Description | |--------|-------------| | `handleApiError(response)` | Handle any API error automatically | | `setErrorsFromResponse(response)` | Populate errors from 422 response | | `hasError('field')` | Check if a field has an error | | `getError('field')` | Get error message for a field | | `hasErrors` | Check if any errors exist | | `clearErrors()` | Clear all validation errors | ### Displaying Server Errors Server-side errors appear automatically under form fields. For manual display: ```dart if (controller.hasError('email')) WText(controller.getError('email')!, className: 'text-red-500 text-sm'), ``` ## Custom Rules Create custom rules by extending `Rule`: ```dart class StrongPassword extends Rule { @override bool passes(String attribute, dynamic value, Map data) { if (value is! String || value.isEmpty) return true; final hasUppercase = value.contains(RegExp(r'[A-Z]')); final hasLowercase = value.contains(RegExp(r'[a-z]')); final hasNumber = value.contains(RegExp(r'[0-9]')); final hasSpecial = value.contains(RegExp(r'[!@#$%^&*]')); return hasUppercase && hasLowercase && hasNumber && hasSpecial; } @override String message() => trans('validation.strong_password'); } // Usage validator: rules([Required(), StrongPassword()], field: 'password') ``` ## Error Styling Wind UI's `WFormInput` automatically adds the `error` state when validation fails: ```dart WFormInput( controller: form['email'], className: ''' p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 error:border-red-500 error:ring-red-200 ''', validator: rules([Required(), Email()], field: 'email'), ) ``` The `error:` prefix applies styles when the field has validation errors. ## Localization ### Validation Messages Define validation messages in your language files: ```json { "validation": { "required": "The :attribute field is required.", "email": "The :attribute must be a valid email address.", "min": { "string": "The :attribute must be at least :min characters." }, "confirmed": "The :attribute confirmation does not match.", "accepted": "The :attribute must be accepted.", "strong_password": "The :attribute must contain uppercase, lowercase, number, and special character." } } ``` ### Attribute Names Define user-friendly field names: ```json { "attributes": { "email": "email address", "password": "password", "password_confirmation": "password confirmation", "accept_terms": "terms and conditions" } } ``` The `:attribute` placeholder is replaced with the localized field name.