search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Validation

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:

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:

// 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:

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:

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:

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:

if (controller.hasError('email'))
  WText(controller.getError('email')!, className: 'text-red-500 text-sm'),

Custom Rules

Create custom rules by extending Rule:

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:

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:

{
  "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:

{
  "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.