Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# Changelog

[0.2.2] - 2025-04-04
## [0.3.0] - 2025-04-05
### Added
- Asynchronous validation support:
- New `AsyncValidationRule` base class for creating async-specific validation rules
- Async validation state tracking with `AsyncValidationState` class
- Debouncing support for async validation to prevent excessive API calls
- Manual async validation triggering with `validateAsync()` method
- Example implementation of username availability checking
- New properties for validation state management:
- `isValidating` - Indicates if async validation is in progress
- `isValid` - Indicates if the last async validation was successful
- `errorMessage` - Provides the current error message from either sync or async validation

## [0.2.2] - 2025-04-04
### Changed
- Update documentation

Expand Down
196 changes: 192 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
<img src="https://raw.githubusercontent.com/stevenosse/form_shield/refs/heads/main/logo.svg" width="200" alt="Form Shield Logo">
</p>

[![pub version](https://img.shields.io/pub/v/form_shield.svg)](https://pub.dev/packages/form_shield)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![codecov](https://codecov.io/gh/stevenosse/form_shield/branch/main/graph/badge.svg)](https://codecov.io/gh/stevenosse/form_shield)
<p align="center">
<a href="https://pub.dev/packages/form_shield"><img src="https://img.shields.io/pub/v/form_shield.svg" alt="pub version"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license"></a>
<a href="https://codecov.io/gh/stevenosse/form_shield"><img src="https://codecov.io/gh/stevenosse/form_shield/branch/main/graph/badge.svg" alt="codecov"></a>
</p>


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.
Expand All @@ -23,6 +25,23 @@ It provides a simple yet powerful way to define and apply validation logic to yo
📚 **Comprehensive Built-in Rules:** Includes common validation scenarios out-of-the-box (required, email, password, length, numeric range, phone, etc.). <br />
🛠️ **Extensible:** Create your own custom validation rules by extending the base class. <br />

## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Basic usage](#basic-usage)
- [Customizing error messages](#customizing-error-messages)
- [Using multiple validation rules](#using-multiple-validation-rules)
- [Custom validation rules](#custom-validation-rules)
- [Dynamic custom validation](#dynamic-custom-validation)
- [Validating numbers](#validating-numbers)
- [Phone number validation](#phone-number-validation)
- [Password validation with options](#password-validation-with-options)
- [Password confirmation](#password-confirmation)
- [Available validation rules](#available-validation-rules)
- [Creating your own validation rules](#creating-your-own-validation-rules)
- [Contributing](#contributing)
- [License](#license)

## Getting started

### Installation
Expand All @@ -33,7 +52,7 @@ Add `form_shield` to your `pubspec.yaml` dependencies:
dependencies:
flutter:
sdk: flutter
form_shield: ^0.1.0
form_shield: ^0.3.0
```

Then, run `flutter pub get`.
Expand Down Expand Up @@ -265,6 +284,175 @@ Validator<String>([
])
```

### 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:
1. Extending the `ValidationRule` class and overriding the `validateAsync` method
2. Extending the specialized `AsyncValidationRule` class

#### Example: Username availability checker

```dart
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:
1. Create a validator instance as a field in your state class
2. Initialize it in `initState()`
3. Dispose of it in `dispose()`
4. Check both sync and async validation states before submitting

```dart
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:

```dart
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:

```dart
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.
Expand Down
45 changes: 45 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
30 changes: 30 additions & 0 deletions example/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "09de023485e95e6d1225c2baa44b8feb85e0d45f"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
- platform: web
create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Sample project for Flutter Shield
28 changes: 28 additions & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
Loading