Skip to content

Commit da44924

Browse files
committed
Merge branch 'release/18.1.0'
2 parents 85c7bfa + 0d18119 commit da44924

14 files changed

+1276
-336
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
# 18.1.0
2+
3+
- Add `oneOf` validator to the list of validators.
4+
- Add `debounced` async validator that allows to specify a custom debounce time for a single validator.
5+
- The `Validators.delegateAsync()` function now accepts an optional `debounceTime` parameter, defaulting to 0. This allows immediate execution or custom debouncing for asynchronous validation.
6+
17
# 18.0.1
28

39
## Features
10+
- Add `allowNull` optional parameter to the `CompareValidator`.
411
- The FormControl.reset() method has been updated to align with
512
the common expectation that resetting a control without specifying a
613
new value should revert it to its initial state.

README.md

Lines changed: 331 additions & 262 deletions
Large diffs are not rendered by default.

lib/reactive_forms.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export 'src/validators/compose_or_validator.dart';
2929
export 'src/validators/compose_validator.dart';
3030
export 'src/validators/contains_validator.dart';
3131
export 'src/validators/credit_card_validator.dart';
32+
export 'src/validators/debounced_async_validator.dart';
3233
export 'src/validators/delegate_async_validator.dart';
3334
export 'src/validators/delegate_validator.dart';
3435
export 'src/validators/email_validator.dart';
@@ -39,6 +40,7 @@ export 'src/validators/min_length_validator.dart';
3940
export 'src/validators/min_validator.dart';
4041
export 'src/validators/must_match_validator.dart';
4142
export 'src/validators/number_validator.dart';
43+
export 'src/validators/one_of_validator.dart';
4244
export 'src/validators/pattern/default_pattern_evaluator.dart';
4345
export 'src/validators/pattern/pattern_evaluator.dart';
4446
export 'src/validators/pattern/regex_pattern_evaluator.dart';

lib/src/models/models.dart

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'dart:async';
77
import 'package:flutter/foundation.dart';
88
import 'package:reactive_forms/reactive_forms.dart';
99

10+
// ignore_for_file: deprecated_member_use_from_same_package
11+
1012
const _controlNameDelimiter = '.';
1113

1214
/// This is the base class for [FormGroup], [FormArray] and [FormControl].
@@ -40,7 +42,14 @@ abstract class AbstractControl<T> {
4042
/// Async validators debounce timer.
4143
Timer? _debounceTimer;
4244

45+
/// **DEPRECATED**: Use [Validators.debounced] to specify a debounce time for
46+
/// individual asynchronous validators. This property will be removed in a
47+
/// future major version.
48+
///
4349
/// Async validators debounce time in milliseconds.
50+
@Deprecated(
51+
"Use [Validators.debounced] to specify a debounce time for individual asynchronous validators.",
52+
)
4453
final int _asyncValidatorsDebounceTime;
4554

4655
bool _touched;
@@ -50,6 +59,12 @@ abstract class AbstractControl<T> {
5059
AbstractControl({
5160
List<Validator<dynamic>> validators = const [],
5261
List<AsyncValidator<dynamic>> asyncValidators = const [],
62+
@Deprecated(
63+
"Use [Validators.debounced] to specify a debounce time for individual asynchronous validators.",
64+
)
65+
/// **DEPRECATED**: Use [Validators.debounced] to specify a debounce time for
66+
/// individual asynchronous validators. This property will be removed in a
67+
/// future major version.
5368
int asyncValidatorsDebounceTime = 250,
5469
bool disabled = false,
5570
bool touched = false,
@@ -699,32 +714,54 @@ abstract class AbstractControl<T> {
699714
return;
700715
}
701716

702-
_status = ControlStatus.pending;
717+
markAsPending(emitEvent: true);
718+
719+
final debouncedValidators = _asyncValidators
720+
.whereType<DebouncedAsyncValidator>()
721+
.map((v) => v.validate(this));
722+
723+
final regularValidators = _asyncValidators.where(
724+
(v) => v is! DebouncedAsyncValidator,
725+
);
726+
727+
final allValidators = <Future<Map<String, dynamic>?>>[];
728+
allValidators.addAll(debouncedValidators);
703729

704730
_debounceTimer?.cancel();
705731

706-
_debounceTimer = Timer(
707-
Duration(milliseconds: _asyncValidatorsDebounceTime),
708-
() {
709-
final validatorsStream = Stream.fromFutures(
710-
asyncValidators.map((validator) => validator.validate(this)).toList(),
711-
);
732+
if (regularValidators.isNotEmpty) {
733+
final completer = Completer<List<Map<String, dynamic>?>>();
734+
_debounceTimer = Timer(
735+
Duration(milliseconds: _asyncValidatorsDebounceTime),
736+
() => completer.complete(
737+
Future.wait(regularValidators.map((v) => v.validate(this))),
738+
),
739+
);
740+
allValidators.add(
741+
completer.future.then(
742+
(errors) => errors.fold<Map<String, dynamic>>(
743+
{},
744+
(previousValue, element) => previousValue..addAll(element ?? {}),
745+
),
746+
),
747+
);
748+
}
712749

713-
final asyncValidationErrors = <String, dynamic>{};
714-
_asyncValidationSubscription = validatorsStream.listen(
715-
(Map<String, dynamic>? error) {
716-
if (error != null) {
717-
asyncValidationErrors.addAll(error);
718-
}
719-
},
720-
onDone: () {
721-
final allErrors = <String, dynamic>{};
722-
allErrors.addAll(errors);
723-
allErrors.addAll(asyncValidationErrors);
724-
725-
setErrors(allErrors, markAsDirty: false);
726-
},
727-
);
750+
final validatorsStream = Stream.fromFutures(allValidators);
751+
752+
final asyncValidationErrors = <String, dynamic>{};
753+
_asyncValidationSubscription = validatorsStream.listen(
754+
(Map<String, dynamic>? error) {
755+
if (error != null) {
756+
asyncValidationErrors.addAll(error);
757+
}
758+
},
759+
onDone: () {
760+
final allErrors = <String, dynamic>{};
761+
allErrors.addAll(errors);
762+
allErrors.addAll(asyncValidationErrors);
763+
764+
setErrors(allErrors, markAsDirty: false);
728765
},
729766
);
730767
}
@@ -842,6 +879,12 @@ class FormControl<T> extends AbstractControl<T> {
842879
T? value,
843880
super.validators,
844881
super.asyncValidators,
882+
@Deprecated(
883+
"Use [Validators.debounced] to specify a debounce time for individual asynchronous validators.",
884+
)
885+
/// **DEPRECATED**: Use [Validators.debounced] to specify a debounce time for
886+
/// individual asynchronous validators. This property will be removed in a
887+
/// future major version.
845888
super.asyncValidatorsDebounceTime,
846889
super.touched,
847890
super.disabled,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2020 Joan Pablo Jimenez Milian. All rights reserved.
2+
// Use of this source code is governed by the MIT license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:reactive_forms/reactive_forms.dart';
8+
9+
/// Delays the execution of an [AsyncValidator] by a specified duration.
10+
///
11+
/// This validator is useful for scenarios where you want to delay validation
12+
/// until the user has stopped typing for a certain period. It helps to
13+
/// avoid excessive validation requests, for example, when validating a
14+
/// username for availability against a remote server.
15+
///
16+
/// The [DebouncedAsyncValidator] should be used when you need to specify a
17+
/// custom debounce time for a single validator. If you want to apply the same
18+
/// debounce time to all asynchronous validators of a form control, it is
19+
/// more convenient to use the [FormControl.asyncValidatorsDebounceTime]
20+
/// property.
21+
///
22+
/// ## Example:
23+
///
24+
/// ```dart
25+
/// final control = FormControl<String>(
26+
/// asyncValidators: [
27+
/// DebouncedAsyncValidator(
28+
/// Validators.delegateAsync((control) async {
29+
/// // Your validation logic here
30+
/// return null;
31+
/// }),
32+
/// 500, // Debounce time in milliseconds
33+
/// ),
34+
/// ],
35+
/// );
36+
/// ```
37+
///
38+
/// In this example, the validation will only be triggered after the user has
39+
/// stopped typing for 500 milliseconds.
40+
class DebouncedAsyncValidator extends AsyncValidator<dynamic> {
41+
final AsyncValidator<dynamic> _validator;
42+
final int _debounceTime;
43+
Timer? _debounceTimer;
44+
45+
/// Creates a new instance of the [DebouncedAsyncValidator] class.
46+
///
47+
/// The [validator] is the async validator to be debounced.
48+
/// The [debounceTime] is the duration in milliseconds to wait before
49+
/// validating the control.
50+
DebouncedAsyncValidator(this._validator, this._debounceTime)
51+
: assert(_debounceTime >= 0);
52+
53+
@override
54+
Future<Map<String, dynamic>?> validate(AbstractControl<dynamic> control) {
55+
final completer = Completer<Map<String, dynamic>?>();
56+
_debounceTimer?.cancel();
57+
_debounceTimer = Timer(Duration(milliseconds: _debounceTime), () {
58+
completer.complete(_validator.validate(control));
59+
});
60+
return completer.future;
61+
}
62+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2020 Joan Pablo Jimenez Milian. All rights reserved.
2+
// Use of this source code is governed by the MIT license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:reactive_forms/reactive_forms.dart';
6+
7+
/// Represents a validator that requires the control's value to be one of
8+
/// the values in the provided [collection].
9+
///
10+
/// For [String] values, the comparison can be made case-sensitive or insensitive
11+
/// using the [caseSensitive] parameter. For other types, this parameter is ignored.
12+
class OneOfValidator extends Validator<dynamic> {
13+
final List<dynamic> collection;
14+
final bool caseSensitive;
15+
16+
/// Constructs an instance of [OneOfValidator].
17+
///
18+
/// The [collection] parameter is the list of allowed values.
19+
/// The [caseSensitive] parameter determines if string comparison
20+
/// should be case-sensitive. Defaults to true.
21+
OneOfValidator(this.collection, {this.caseSensitive = true});
22+
23+
@override
24+
Map<String, dynamic>? validate(AbstractControl<dynamic> control) {
25+
final value = control.value;
26+
var found = false;
27+
28+
for (final item in collection) {
29+
if (value is String && item is String && !caseSensitive) {
30+
if (item.toLowerCase() == value.toLowerCase()) {
31+
found = true;
32+
break;
33+
}
34+
} else {
35+
if (item == value) {
36+
found = true;
37+
break;
38+
}
39+
}
40+
}
41+
42+
if (found) {
43+
return null; // Valid
44+
} else {
45+
return {
46+
ValidationMessage.oneOf: {
47+
'requiredOneOf': collection,
48+
'actual': value,
49+
if (value is String) 'caseSensitive': caseSensitive,
50+
},
51+
};
52+
}
53+
}
54+
}

lib/src/validators/validation_message.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ class ValidationMessage {
5959
/// Key text for `contains` validation message.
6060
static const String contains = 'contains';
6161

62+
/// Key text for `oneOf` validation message.
63+
static const String oneOf = 'oneOf';
64+
6265
/// Key text for `any` validation message.
6366
static const String any = 'any';
6467
}

0 commit comments

Comments
 (0)