Repository: joanpablo/reactive_forms

Issue:
Conditional Date Validation in Flutter
I need to specify start and end dates for an event with the following constraints:

- Start date field is mandatory.
- End date field is optional.
- If the end date field is populated, it must be greater than or equal to the start date.

Currently, using Validators.compare, I'm getting an error notification even if the end date is not specified.

Should I implement a custom validator?



Fix patch:
diff --git a/lib/src/validators/compare_validator.dart b/lib/src/validators/compare_validator.dart
index 283dfde..8ca75d6 100644
--- a/lib/src/validators/compare_validator.dart
+++ b/lib/src/validators/compare_validator.dart
@@ -9,16 +9,21 @@ class CompareValidator extends Validator<dynamic> {
   final String controlName;
   final String compareControlName;
   final CompareOption compareOption;
+  final bool allowNull;
 
   /// Constructs an instance of the validator.
   ///
   /// The arguments [controlName], [compareControlName] and [compareOption]
   /// must not be null.
+  ///
+  /// [allowNull] (optional): skip this validation if one of the controls has
+  /// null value. Defaults to false (report an error in case of null).
   const CompareValidator(
     this.controlName,
     this.compareControlName,
-    this.compareOption,
-  ) : super();
+    this.compareOption, {
+    this.allowNull = false,
+  }) : super();
 
   @override
   Map<String, dynamic>? validate(AbstractControl<dynamic> control) {
@@ -42,7 +47,11 @@ class CompareValidator extends Validator<dynamic> {
         compareControl.isNull) {
       return null;
     } else if (mainControl.isNull || compareControl.isNull) {
-      mainControl.setErrors(error);
+      if (allowNull) {
+        mainControl.removeError(ValidationMessage.compare);
+      } else {
+        mainControl.setErrors(error);
+      }
       return null;
     } else if (mainControl.value is! Comparable<dynamic>) {
       throw ValidatorException(
diff --git a/lib/src/validators/validators.dart b/lib/src/validators/validators.dart
index 7ef9926..983a16d 100644
--- a/lib/src/validators/validators.dart
+++ b/lib/src/validators/validators.dart
@@ -225,6 +225,9 @@ class Validators {
   /// The arguments [controlName], [compareControlName] and [compareOption]
   /// must not be null.
   ///
+  /// [allowNull] (optional): skip this validation if one of the controls has
+  /// null value. Defaults to false (report an error in case of null).
+  ///
   /// ## Example:
   /// Validates that 'amount' is lower or equals than 'balance'
   /// ```dart
@@ -236,9 +239,15 @@ class Validators {
   static Validator<dynamic> compare(
     String controlName,
     String compareControlName,
-    CompareOption compareOption,
-  ) {
-    return CompareValidator(controlName, compareControlName, compareOption);
+    CompareOption compareOption, {
+    bool allowNull = false,
+  }) {
+    return CompareValidator(
+      controlName,
+      compareControlName,
+      compareOption,
+      allowNull: allowNull,
+    );
   }
 
   /// Compose multiple validators into a single validator that returns the union


Test patch:
diff --git a/test/src/validators/compare_validator_test.dart b/test/src/validators/compare_validator_test.dart
index 92a14de..7bf9094 100644
--- a/test/src/validators/compare_validator_test.dart
+++ b/test/src/validators/compare_validator_test.dart
@@ -287,6 +287,75 @@ void main() {
       // Expect: form is invalid
       expect(form, throwsA(isInstanceOf<ValidatorException>()));
     });
+
+    test('Lower compare (allow null)', () {
+      // Given: a valid form
+      final form = fb.group({
+        'amount': 10,
+        'balance': 20,
+      }, [
+        Validators.compare(
+          'amount',
+          'balance',
+          CompareOption.lower,
+          allowNull: true,
+        ),
+      ]);
+
+      // Expect: form is valid
+      expect(form.valid, true);
+    });
+    test('Lower compare invalid (allow null)', () {
+      // Given: an invalid form
+      final form = fb.group({
+        'amount': 10,
+        'balance': 10,
+      }, [
+        Validators.compare(
+          'amount',
+          'balance',
+          CompareOption.lower,
+          allowNull: true,
+        ),
+      ]);
+
+      // Expect: form is invalid
+      expect(form.valid, false);
+    });
+    test('Compare with control null (allow null)', () {
+      // Given: a form with null values (allowed)
+      final form = fb.group({
+        'value1': FormControl<int>(),
+        'value2': FormControl<int>(value: 10),
+      }, [
+        Validators.compare(
+          'value1',
+          'value2',
+          CompareOption.equal,
+          allowNull: true,
+        ),
+      ]);
+
+      // Expect: form is valid
+      expect(form.valid, true);
+    });
+    test('Compare with the other control null (allow null)', () {
+      // Given: a form with null values (allowed)
+      final form = fb.group({
+        'value1': FormControl<int>(value: 10),
+        'value2': FormControl<int>(),
+      }, [
+        Validators.compare(
+          'value1',
+          'value2',
+          CompareOption.equal,
+          allowNull: true,
+        ),
+      ]);
+
+      // Expect: form is valid
+      expect(form.valid, true);
+    });
   });
 }