diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..11d127b9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,53 @@
+---
+name: "\U0001F41B Bug"
+about: Something is crashing or not working as intended
+labels: bug
+
+---
+
+## Environment
+
+**Package version:**
+
+
+ Flutter doctor
+
+
+```
+```
+
+
+
+
+ Code sample
+
+
+
+```dart
+```
+
+
+
+## Description
+
+**Expected behavior:**
+
+**Current behavior:**
+
+## Steps to reproduce
+
+1. This
+2. Than that
+3. Then
+
+## Images
+
+## Stacktrace/Logcat
diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md
new file mode 100644
index 00000000..b243d471
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.md
@@ -0,0 +1,18 @@
+---
+name: "\U0001F4C3 Documentation Bug"
+about: You want to report something that is wrong or missing from the documentation.
+labels: documentation
+
+---
+
+### Describe the change you would like to see
+
+
+### How would the suggested change make the documentation more useful?
+
+
+### Additional context
+
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..a6b3c866
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,18 @@
+---
+name: "\U0001F680 Feature request"
+about: Suggest new feature or request for this project
+labels: enhancement
+
+---
+
+## Environment
+
+**Package version:**
+
+## Description
+
+**What you'd like to happen:**
+
+**Alternatives you've considered:**
+
+**Images:**
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 00000000..0e9a57f1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,14 @@
+---
+name: "\U0001F914 Questions and Help"
+about: You have a quetion or need help using this packages
+labels: question
+
+---
+
+## Environment
+
+**Package version:**
+
+## Describe your question
+
\ No newline at end of file
diff --git a/.github/PULL_REQUEST_TEMPLATE.MD b/.github/PULL_REQUEST_TEMPLATE.MD
new file mode 100644
index 00000000..95ff2b04
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.MD
@@ -0,0 +1,20 @@
+## Connection with issue(s)
+
+
+Close #???
+
+
+Connected to #???
+
+## Solution description
+
+## Screenshots or Videos
+
+
+
+## To Do
+
+- [ ] Check the original issue to confirm it is fully satisfied
+- [ ] Add solution description to help guide reviewers
+- [ ] Add unit test to verify new or fixed behaviour
+- [ ] If apply, add documentation to code properties and package readme
\ No newline at end of file
diff --git a/.github/workflows/reactive_forms.yaml b/.github/workflows/reactive_forms.yaml
index 938bbb71..15b08303 100644
--- a/.github/workflows/reactive_forms.yaml
+++ b/.github/workflows/reactive_forms.yaml
@@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
- flutter-version: "3.0.0"
+ flutter-version: "3.16.0"
channel: "stable"
- run: flutter pub get
- run: flutter test --no-pub --coverage
@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
- flutter-version: "3.0.0"
+ flutter-version: "3.16.0"
channel: "stable"
- run: flutter pub get
- name: Analyze lib
@@ -49,13 +49,13 @@ jobs:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
- flutter-version: "3.0.0"
+ flutter-version: "3.16.0"
channel: "stable"
- run: flutter pub get
- name: Format lib
- run: flutter format lib --set-exit-if-changed
+ run: dart format lib --set-exit-if-changed
- name: Format test
- run: flutter format test --set-exit-if-changed
+ run: dart format test --set-exit-if-changed
publish-warnings:
name: Publish warnings
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 240be932..a731f1f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,100 @@
+# 17.0.0
+
+## Breaking changes
+
+- Removed deprecated `onWillPop` from `ReactiveForm` and `ReactiveFormBuilder` widgets.
+It was replaced with the `PopScope` widget.
+- `Validators.number` allows now to define negative numbers and decimal numbers with the addition
+of two optional arguments `allowNegatives` and `allowedDecimals`.
+
+## Features
+
+- Add `canPop` and `onPopInvoked` to `ReactiveForm` and `ReactiveFormBuilder` widgets.
+
+# 16.1.1
+
+## Fixes
+
+- Add default Context Menu to `ReactiveTextField`.
+
+# 16.1.0
+
+## Features
+
+- Add `markAsPending()` method to `AbstractControl` to allow set the status
+to PENDING by demand.
+
+# 16.0.4
+
+## Fixes
+
+- Add missing properties to `ReactiveSwitchListTile.adaptative()` widget.
+- Add `showError()` to `ReactiveCheckbox` and `ReactiveCheckboxListTile` widgets. This does not
+display any error messages but it is now compatible with the Flutter builtin behavior of Checkboxes
+when Material 3 is enabled (`ThemeData(useMaterial3: true)`) in the active App Theme.
+
+## Enhances
+
+- Update `Readme.md` file with testing examples in the section
+`ReactiveForm vs ReactiveFormBuilder which one?`
+
+# 16.0.3
+
+## Fixes
+
+- Fix an issue with `FormGroup` and `FormArray` were recursive `Raw Value` was not working properly.
+
+# 16.0.2
+
+## Fixes
+
+- Fix an issue with `FormBuilder` when trying to build a control with a nullable '?' data type.
+- Fix an issue with `FormGroup` that was not triggering the event `collectionChanges` when a control
+is removed.
+- Fix an issue with `FormGroup` and `FormArray` when trying to find a control with a nullable '?'
+data type.
+
+# 16.0.1
+
+## Fixes
+
+- Update the intl dependency version, because in flutter 3.10,
+`flutter_localizations` depends on intl 0.18.0.
+
+# 16.0.0
+
+## Breaking Changes
+
+- Flutter >= 3.10 required for this version.
+
+## Fixes
+
+- Updated some documentation.
+- Expose validator classes to allow direct instantiation.
+
+# 15.0.0
+
+## Breaking Changes
+
+- All validators have been changed to classes with `const` constructors.
+- The Asynchronous Validator is now a class from where any custom async validator can inherit.
+
+## Features
+
+- A new validator `Validators.delegate(...)` has been introduced to be used with a custom
+ validation function.
+- A new validator `Validators.delegateAsync(...)` has been introduced to be used with a custom
+ async validation functions.
+
+# 14.3.0
+
+- Fix the inkwell ripple effect in the **ReactiveDropdownField**.
+- Add some other minor fixes.
+
+# 14.2.0
+
+- Update intl to latest version 0.18.0.
+
# 14.1.0
## Enhances
diff --git a/README.md b/README.md
index 697e5b4d..b9eaa6a8 100644
--- a/README.md
+++ b/README.md
@@ -64,8 +64,8 @@ samples, guidance on mobile development, and a full API reference.
## Minimum Requirements
-- Dart SDK: >= 2.17.0 <3.0.0
-- Flutter: >= 3.0.0
+- Dart SDK: >=3.2.0 <4.0.0
+- Flutter: >=3.16.0
> For using **Reactive Forms** in projects below Flutter 2.8.0 please use the version <= 10.7.0 of
> **Reactive Forms**.
@@ -88,7 +88,7 @@ dependencies:
flutter:
sdk: flutter
- reactive_forms: ^14.1.0
+ reactive_forms: ^17.0.0
```
Then run the command `flutter packages get` on the console.
@@ -209,22 +209,67 @@ There are common predefined validators, but you can implement custom validators
### Custom Validators
-A custom **FormControl** validator is a function that receives the _control_ to validate and returns a **Map**. If the value of the _control_ is valid the function must returns **null** otherwise returns a **Map** with a key and custom information, in the previous example we just set **true** as custom information.
+All validators are instances of classes that inherit from the `Validator` abstract class.
+In order to implement a custom validator you can follow two different approaches:
-Let's implement a custom validator that validates a control's value must be _true_:
+1- Extend from `Validator` class and override the `validate` method.
+2- Or implement a custom validator function|method, and use it with the `Validators.delegate(...)` validator.
+
+Let's implement a custom validator that validates a control's value must be `true`:
+
+### Inheriting from `Validator` class:
+
+Let's create a class that extends from `Validator` and overrides the `validate` method:
+
+```dart
+/// Validator that validates the control's value must be `true`.
+class RequiredTrueValidator extends Validator {
+ const RequiredTrueValidator() : super();
+
+ @override
+ Map? validate(AbstractControl control) {
+ return control.isNotNull &&
+ control.value is bool &&
+ control.value == true
+ ? null
+ : {'requiredTrue': true};
+ }
+}
+```
+
+The `validator` method is a function that receives the _control_ to validate and returns a `Map`. If the value of the _control_ is valid the function returns `null`, otherwise returns a `Map` with the error key and a custom information. In the previous example we have defined `requiredTrue` as the error key and `true` as the custom information.
+
+In order to use the new validator class we provide an instance of it in the FormControl definition.
```dart
final form = FormGroup({
'acceptLicense': FormControl(
value: false,
- validators: [_requiredTrue], // custom validator
+ validators: [
+ RequiredTrueValidator(), // providing the new custom validator
+ ],
),
});
```
+### Using the `Validators.delegate()` validator:
+
+Sometimes it's more convenient to implement a custom validator in a separate method|function than in a different new class. In that case, it is necessary to use the `Validators.delegate()` validator. It creates a validator that delegates the validation to the external function|method.
+
```dart
-/// Validates that control's value must be `true`
-Map _requiredTrue(AbstractControl control) {
+final form = FormGroup({
+ 'acceptLicense': FormControl(
+ value: false,
+ validators: [
+ Validators.delegate(_requiredTrue) // delegates validation to a custom function
+ ],
+ ),
+});
+```
+
+```dart
+/// Custom function that validates that control's value must be `true`.
+Map? _requiredTrue(AbstractControl control) {
return control.isNotNull &&
control.value is bool &&
control.value == true
@@ -233,7 +278,7 @@ Map _requiredTrue(AbstractControl control) {
}
```
-> You can see the current implementation of predefined validators in the source code to see more examples.
+> Check the [Migration Guide](https://github.com/joanpablo/reactive_forms/wiki/Migration-Guide/_edit#breaking-changes-in-15x) to learn more about custom validators after version 15.0.0 of the package.
### Pattern Validator
@@ -281,7 +326,7 @@ There are special validators that can be attached to **FormGroup**. In the next
There are some cases where we want to implement a Form where a validation of a field depends on the value of another field. For example a sign-up form with _email_ and _emailConfirmation_ or _password_ and _passwordConfirmation_.
-For that cases we could implement a custom validator and attach it to the **FormGroup**, let's see an example:
+For those cases we could implement a custom validator as a class and attach it to the **FormGroup**. Let's see an example:
```dart
final form = FormGroup({
@@ -293,7 +338,7 @@ final form = FormGroup({
]),
'passwordConfirmation': FormControl(),
}, validators: [
- _mustMatch('password', 'passwordConfirmation')
+ MustMatchValidator(controlName: 'password', matchingControlName: 'passwordConfirmation')
]);
```
@@ -304,8 +349,17 @@ In the previous code we have added two more fields to the form: _password_ and _
However the most important thing here is that we have attached a **validator** to the **FormGroup**. This validator is a custom validator and the implementation follows as:
```dart
-ValidatorFunction _mustMatch(String controlName, String matchingControlName) {
- return (AbstractControl control) {
+class MustMatchValidator extends Validator {
+ final String controlName;
+ final String matchingControlName;
+
+ MustMatchValidator({
+ required this.controlName,
+ required this.matchingControlName,
+ }) : super();
+
+ @override
+ Map? validate(AbstractControl control) {
final form = control as FormGroup;
final formControl = form.control(controlName);
@@ -321,7 +375,7 @@ ValidatorFunction _mustMatch(String controlName, String matchingControlName) {
}
return null;
- };
+ }
}
```
@@ -344,7 +398,7 @@ final form = FormGroup({
Some times you want to perform a validation against a remote server, this operations are more time consuming and need to be done asynchronously.
-For example you want to validate that the _email_ the user is currently typing in a _registration form_ is unique and is not already used in your application. **Asynchronous Validators** are just another tool so use it wisely.
+For example you want to validate that the _email_ the user is currently typing in a _registration form_ is unique and is not already used in your application. **Asynchronous Validators** are just another tool so use them wisely.
**Asynchronous Validators** are very similar to their synchronous counterparts, with the following difference:
@@ -365,7 +419,9 @@ final form = FormGroup({
Validators.required, // traditional required and email validators
Validators.email,
],
- asyncValidators: [_uniqueEmail], // custom asynchronous validator :)
+ asyncValidators: [
+ UniqueEmailAsyncValidator(), // custom asynchronous validator :)
+ ],
),
});
```
@@ -373,34 +429,44 @@ final form = FormGroup({
We have declared a simple **Form** with an email **field** that is _required_ and must have a valid email value, and we have include a custom async validator that will validate if the email is unique. Let's see the implementation of our new async validator:
```dart
-/// just a simple array to simulate a database of emails in a server
-const inUseEmails = ['johndoe@email.com', 'john@email.com'];
-
-/// Async validator example that simulates a request to a server
-/// and validates if the email of the user is unique.
-Future> _uniqueEmail(AbstractControl control) async {
- final error = {'unique': false};
+/// Validator that validates the user's email is unique, sending a request to
+/// the Server.
+class UniqueEmailAsyncValidator extends AsyncValidator {
+ @override
+ Future?> validate(AbstractControl control) async {
+ final error = {'unique': false};
- final emailAlreadyUsed = await Future.delayed(
- Duration(seconds: 5), // a delay to simulate a time consuming operation
- () => inUseEmails.contains(control.value),
- );
+ final isUniqueEmail = await _getIsUniqueEmail(control.value.toString());
+ if (!isUniqueEmail) {
+ control.markAsTouched();
+ return error;
+ }
- if (emailAlreadyUsed) {
- control.markAsTouched();
- return error;
+ return null;
}
- return null;
+ /// Simulates a time consuming operation (i.e. a Server request)
+ Future _getIsUniqueEmail(String email) {
+ // simple array that simulates emails stored in the Server DB.
+ final storedEmails = ['johndoe@email.com', 'john@email.com'];
+
+ return Future.delayed(
+ const Duration(seconds: 5),
+ () => !storedEmails.contains(email),
+ );
+ }
}
```
> Note the use of **control.markAsTouched()** to force the validation message to show up as soon as possible.
-The previous implementation was a simple function that receives the **AbstractControl** and returns a [Future](https://api.dart.dev/stable/dart-async/Future-class.html) that completes 5 seconds after its call and performs a simple check: if the _value_ of the _control_ is contained in the _server_ array of emails.
+The previous implementation was a simple validator that receives the **AbstractControl** and returns a [Future](https://api.dart.dev/stable/dart-async/Future-class.html) that completes 5 seconds after its call and performs a simple check: if the _value_ of the _control_ is contained in the _server_ array of emails.
> If you want to see **Async Validators** in action with a **full example** using widgets and animations to feedback the user we strong advice you to visit our [Wiki](https://github.com/joanpablo/reactive_forms/wiki/Asynchronous-Validators). We have not included the full example in this README.md file just to simplify things here and to not anticipate things that we will see later in this doc.
+> The validator `Validators.delegateAsync()` is another way to implement custom validator, for more reference
+> check the [Custom validators](https://github.com/joanpablo/reactive_forms#custom-validators) section.
+
### Debounce time in async validators
Asynchronous validators have a debounce time that is useful if you want to minimize requests to a remote API. The debounce time is set in milliseconds and the default value is 250 milliseconds.
@@ -409,7 +475,7 @@ You can set a different debounce time as an optionally argument in the **FormCon
```dart
final control = FormControl(
- asyncValidators: [_uniqueEmail],
+ asyncValidators: [UniqueEmailAsyncValidator()],
asyncValidatorsDebounceTime: 1000, // sets 1 second of debounce time.
);
```
@@ -973,7 +1039,7 @@ set name(String newName) {
> form.markAllAsTouched();
> ```
-### Overriding Reactive Widgets _show errors_ behavior
+### Overriding Reactive Widgets show errors behavior
The second way to customize when to show error messages is to override the method **showErrors** in reactive widgets.
@@ -1505,6 +1571,86 @@ You should use **ReactiveFormBuilder** if:
But the final decision is really up to you, you can use any of them in any situations ;)
+## Widget testing
+
+**note: mark your fields with `Key`'s for easy access via widget tester**
+
+### example component
+
+```dart
+class LoginForm extends StatefulWidget {
+ const LoginForm({Key? key}) : super(key: key);
+
+ @override
+ LoginFormState createState() => LoginFormState();
+}
+
+class LoginFormState extends State {
+ final form = FormGroup({
+ 'email': FormControl(validators: [Validators.required, Validators.email]),
+ 'password': FormControl(validators: [Validators.required]),
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return ReactiveForm(
+ formGroup: form,
+ child: Column(
+ children: [
+ ReactiveTextField(
+ key: const Key('email'),
+ formControlName: 'email',
+ ),
+ ReactiveTextField(
+ key: const Key('password'),
+ formControlName: 'password',
+ obscureText: true,
+ ),
+ ElevatedButton(
+ key: const Key('submit'),
+ onPressed: () {},
+ child: const Text('Submit'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+```
+
+### example test
+
+```dart
+
+void main() {
+ testWidgets('LoginForm should pass with correct values', (tester) async {
+ // Build the widget.
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(body: LoginForm()),
+ ));
+
+ await tester.enterText(find.byKey(const Key('email')), 'etc@test.qa');
+ await tester.enterText(find.byKey(const Key('password')), 'password');
+
+ await tester.tap(find.byKey(const Key('submit')));
+
+ await tester.pump();
+
+ // Expect to find the item on screen if needed
+ expect(find.text('etc@test.qa'), findsOneWidget);
+
+ // Get form state
+ final LoginFormState loginFormState = tester.state(find.byType(LoginForm));
+
+ // Check form state
+ expect(loginFormState.form.valid, true);
+ });
+}
+
+```
+
+
+
## Reactive Forms + [Provider](https://pub.dev/packages/provider) plugin :muscle:
Although **Reactive Forms** can be used with any state management library or even without any one at all, **Reactive Forms** gets its maximum potential when is used in combination with a state management library like the [Provider](https://pub.dev/packages/provider) plugin.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index d4fc3508..705fe1d0 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -2,11 +2,9 @@ include: package:lints/recommended.yaml
analyzer:
language:
+ strict-casts: true
strict-inference: true
strict-raw-types: true
- strong-mode:
- implicit-dynamic: false
- implicit-casts: false
linter:
rules:
- always_declare_return_types
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index f852a55f..6a21e162 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -39,7 +39,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.reactive_forms_example"
- minSdkVersion 16
+ minSdkVersion flutter.minSdkVersion
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/example/android/build.gradle b/example/android/build.gradle
index 2ed02f9a..b5ae9fa3 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
+tasks.register("clean", Delete) {
delete rootProject.buildDir
}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index b5954542..8717eda8 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -40,7 +40,14 @@ class ReactiveFormsApp extends StatelessWidget {
'uniqueEmail': (_) => 'This email is already in use',
},
child: MaterialApp(
- theme: customTheme,
+ theme: ThemeData(
+ useMaterial3: true,
+ inputDecorationTheme: const InputDecorationTheme(
+ border: OutlineInputBorder(),
+ floatingLabelBehavior: FloatingLabelBehavior.auto,
+ alignLabelWithHint: true,
+ ),
+ ),
routes: {
Routes.complex: (_) => ComplexSample(),
Routes.simple: (_) => SimpleSample(),
@@ -55,11 +62,3 @@ class ReactiveFormsApp extends StatelessWidget {
);
}
}
-
-final customTheme = ThemeData.light().copyWith(
- inputDecorationTheme: const InputDecorationTheme(
- border: OutlineInputBorder(),
- floatingLabelBehavior: FloatingLabelBehavior.auto,
- alignLabelWithHint: true,
- ),
-);
diff --git a/example/lib/sample_screen.dart b/example/lib/sample_screen.dart
index 8aff4871..a168e88b 100644
--- a/example/lib/sample_screen.dart
+++ b/example/lib/sample_screen.dart
@@ -5,8 +5,7 @@ class SampleScreen extends StatelessWidget {
final Widget body;
final Widget? title;
- const SampleScreen({Key? key, required this.body, this.title})
- : super(key: key);
+ const SampleScreen({super.key, required this.body, this.title});
@override
Widget build(BuildContext context) {
diff --git a/example/lib/samples/add_dynamic_controls_sample.dart b/example/lib/samples/add_dynamic_controls_sample.dart
index fc34097e..48ed0aea 100644
--- a/example/lib/samples/add_dynamic_controls_sample.dart
+++ b/example/lib/samples/add_dynamic_controls_sample.dart
@@ -7,8 +7,8 @@ class ViewModelProvider extends InheritedWidget {
ViewModelProvider({
required this.viewModel,
- required Widget child,
- }) : super(child: child);
+ required super.child,
+ });
static NewContactViewModel? of(BuildContext context) =>
context.findAncestorWidgetOfExactType()?.viewModel;
diff --git a/example/lib/samples/array_sample.dart b/example/lib/samples/array_sample.dart
index d05fa726..8c352ffb 100644
--- a/example/lib/samples/array_sample.dart
+++ b/example/lib/samples/array_sample.dart
@@ -10,7 +10,12 @@ class ArraySample extends StatefulWidget {
class _ArraySampleState extends State {
final contacts = ['john@email.com', 'susan@email.com', 'caroline@email.com'];
final form = FormGroup({
- 'selectedContacts': FormArray([], validators: [_emptyAddressee]),
+ 'selectedContacts': FormArray(
+ [],
+ validators: [
+ Validators.delegate(_emptyAddressee),
+ ],
+ ),
});
FormArray get selectedContacts =>
diff --git a/example/lib/samples/complex_sample.dart b/example/lib/samples/complex_sample.dart
index 45646d6c..1078a5b0 100644
--- a/example/lib/samples/complex_sample.dart
+++ b/example/lib/samples/complex_sample.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart' hide ProgressIndicator;
import 'package:reactive_forms/reactive_forms.dart';
import 'package:reactive_forms_example/progress_indicator.dart';
import 'package:reactive_forms_example/sample_screen.dart';
+import 'package:reactive_forms_example/samples/validators/unique_email_async_validator.dart';
class BooleanObject {
final String name;
@@ -13,7 +14,7 @@ class BooleanObject {
other is BooleanObject && name == other.name;
@override
- int get hashCode => hashValues(name, name);
+ int get hashCode => Object.hash(name, name);
}
final yes = BooleanObject('Yes');
@@ -23,7 +24,7 @@ class ComplexSample extends StatelessWidget {
FormGroup buildForm() => fb.group({
'email': FormControl(
validators: [Validators.required, Validators.email],
- asyncValidators: [_uniqueEmail],
+ asyncValidators: [UniqueEmailAsyncValidator()],
),
'password': ['', Validators.required, Validators.minLength(8)],
'passwordConfirmation': '',
@@ -48,6 +49,7 @@ class ComplexSample extends StatelessWidget {
form: buildForm,
builder: (context, form, child) {
return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ReactiveTextField(
formControlName: 'email',
@@ -257,25 +259,3 @@ class ComplexSample extends StatelessWidget {
);
}
}
-
-/// Async validator in use emails example
-const inUseEmails = ['johndoe@email.com', 'john@email.com'];
-
-/// Async validator example that simulates a request to a server
-/// to validate if the email of the user is unique.
-Future?> _uniqueEmail(
- AbstractControl control) async {
- final error = {'unique': false};
-
- final emailAlreadyInUse = await Future.delayed(
- const Duration(seconds: 5), // delay to simulate a time consuming operation
- () => inUseEmails.contains(control.value.toString()),
- );
-
- if (emailAlreadyInUse) {
- control.markAsTouched();
- return error;
- }
-
- return null;
-}
diff --git a/example/lib/samples/login_sample.dart b/example/lib/samples/login_sample.dart
index 95bb56a3..21aedec1 100644
--- a/example/lib/samples/login_sample.dart
+++ b/example/lib/samples/login_sample.dart
@@ -8,7 +8,10 @@ class LoginSample extends StatelessWidget {
validators: [Validators.required, Validators.email],
),
'password': ['', Validators.required, Validators.minLength(8)],
- 'rememberMe': false,
+ 'acceptTerms': FormControl(
+ value: false,
+ validators: [Validators.requiredTrue],
+ ),
});
@override
@@ -19,6 +22,7 @@ class LoginSample extends StatelessWidget {
form: buildForm,
builder: (context, form, child) {
return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ReactiveTextField(
formControlName: 'email',
@@ -55,11 +59,10 @@ class LoginSample extends StatelessWidget {
errorStyle: TextStyle(height: 0.7),
),
),
- Row(
- children: [
- ReactiveCheckbox(formControlName: 'rememberMe'),
- const Text('Remember me')
- ],
+ const SizedBox(height: 16.0),
+ ReactiveCheckboxListTile(
+ formControlName: 'acceptTerms',
+ title: const Text('Accept terms & conditions'),
),
const SizedBox(height: 16.0),
ElevatedButton(
@@ -72,12 +75,16 @@ class LoginSample extends StatelessWidget {
},
child: const Text('Sign Up'),
),
+ const SizedBox(height: 16.0),
ElevatedButton(
- onPressed: () => form.resetState({
- 'email': ControlState(value: null),
- 'password': ControlState(value: null),
- 'rememberMe': ControlState(value: false),
- }, removeFocus: true),
+ onPressed: () => form.resetState(
+ {
+ 'email': ControlState(value: null),
+ 'password': ControlState(value: null),
+ 'acceptTerms': ControlState(value: false),
+ },
+ removeFocus: true,
+ ),
child: const Text('Reset all'),
),
],
diff --git a/example/lib/samples/validators/unique_email_async_validator.dart b/example/lib/samples/validators/unique_email_async_validator.dart
new file mode 100644
index 00000000..69993bf8
--- /dev/null
+++ b/example/lib/samples/validators/unique_email_async_validator.dart
@@ -0,0 +1,30 @@
+import 'package:reactive_forms/reactive_forms.dart';
+
+/// Validator that validates the user's email is unique, sending a request to
+/// the Server.
+class UniqueEmailAsyncValidator extends AsyncValidator {
+ @override
+ Future?> validate(
+ AbstractControl control) async {
+ final error = {'unique': false};
+
+ final isUniqueEmail = await _getIsUniqueEmail(control.value.toString());
+ if (!isUniqueEmail) {
+ control.markAsTouched();
+ return error;
+ }
+
+ return null;
+ }
+
+ /// Simulates a time consuming operation (i.e. a Server request)
+ Future _getIsUniqueEmail(String email) {
+ // simple array that simulates emails stored in the Server DB.
+ final storedEmails = ['johndoe@email.com', 'john@email.com'];
+
+ return Future.delayed(
+ const Duration(seconds: 5),
+ () => !storedEmails.contains(email),
+ );
+ }
+}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index ba595993..850092e5 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -18,34 +18,30 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
- sdk: ">=2.17.0 <3.0.0"
- flutter: ">=3.0.0"
+ sdk: ">=3.2.0 <4.0.0"
+ flutter: ">=3.16.0"
dependencies:
flutter:
sdk: flutter
- intl: ^0.17.0
+ intl: ^0.19.0
reactive_forms:
path: ../
#reactive_forms_widgets: ^0.3.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
- cupertino_icons: ^1.0.5
+ cupertino_icons: ^1.0.6
#reactive_dropdown_search: ^0.9.0
#reactive_touch_spin: ^0.6.0
#reactive_segmented_control: ^0.4.0
#reactive_date_time_picker: ^0.4.0
dev_dependencies:
- lints: ^2.0.0
+ flutter_lints: ^3.0.2
flutter_test:
sdk: flutter
-dependency_overrides:
- reactive_forms:
- path: ../
-
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
diff --git a/lib/reactive_forms.dart b/lib/reactive_forms.dart
index f7420302..80c35905 100644
--- a/lib/reactive_forms.dart
+++ b/lib/reactive_forms.dart
@@ -18,11 +18,32 @@ export 'src/models/control_state.dart';
export 'src/models/control_status.dart';
export 'src/models/focus_controller.dart';
export 'src/models/form_builder.dart';
-export 'src/models/form_control_collection.dart';
export 'src/models/models.dart';
export 'src/utils/control_extensions.dart';
export 'src/utils/control_utils.dart';
+export 'src/validators/any_validator.dart';
+export 'src/validators/async_validator.dart';
export 'src/validators/compare_option.dart';
+export 'src/validators/compare_validator.dart';
+export 'src/validators/compose_or_validator.dart';
+export 'src/validators/compose_validator.dart';
+export 'src/validators/contains_validator.dart';
+export 'src/validators/credit_card_validator.dart';
+export 'src/validators/delegate_async_validator.dart';
+export 'src/validators/delegate_validator.dart';
+export 'src/validators/email_validator.dart';
+export 'src/validators/equals_validator.dart';
+export 'src/validators/max_length_validator.dart';
+export 'src/validators/max_validator.dart';
+export 'src/validators/min_length_validator.dart';
+export 'src/validators/min_validator.dart';
+export 'src/validators/must_match_validator.dart';
+export 'src/validators/number_validator.dart';
+export 'src/validators/pattern/default_pattern_evaluator.dart';
+export 'src/validators/pattern/pattern_evaluator.dart';
+export 'src/validators/pattern/regex_pattern_evaluator.dart';
+export 'src/validators/pattern_validator.dart';
+export 'src/validators/required_validator.dart';
export 'src/validators/validation_message.dart';
export 'src/validators/validator.dart';
export 'src/validators/validators.dart';
@@ -38,7 +59,6 @@ export 'src/widgets/inherited_streamer.dart';
export 'src/widgets/reactive_checkbox.dart';
export 'src/widgets/reactive_checkbox_list_tile.dart';
export 'src/widgets/reactive_date_picker.dart';
-export 'src/widgets/reactive_date_picker.dart';
export 'src/widgets/reactive_dropdown_field.dart';
export 'src/widgets/reactive_focusable_form_field.dart';
export 'src/widgets/reactive_form.dart';
diff --git a/lib/src/models/form_builder.dart b/lib/src/models/form_builder.dart
index 7c415b4a..f3f6352e 100644
--- a/lib/src/models/form_builder.dart
+++ b/lib/src/models/form_builder.dart
@@ -61,11 +61,11 @@ class FormBuilder {
/// ```
FormGroup group(
Map controls, [
- List validators = const [],
- List asyncValidators = const [],
+ List> validators = const [],
+ List> asyncValidators = const [],
]) {
final map = controls
- .map>((String key, Object value) {
+ .map>((String key, Object value) {
if (value is String) {
return MapEntry(key, FormControl(value: value));
} else if (value is int) {
@@ -78,39 +78,39 @@ class FormBuilder {
return MapEntry(key, FormControl(value: value));
} else if (value is TimeOfDay) {
return MapEntry(key, FormControl(value: value));
- } else if (value is AbstractControl) {
+ } else if (value is AbstractControl) {
return MapEntry(key, value);
- } else if (value is ValidatorFunction) {
- return MapEntry(key, FormControl(validators: [value]));
- } else if (value is List) {
- return MapEntry(key, FormControl(validators: value));
+ } else if (value is Validator) {
+ return MapEntry(key, FormControl(validators: [value]));
+ } else if (value is List>) {
+ return MapEntry(key, FormControl(validators: value));
} else if (value is List) {
if (value.isEmpty) {
- return MapEntry(key, FormControl());
+ return MapEntry(key, FormControl());
} else {
final defaultValue = value.first;
final validators = List.of(value.skip(1));
if (validators.isNotEmpty &&
- validators.any((validator) => validator is! ValidatorFunction)) {
+ validators.any((validator) => validator is! Validator)) {
throw FormBuilderInvalidInitializationException(
'Invalid validators initialization');
}
- if (defaultValue is ValidatorFunction) {
+ if (defaultValue is Validator) {
throw FormBuilderInvalidInitializationException(
'Expected first value in array to be default value of the control and not a validator.');
}
final effectiveValidators = validators
- .map((v) => v! as ValidatorFunction)
+ .map>((v) => v! as Validator)
.toList();
- final control = _control(defaultValue, effectiveValidators);
- return MapEntry(key, control as AbstractControl);
+
+ return MapEntry(key, _control(defaultValue, effectiveValidators));
}
}
- return MapEntry(key, FormControl(value: value));
+ return MapEntry(key, FormControl(value: value));
});
return FormGroup(
@@ -161,8 +161,8 @@ class FormBuilder {
/// ```
FormControl control(
T value, [
- List validators = const [],
- List asyncValidators = const [],
+ List> validators = const [],
+ List> asyncValidators = const [],
]) {
return FormControl(
value: value,
@@ -201,8 +201,8 @@ class FormBuilder {
///
FormArray array(
List value, [
- List validators = const [],
- List asyncValidators = const [],
+ List> validators = const [],
+ List> asyncValidators = const [],
]) {
return FormArray(
value.map>((v) {
@@ -221,7 +221,7 @@ class FormBuilder {
}
FormControl _control(
- dynamic value, List validators) {
+ dynamic value, List> validators) {
if (value is AbstractControl) {
throw FormBuilderInvalidInitializationException(
'Default value of control must not be an AbstractControl.');
@@ -241,7 +241,7 @@ class FormBuilder {
return FormControl(value: value);
}
- return FormControl(value: value, validators: validators);
+ return FormControl(value: value, validators: validators);
}
}
diff --git a/lib/src/models/form_control_collection.dart b/lib/src/models/form_control_collection.dart
deleted file mode 100644
index 31defe19..00000000
--- a/lib/src/models/form_control_collection.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2020 Joan Pablo Jimenez Milian. All rights reserved.
-// Use of this source code is governed by the MIT license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:flutter/foundation.dart';
-import 'package:reactive_forms/reactive_forms.dart';
-
-/// The base class form [FormGroup] and [FormArray].
-/// Its provides methods for get a control by name and a [Listenable]
-/// that emits events each time you add or remove a control to the collection.
-abstract class FormControlCollection {
- final _collectionChanges =
- StreamController>>.broadcast();
-
- /// Retrieves a child control given the control's [name] or path.
- ///
- /// The [name] is a dot-delimited string that define the path to the
- /// control.
- ///
- /// Throws [FormControlNotFoundException] if no control founded with
- /// the specified [name]/path.
- AbstractControl control(String name);
-
- /// Checks if collection contains a control by a given [name].
- ///
- /// Returns true if collection contains the control, otherwise returns false.
- bool contains(String name);
-
- /// Emits when a control is added or removed from collection.
- Stream>> get collectionChanges =>
- _collectionChanges.stream;
-
- /// Close stream that emit collection change events
- void closeCollectionEvents() {
- _collectionChanges.close();
- }
-
- /// Notify to listeners that the collection changed.
- ///
- /// This is for internal use only.
- @protected
- void emitsCollectionChanged(List> controls) {
- _collectionChanges.add(List.unmodifiable(controls));
- }
-
- /// Walks the [path] to find the matching control.
- ///
- /// Returns null if no match is found.
- AbstractControl? findControlInCollection(List path) {
- if (path.isEmpty) {
- return null;
- }
-
- final result = path.fold(this as AbstractControl, (control, name) {
- if (control is FormControlCollection) {
- final collection = control;
- return collection.contains(name) ? collection.control(name) : null;
- } else {
- return null;
- }
- });
-
- return result != null ? result as AbstractControl : null;
- }
-}
diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart
index 47b28395..9d809f32 100644
--- a/lib/src/models/models.dart
+++ b/lib/src/models/models.dart
@@ -22,9 +22,9 @@ abstract class AbstractControl {
final _statusChanges = StreamController.broadcast();
final _valueChanges = StreamController.broadcast();
final _touchChanges = StreamController.broadcast();
- final List _validators = [];
- final List _asyncValidators =
- [];
+ final List> _validators = >[];
+ final List> _asyncValidators =
+ >[];
StreamSubscription?>? _asyncValidationSubscription;
Map _errors = {};
@@ -47,8 +47,8 @@ abstract class AbstractControl {
/// Constructor of the [AbstractControl].
AbstractControl({
- List validators = const [],
- List asyncValidators = const [],
+ List> validators = const [],
+ List> asyncValidators = const [],
int asyncValidatorsDebounceTime = 250,
bool disabled = false,
bool touched = false,
@@ -89,8 +89,8 @@ abstract class AbstractControl {
///
/// In [FormGroup] these come in handy when you want to perform validation
/// that considers the value of more than one child control.
- List get validators =>
- List.unmodifiable(_validators);
+ List> get validators =>
+ List>.unmodifiable(_validators);
/// Sets the synchronous [validators] that are active on this control. Calling
/// this overwrites any existing sync validators.
@@ -111,7 +111,7 @@ abstract class AbstractControl {
/// This argument is only taking into account if [autoValidate] is equals to
/// `true`.
void setValidators(
- List validators, {
+ List> validators, {
bool autoValidate = false,
bool updateParent = true,
bool emitEvent = true,
@@ -137,8 +137,8 @@ abstract class AbstractControl {
///
/// In [FormGroup] these come in handy when you want to perform validation
/// that considers the value of more than one child control.
- List get asyncValidators =>
- List.unmodifiable(_asyncValidators);
+ List> get asyncValidators =>
+ List>.unmodifiable(_asyncValidators);
/// Sets the async [validators] that are active on this control. Calling this
/// overwrites any existing async validators.
@@ -159,7 +159,7 @@ abstract class AbstractControl {
/// This argument is only taking into account if [autoValidate] is equals to
/// `true`.
void setAsyncValidators(
- List validators, {
+ List> validators, {
bool autoValidate = false,
bool updateParent = true,
bool emitEvent = true,
@@ -228,6 +228,8 @@ abstract class AbstractControl {
/// * VALID: This control has passed all validation checks.
/// * INVALID: This control has failed at least one validation check.
/// * PENDING: This control is in the midst of conducting a validation check.
+ /// * DISABLED: This control is exempt from ancestor calculations of
+ /// validity or value.
///
/// These status values are mutually exclusive, so a control cannot be both
/// valid AND invalid or invalid AND pending.
@@ -420,6 +422,28 @@ abstract class AbstractControl {
_updateAncestors(updateParent);
}
+ /// Marks the control as `pending`.
+ ///
+ /// A control is pending while the control performs async validation.
+ ///
+ /// When [updateParent] is false, mark only this control. When true or not
+ /// supplied, marks all direct ancestors. Default is true.
+ ///
+ /// When [emitEvent] is true or not supplied (the default), a [statusChanged]
+ /// event is emitted.
+ ///
+ void markAsPending({bool updateParent = true, bool emitEvent = true}) {
+ _status = ControlStatus.pending;
+
+ if (emitEvent) {
+ this._statusChanges.add(_status);
+ }
+
+ if (updateParent) {
+ parent?.markAsPending(updateParent: updateParent, emitEvent: emitEvent);
+ }
+ }
+
/// Disposes the control
void dispose() {
_statusChanges.close();
@@ -602,7 +626,7 @@ abstract class AbstractControl {
Map _runValidators() {
final errors = {};
for (final validator in validators) {
- final error = validator(this);
+ final error = validator.validate(this);
if (error != null) {
errors.addAll(error);
}
@@ -676,8 +700,9 @@ abstract class AbstractControl {
_debounceTimer = Timer(
Duration(milliseconds: _asyncValidatorsDebounceTime),
() {
- final validatorsStream = Stream.fromFutures(
- asyncValidators.map((validator) => validator(this)).toList());
+ final validatorsStream = Stream.fromFutures(asyncValidators
+ .map((validator) => validator.validate(this))
+ .toList());
final asyncValidationErrors = {};
_asyncValidationSubscription = validatorsStream.listen(
@@ -808,18 +833,12 @@ class FormControl extends AbstractControl {
///
FormControl({
T? value,
- List validators = const [],
- List asyncValidators = const [],
- int asyncValidatorsDebounceTime = 250,
- bool touched = false,
- bool disabled = false,
- }) : super(
- validators: validators,
- asyncValidators: asyncValidators,
- asyncValidatorsDebounceTime: asyncValidatorsDebounceTime,
- disabled: disabled,
- touched: touched,
- ) {
+ super.validators,
+ super.asyncValidators,
+ super.asyncValidatorsDebounceTime,
+ super.touched,
+ super.disabled,
+ }) {
if (value != null) {
this.value = value;
} else {
@@ -1009,6 +1028,76 @@ class FormControl extends AbstractControl {
AbstractControl findControl(String path) => this;
}
+/// The base class form [FormGroup] and [FormArray].
+/// Its provides methods for get a control by name and a [Listenable]
+/// that emits events each time you add or remove a control to the collection.
+abstract class FormControlCollection extends AbstractControl {
+ FormControlCollection({
+ super.validators,
+ super.asyncValidators,
+ super.asyncValidatorsDebounceTime,
+ super.disabled,
+ });
+
+ final _collectionChanges =
+ StreamController>>.broadcast();
+
+ /// Retrieves a child control given the control's [name] or path.
+ ///
+ /// The [name] is a dot-delimited string that define the path to the
+ /// control.
+ ///
+ /// Throws [FormControlNotFoundException] if no control founded with
+ /// the specified [name]/path.
+ AbstractControl control(String name);
+
+ /// Checks if collection contains a control by a given [name].
+ ///
+ /// Returns true if collection contains the control, otherwise returns false.
+ bool contains(String name);
+
+ /// Gets the value of the control, including any disabled control.
+ ///
+ /// Retrieves all values regardless of disabled status of children controls.
+ T get rawValue;
+
+ /// Emits when a control is added or removed from collection.
+ Stream>> get collectionChanges =>
+ _collectionChanges.stream;
+
+ /// Close stream that emit collection change events
+ void closeCollectionEvents() {
+ _collectionChanges.close();
+ }
+
+ /// Notify to listeners that the collection changed.
+ ///
+ /// This is for internal use only.
+ @protected
+ void emitsCollectionChanged(List> controls) {
+ _collectionChanges.add(List.unmodifiable(controls));
+ }
+
+ /// Walks the [path] to find the matching control.
+ ///
+ /// Returns null if no match is found.
+ AbstractControl? findControlInCollection(List path) {
+ if (path.isEmpty) {
+ return null;
+ }
+
+ final result = path.fold?>(this, (control, name) {
+ if (control != null && control is FormControlCollection) {
+ return control.contains(name) ? control.control(name) : null;
+ } else {
+ return null;
+ }
+ });
+
+ return result;
+ }
+}
+
/// Tracks the value and validity state of a group of FormControl instances.
///
/// A FormGroup aggregates the values of each child FormControl into one object,
@@ -1017,8 +1106,7 @@ class FormControl extends AbstractControl {
/// It calculates its status by reducing the status values of its children.
/// For example, if one of the controls in a group is invalid, the entire group
/// becomes invalid.
-class FormGroup extends AbstractControl>
- with FormControlCollection {
+class FormGroup extends FormControlCollection> {
final Map> _controls = {};
/// Creates a new FormGroup instance.
@@ -1057,17 +1145,14 @@ class FormGroup extends AbstractControl>
/// See also [AbstractControl.validators]
FormGroup(
Map> controls, {
- List validators = const [],
- List asyncValidators = const [],
- int asyncValidatorsDebounceTime = 250,
+ super.validators,
+ super.asyncValidators,
+ super.asyncValidatorsDebounceTime,
bool disabled = false,
}) : assert(
!controls.keys.any((name) => name.contains(_controlNameDelimiter)),
'Control name should not contain dot($_controlNameDelimiter)'),
super(
- validators: validators,
- asyncValidators: asyncValidators,
- asyncValidatorsDebounceTime: asyncValidatorsDebounceTime,
disabled: disabled,
) {
addAll(controls);
@@ -1080,8 +1165,15 @@ class FormGroup extends AbstractControl>
/// Gets the value of the [FormGroup], including any disabled controls.
///
/// Retrieves all values regardless of disabled status.
- Map get rawValue => _controls
- .map((key, control) => MapEntry(key, control.value));
+ @override
+ Map get rawValue =>
+ _controls.map((key, control) {
+ if (control is FormControlCollection) {
+ return MapEntry(key, control.rawValue);
+ }
+
+ return MapEntry(key, control.value);
+ });
@override
bool contains(String name) {
@@ -1137,7 +1229,7 @@ class FormGroup extends AbstractControl>
Map> get controls =>
Map>.unmodifiable(_controls);
- /// Reduce the value of the group is a key-value pair for each control
+ /// Reduce the value of the group as a key-value pair for each control
/// in the group.
///
/// ### Example:
@@ -1496,6 +1588,8 @@ class FormGroup extends AbstractControl>
_controls.removeWhere((key, value) => key == name);
updateValueAndValidity(updateParent: updateParent, emitEvent: emitEvent);
+
+ emitsCollectionChanged(_controls.values.toList());
}
@override
@@ -1522,8 +1616,7 @@ class FormGroup extends AbstractControl>
///
/// FormArray is one of the three fundamental building blocks used to define
/// forms in Reactive Forms, along with [FormControl] and [FormGroup].
-class FormArray extends AbstractControl>
- with FormControlCollection {
+class FormArray extends FormControlCollection> {
final List> _controls = [];
/// Creates a new [FormArray] instance.
@@ -1563,14 +1656,11 @@ class FormArray extends AbstractControl>
/// See also [AbstractControl.validators]
FormArray(
List> controls, {
- List validators = const [],
- List asyncValidators = const [],
- int asyncValidatorsDebounceTime = 250,
+ super.validators,
+ super.asyncValidators,
+ super.asyncValidatorsDebounceTime,
bool disabled = false,
}) : super(
- validators: validators,
- asyncValidators: asyncValidators,
- asyncValidatorsDebounceTime: asyncValidatorsDebounceTime,
disabled: disabled,
) {
addAll(controls);
@@ -1587,8 +1677,14 @@ class FormArray extends AbstractControl>
/// Gets the value of the [FormArray], including any disabled controls.
///
/// Retrieves all values regardless of disabled status.
- List get rawValue =>
- _controls.map((control) => control.value).toList();
+ @override
+ List get rawValue => _controls.map((control) {
+ if (control is FormControlCollection) {
+ return (control as FormControlCollection).rawValue;
+ }
+
+ return control.value;
+ }).toList();
/// Sets the value of the [FormArray].
///
diff --git a/lib/src/validators/any_validator.dart b/lib/src/validators/any_validator.dart
index 9d686de5..83834651 100644
--- a/lib/src/validators/any_validator.dart
+++ b/lib/src/validators/any_validator.dart
@@ -15,7 +15,7 @@ class AnyValidator extends Validator {
/// Constructs an instance of the validator.
///
/// The argument [test] must not be null.
- AnyValidator(this.test);
+ const AnyValidator(this.test) : super();
@override
Map? validate(AbstractControl control) {
diff --git a/lib/src/validators/async_validator.dart b/lib/src/validators/async_validator.dart
new file mode 100644
index 00000000..7f0d8fd4
--- /dev/null
+++ b/lib/src/validators/async_validator.dart
@@ -0,0 +1,17 @@
+// Copyright 2020 Joan Pablo Jimenez Milian. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+import 'package:reactive_forms/reactive_forms.dart';
+
+/// An abstract class extended by classes that perform asynchronous validation.
+abstract class AsyncValidator {
+ const AsyncValidator();
+
+ /// Validates the [control].
+ Future?> validate(AbstractControl control);
+
+ Future?> call(AbstractControl control) {
+ return validate(control);
+ }
+}
diff --git a/lib/src/validators/compare_validator.dart b/lib/src/validators/compare_validator.dart
index 6f551b52..283dfdeb 100644
--- a/lib/src/validators/compare_validator.dart
+++ b/lib/src/validators/compare_validator.dart
@@ -14,11 +14,11 @@ class CompareValidator extends Validator {
///
/// The arguments [controlName], [compareControlName] and [compareOption]
/// must not be null.
- CompareValidator(
+ const CompareValidator(
this.controlName,
this.compareControlName,
this.compareOption,
- );
+ ) : super();
@override
Map? validate(AbstractControl control) {
diff --git a/lib/src/validators/compose_or_validator.dart b/lib/src/validators/compose_or_validator.dart
index 18c3833c..bd2bea8f 100644
--- a/lib/src/validators/compose_or_validator.dart
+++ b/lib/src/validators/compose_or_validator.dart
@@ -10,19 +10,19 @@ import 'package:reactive_forms/reactive_forms.dart';
/// returns 'null', otherwise returns the union of the individual error
/// maps returned by each validator.
class ComposeOrValidator extends Validator {
- final List validators;
+ final List> validators;
/// Constructs an instance of the validator.
///
/// The argument [validators] must not be null.
- ComposeOrValidator(this.validators);
+ const ComposeOrValidator(this.validators) : super();
@override
Map? validate(AbstractControl control) {
final composedError = {};
for (final validator in validators) {
- final error = validator(control);
+ final error = validator.validate(control);
if (error != null) {
composedError.addAll(error);
} else {
diff --git a/lib/src/validators/compose_validator.dart b/lib/src/validators/compose_validator.dart
index 2047fa5c..8f066e1a 100644
--- a/lib/src/validators/compose_validator.dart
+++ b/lib/src/validators/compose_validator.dart
@@ -10,19 +10,19 @@ import 'package:reactive_forms/reactive_forms.dart';
/// returns 'null', otherwise returns the union of the individual error
/// maps returned by each validator.
class ComposeValidator extends Validator {
- final List validators;
+ final List> validators;
/// Constructs an instance of the validator.
///
/// The argument [validators] must not be null.
- ComposeValidator(this.validators);
+ const ComposeValidator(this.validators) : super();
@override
Map? validate(AbstractControl control) {
final composedError = {};
for (final validator in validators) {
- final error = validator(control);
+ final error = validator.validate(control);
if (error != null) {
composedError.addAll(error);
}
diff --git a/lib/src/validators/contains_validator.dart b/lib/src/validators/contains_validator.dart
index bff198fe..ec627e7a 100644
--- a/lib/src/validators/contains_validator.dart
+++ b/lib/src/validators/contains_validator.dart
@@ -11,7 +11,7 @@ class ContainsValidator extends Validator {
/// Constructs the instance of the validator.
///
/// The argument [values] must not be null.
- ContainsValidator(this.values);
+ const ContainsValidator(this.values) : super();
@override
Map? validate(AbstractControl control) {
diff --git a/lib/src/validators/credit_card_validator.dart b/lib/src/validators/credit_card_validator.dart
index 43dfae66..a77ef808 100644
--- a/lib/src/validators/credit_card_validator.dart
+++ b/lib/src/validators/credit_card_validator.dart
@@ -3,11 +3,14 @@
// found in the LICENSE file.
import 'package:reactive_forms/reactive_forms.dart';
-import 'package:reactive_forms/src/validators/number_validator.dart';
/// A credit card validator that validates that the control's value is a valid
/// credit card.
class CreditCardValidator extends Validator {
+ static final RegExp onlyNumbersRegex = RegExp(r'^[0-9]+$');
+
+ const CreditCardValidator() : super();
+
@override
Map? validate(AbstractControl control) {
final error = {ValidationMessage.creditCard: true};
@@ -17,7 +20,7 @@ class CreditCardValidator extends Validator {
}
final cardNumber = control.value.toString().replaceAll(' ', '');
- final isNumber = NumberValidator.numberRegex.hasMatch(cardNumber);
+ final isNumber = onlyNumbersRegex.hasMatch(cardNumber);
return isNumber &&
cardNumber.length >= 13 &&
diff --git a/lib/src/validators/delegate_async_validator.dart b/lib/src/validators/delegate_async_validator.dart
new file mode 100644
index 00000000..20204314
--- /dev/null
+++ b/lib/src/validators/delegate_async_validator.dart
@@ -0,0 +1,28 @@
+// Copyright 2020 Joan Pablo Jimenez Milian. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+import 'package:reactive_forms/reactive_forms.dart';
+
+/// Signature of a function that receives a control and returns a Future
+/// that emits validation errors if present, otherwise null.
+typedef AsyncValidatorFunction = Future?> Function(
+ AbstractControl control);
+
+/// Validator that delegates the validation to an external function.
+class DelegateAsyncValidator extends AsyncValidator {
+ final AsyncValidatorFunction _asyncValidator;
+
+ /// Creates an instance of the [DelegateAsyncValidator] class.
+ ///
+ /// The [DelegateAsyncValidator] validator delegates the validation to the
+ /// external asynchronous [validator] function.
+ const DelegateAsyncValidator(AsyncValidatorFunction asyncValidator)
+ : _asyncValidator = asyncValidator,
+ super();
+
+ @override
+ Future?> validate(AbstractControl control) {
+ return _asyncValidator(control);
+ }
+}
diff --git a/lib/src/validators/delegate_validator.dart b/lib/src/validators/delegate_validator.dart
new file mode 100644
index 00000000..f0dcd0bb
--- /dev/null
+++ b/lib/src/validators/delegate_validator.dart
@@ -0,0 +1,28 @@
+// Copyright 2020 Joan Pablo Jimenez Milian. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+import 'package:reactive_forms/reactive_forms.dart';
+
+/// Signature of a function that receives a control and synchronously
+/// returns a map of validation errors if present, otherwise null.
+typedef ValidatorFunction = Map? Function(
+ AbstractControl control);
+
+/// Validator that delegates the validation to an external function.
+class DelegateValidator extends Validator {
+ final ValidatorFunction _validator;
+
+ /// Creates an instance of the [DelegateValidator] class.
+ ///
+ /// The [DelegateValidator] validator delegates the validation to the
+ /// external [validator] function.
+ const DelegateValidator(ValidatorFunction validator)
+ : _validator = validator,
+ super();
+
+ @override
+ Map? validate(AbstractControl control) {
+ return _validator(control);
+ }
+}
diff --git a/lib/src/validators/email_validator.dart b/lib/src/validators/email_validator.dart
index 2344e9df..c781440c 100644
--- a/lib/src/validators/email_validator.dart
+++ b/lib/src/validators/email_validator.dart
@@ -9,6 +9,8 @@ class EmailValidator extends Validator {
static final RegExp emailRegex = RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$');
+ const EmailValidator() : super();
+
@override
Map? validate(AbstractControl control) {
// don't validate empty values to allow optional controls
diff --git a/lib/src/validators/equals_validator.dart b/lib/src/validators/equals_validator.dart
index 2e19f4f2..68d4b826 100644
--- a/lib/src/validators/equals_validator.dart
+++ b/lib/src/validators/equals_validator.dart
@@ -16,10 +16,10 @@ class EqualsValidator