form_shield 0.3.0
form_shield: ^0.3.0 copied to clipboard
A declarative, rule-based form validation library for Flutter apps. Supports async validation, custom validation logic, and more.
Form Shield #
A declarative, rule-based form validation library for Flutter apps, offering customizable rules and messages, seamless integration with Flutter forms, type safety, and chainable validation.
It provides a simple yet powerful way to define and apply validation logic to your form fields.
Features #
✨ Declarative Validation: Define validation rules in a clear, readable way.
🎨 Customizable: Easily tailor rules and error messages to your needs.
🤝 Flutter Integration: Works seamlessly with Flutter's Form
and TextFormField
widgets.
🔒 Type-Safe: Leverages Dart's type system for safer validation logic.
🔗 Chainable Rules: Combine multiple validation rules effortlessly.
📚 Comprehensive Built-in Rules: Includes common validation scenarios out-of-the-box (required, email, password, length, numeric range, phone, etc.).
🛠️ Extensible: Create your own custom validation rules by extending the base class.
Table of Contents #
- Installation
- Usage
- Available validation rules
- Creating your own validation rules
- Asynchronous validation rules
- Contributing
- License
Getting started #
Installation #
Add form_shield
to your pubspec.yaml
dependencies:
dependencies:
flutter:
sdk: flutter
form_shield: ^0.3.0
Then, run flutter pub get
.
Usage #
Basic usage #
Import the package:
import 'package:form_shield/form_shield.dart';
Wrap your TextFormField
(or other form fields) within a Form
widget and assign a GlobalKey<FormState>
. Use the Validator
class to attach rules to the validator
property of your fields:
import 'package:flutter/material.dart';
import 'package:form_shield/form_shield.dart';
class MyForm extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: Validator<String>([
RequiredRule(),
EmailRule(),
]),
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: Validator<String>([
RequiredRule(),
PasswordRule(),
]),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Form is valid, proceed
}
},
child: Text('Submit'),
),
],
),
);
}
}
Customizing error messages #
Validator<String>([
RequiredRule(errorMessage: 'Please enter your email address'),
EmailRule(errorMessage: 'Please enter a valid email address'),
])
Using multiple validation rules #
Validator<String>([
RequiredRule(),
MinLengthRule(8, errorMessage: 'Username must be at least 8 characters'),
MaxLengthRule(20, errorMessage: 'Username cannot exceed 20 characters'),
])
Custom validation rules #
Validator<String>([
RequiredRule(),
CustomRule(
validator: (value) => value != 'admin',
errorMessage: 'Username cannot be "admin"',
),
])
Dynamic custom validation #
Validator<String>([
DynamicCustomRule(
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a value';
}
if (value.contains(' ')) {
return 'No spaces allowed';
}
return null; // Validation passed
},
),
])
Validating numbers #
Validator<num>([
MinValueRule(18, errorMessage: 'You must be at least 18 years old'),
MaxValueRule(120, errorMessage: 'Please enter a valid age'),
])
Phone number validation #
// General phone validation
Validator<String>([
RequiredRule(),
PhoneRule(),
])
// Country-specific phone validation
Validator<String>([
RequiredRule(),
CountryPhoneRule(countryCode: 'CM'),
])
Password validation with options #
Validator<String>([
RequiredRule(),
PasswordRule(
options: PasswordOptions(
minLength: 10,
requireUppercase: true,
requireLowercase: true,
requireDigit: true,
requireSpecialChar: true,
),
errorMessage: 'Password does not meet security requirements',
),
])
Password confirmation #
final passwordController = TextEditingController();
// Password field
TextFormField(
controller: passwordController,
validator: Validator<String>([
RequiredRule(),
PasswordRule(),
]),
)
// Confirm password field
TextFormField(
validator: Validator<String>([
RequiredRule(),
PasswordMatchRule(
passwordGetter: () => passwordController.text,
errorMessage: 'Passwords do not match',
),
]),
)
Available validation rules #
RequiredRule
- Validates that a value is not null or emptyEmailRule
- Validates that a string is a valid email addressPasswordRule
- Validates that a string meets password requirementsPasswordMatchRule
- Validates that a string matches another stringLengthRule
- Validates that a string's length is within specified boundsMinLengthRule
- Validates that a string's length is at least a specified minimumMaxLengthRule
- Validates that a string's length is at most a specified maximumValueRule
- Validates that a numeric value is within specified boundsMinValueRule
- Validates that a numeric value is at least a specified minimumMaxValueRule
- Validates that a numeric value is at most a specified maximumPhoneRule
- Validates that a string is a valid phone numberCountryPhoneRule
- Validates that a string is a valid phone number for a specific countryUrlRule
- Validates that a string is a valid URLIPAddressRule
- Validates that a string is a valid IPv4 or IPv6 addressCreditCardRule
- Validates that a string is a valid credit card numberDateRangeRule
- Validates that a date is within a specified rangeCustomRule
- A validation rule that uses a custom function to validate valuesDynamicCustomRule
- A validation rule that uses a custom function to validate values and return a dynamic error message
Creating your own validation rules #
You can create your own validation rules by extending the ValidationRule
class:
class NoSpacesRule extends ValidationRule<String> {
const NoSpacesRule({
String errorMessage = 'No spaces allowed',
}) : super(errorMessage: errorMessage);
@override
ValidationResult validate(String? value) {
if (value == null || value.isEmpty) {
return const ValidationResult.success();
}
if (value.contains(' ')) {
return ValidationResult.error(errorMessage);
}
return const ValidationResult.success();
}
}
Then use it like any other validation rule:
Validator<String>([
RequiredRule(),
NoSpacesRule(),
])
Asynchronous validation rules #
Form Shield supports asynchronous validation for scenarios where validation requires network requests or other async operations (like checking username availability or email uniqueness).
You can create async validation rules by either:
- Extending the
ValidationRule
class and overriding thevalidateAsync
method - Extending the specialized
AsyncValidationRule
class
Example: Username availability checker
class UsernameAvailabilityRule extends ValidationRule<String> {
final Future<bool> Function(String username) _checkAvailability;
const UsernameAvailabilityRule({
required Future<bool> Function(String username) checkAvailability,
super.errorMessage = 'This username is already taken',
}) : _checkAvailability = checkAvailability;
@override
ValidationResult validate(String? value) {
// Perform synchronous validation first
if (value == null || value.isEmpty) {
return ValidationResult.error('Username cannot be empty');
}
return const ValidationResult.success();
}
@override
Future<ValidationResult> validateAsync(String? value) async {
// Run sync validation first
final syncResult = validate(value);
if (!syncResult.isValid) {
return syncResult;
}
try {
// Perform the async validation
final isAvailable = await _checkAvailability(value!);
if (isAvailable) {
return const ValidationResult.success();
} else {
return ValidationResult.error(errorMessage);
}
} catch (e) {
return ValidationResult.error('Error checking username availability: $e');
}
}
}
Using async validation in forms
When using async validation, you need to:
- Create a validator instance as a field in your state class
- Initialize it in
initState()
- Dispose of it in
dispose()
- Check both sync and async validation states before submitting
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
late final Validator<String> _usernameValidator;
@override
void initState() {
super.initState();
_usernameValidator = Validator<String>([
RequiredRule(),
UsernameAvailabilityRule(
checkAvailability: _checkUsernameAvailability,
),
], debounceDuration: Duration(milliseconds: 500));
}
@override
void dispose() {
_usernameController.dispose();
_usernameValidator.dispose(); // Important to prevent memory leaks
super.dispose();
}
Future<bool> _checkUsernameAvailability(String username) async {
// Simulate API call with delay
await Future.delayed(const Duration(seconds: 1));
final takenUsernames = ['admin', 'user', 'test'];
return !takenUsernames.contains(username.toLowerCase());
}
void _submitForm() {
if (_formKey.currentState!.validate() &&
!_usernameValidator.isValidating &&
_usernameValidator.isValid) {
// All validations passed, proceed with form submission
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _usernameController,
decoration: InputDecoration(labelText: 'Username'),
validator: _usernameValidator,
),
// Show async validation state
ValueListenableBuilder<AsyncValidationStateData>(
valueListenable: _usernameValidator.asyncState,
builder: (context, state, _) {
if (state.isValidating) {
return Text('Checking username availability...');
} else if (state.isValid == false) {
return Text(
state.errorMessage ?? 'Invalid username',
style: TextStyle(color: Colors.red),
);
} else if (state.isValid == true) {
return Text(
'Username is available',
style: TextStyle(color: Colors.green),
);
}
return SizedBox.shrink();
},
),
ElevatedButton(
onPressed: _submitForm,
child: Text('Submit'),
),
],
),
);
}
}
Debouncing async validation
Form Shield includes built-in debouncing for async validation to prevent excessive API calls during typing. You can customize the debounce duration:
Validator<String>([
RequiredRule(),
UsernameAvailabilityRule(checkAvailability: _checkUsername),
], debounceDuration: Duration(milliseconds: 800)) // Custom debounce time
Manually triggering async validation
You can manually trigger async validation using the validateAsync
method:
Future<void> _checkUsername() async {
final isValid = await _usernameValidator.validateAsync(
_usernameController.text,
debounceDuration: Duration.zero, // Optional: skip debouncing
);
if (isValid) {
// Username is valid and available
}
}
Contributing #
Contributions are welcome! Please feel free to submit issues, pull requests, or suggest improvements.
License #
This project is licensed under the MIT License - see the LICENSE file for details.