Validation
- Introduction
- Quick Start
- Defining Validation Rules
- Available Rules
- Server-Side Validation
- Custom Rules
- Error Styling
- 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:
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.