Skip to content

Commit ec7023a

Browse files
committed
Merge branch 'release/18.1.1'
2 parents da44924 + aaae9fe commit ec7023a

File tree

6 files changed

+277
-14
lines changed

6 files changed

+277
-14
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 18.1.1
2+
3+
## Fixes
4+
- Fix `NumberValidator` to correctly handle decimal numbers with trailing zeros.
5+
- Improve `NumberValidator` to correctly validate numbers with multiple decimal points, single dot, and dot at the end.
6+
17
# 18.1.0
28

39
- Add `oneOf` validator to the list of validators.
@@ -1000,4 +1006,4 @@ to migrate from version 1.x to 2.x.
10001006

10011007
- ReactiveForm
10021008
- ReactiveFormConsumer
1003-
- ReactiveValueListenableBuilder
1009+
- ReactiveValueListenableBuilder

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
'''# Reactive Forms
1+
# Reactive Forms
22

33
This is a model-driven approach to handling form inputs and validations, heavily inspired by [Angular's Reactive Forms](https://angular.io/guide/reactive-forms).
44

@@ -88,7 +88,7 @@ dependencies:
8888
flutter:
8989
sdk: flutter
9090

91-
reactive_forms: ^18.1.0
91+
reactive_forms: ^18.1.1
9292
```
9393
9494
Then, run the command `flutter packages get` in the console.
@@ -1756,4 +1756,3 @@ You can also check the [Star Rating with Flutter Reactive Forms](https://dev.to/
17561756

17571757
Visit the [Migration Guide](https://github.com/joanpablo/reactive_forms/wiki/Migration-Guide) to see
17581758
more details about different version breaking changes.
1759-
''

lib/src/validators/number_validator.dart

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,42 @@
55
import 'package:reactive_forms/reactive_forms.dart';
66
import 'package:reactive_forms/src/validators/number_validator_error.dart';
77

8-
/// Validator that validates if control's value is a numeric value.
8+
/// A validator that checks if a control's value represents a valid number.
9+
///
10+
/// This validator can be used for both `String` and `num` types. It provides
11+
/// options to allow or disallow null values, negative numbers, and to specify
12+
/// the number of allowed decimal places.
13+
///
14+
/// ## Example:
15+
///
16+
/// ```dart
17+
/// final control = FormControl<String>(
18+
/// validators: [Validators.number()],
19+
/// );
20+
///
21+
/// control.value = '123';
22+
/// print(control.valid); // true
23+
///
24+
/// control.value = 'abc';
25+
/// print(control.valid); // false
26+
///
27+
/// control.value = '12.34';
28+
/// print(control.valid); // false, because decimals are not allowed by default
29+
/// ```
30+
///
31+
/// To allow decimals, you can use the `allowedDecimals` parameter:
32+
///
33+
/// ```dart
34+
/// final decimalControl = FormControl<String>(
35+
/// validators: [Validators.number(allowedDecimals: 2)],
36+
/// );
37+
///
38+
/// decimalControl.value = '12.34';
39+
/// print(decimalControl.valid); // true
40+
///
41+
/// decimalControl.value = '12.345';
42+
/// print(decimalControl.valid); // false, as it exceeds the allowed decimal places
43+
/// ```
944
class NumberValidator extends Validator<dynamic> {
1045
/// The regex expression of a numeric string value.
1146
static final RegExp notNumbersRegex = RegExp(r'[^0-9.]');
@@ -93,17 +128,17 @@ class NumberValidator extends Validator<dynamic> {
93128
// Split the number string at the decimal point
94129
final parts = numberString.split('.');
95130

96-
if (parts.length > 2) {
131+
if (parts.length > 2 || numberString == '.' || numberString.endsWith('.')) {
97132
// More than one decimal point, invalid format
98133
return false;
99134
}
100135

101136
if (parts.length == 1) {
102-
// No decimal part, validate it has 0 decimals
103-
return allowedDecimals == 0;
137+
// No decimal part, so it's valid
138+
return true;
104139
}
105140

106-
// Check if the decimal part length is equal to the allowed decimals
107-
return parts[1].length == allowedDecimals;
141+
// Check if the decimal part length is within the allowed limit
142+
return parts[1].length <= allowedDecimals;
108143
}
109144
}

lib/src/validators/validators.dart

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,101 @@ class Validators {
131131
/// validation test.
132132
static Validator<dynamic> get email => const EmailValidator();
133133

134-
/// Creates a validator function that checks if a control's value is a valid number.
134+
/// Creates a validator that checks if a control's value is a valid number.
135135
///
136-
/// [allowedDecimals] (optional): The allowed number of decimal places. Defaults to 0.
137-
/// [allowNegatives] (optional): Whether to allow negative numbers. Defaults to true.
136+
/// This validator can be applied to controls with `String` or `num` (int, double) values.
137+
/// It provides fine-grained control over what constitutes a valid number:
138+
///
139+
/// - [allowNull]: If `true`, a `null` control value is considered valid. Defaults to `false`.
140+
/// - [allowedDecimals]: Specifies the maximum number of digits allowed after the decimal point.
141+
/// Defaults to `0`, meaning only integers are allowed. For example, `allowedDecimals: 2`
142+
/// would allow numbers like `123.45` or `123.4` but not `123.456`.
143+
/// - [allowNegatives]: If `true`, negative numbers (e.g., `-10`) are considered valid.
144+
/// Defaults to `true`.
145+
///
146+
/// The validation error is exposed under the `ValidationMessage.number` key.
147+
/// Possible error values include `NumberValidatorError.nullValue` (if `allowNull` is `false`
148+
/// and the value is `null`), `NumberValidatorError.unsignedNumber` (if `allowNegatives` is
149+
/// `false` and the number is negative), `NumberValidatorError.invalidDecimals` (if the number
150+
/// of decimal places exceeds `allowedDecimals`), or `NumberValidatorError.invalidNumber`
151+
/// (for non-numeric input or invalid formatting).
152+
///
153+
/// ## Examples:
154+
///
155+
/// ### Basic Usage (Integer only, not null, positive):
156+
/// ```dart
157+
/// final control = FormControl<String>(validators: [Validators.number()]);
158+
///
159+
/// control.value = '123';
160+
/// print(control.valid); // true
161+
///
162+
/// control.value = '-123';
163+
/// print(control.valid); // true (allowNegatives is true by default)
164+
///
165+
/// control.value = '12.34';
166+
/// print(control.valid); // false (allowedDecimals is 0 by default)
167+
///
168+
/// control.value = 'abc';
169+
/// print(control.valid); // false
170+
///
171+
/// control.value = null;
172+
/// print(control.valid); // false (allowNull is false by default)
173+
/// ```
174+
///
175+
/// ### Allowing Decimals:
176+
/// ```dart
177+
/// final decimalControl = FormControl<String>(
178+
/// validators: [Validators.number(allowedDecimals: 2)],
179+
/// );
180+
///
181+
/// decimalControl.value = '12.34';
182+
/// print(decimalControl.valid); // true
183+
///
184+
/// decimalControl.value = '12.345';
185+
/// print(decimalControl.valid); // false (too many decimal places)
186+
///
187+
/// decimalControl.value = '12';
188+
/// print(decimalControl.valid); // true (integers are valid when decimals are allowed)
189+
/// ```
190+
///
191+
/// ### Disallowing Negative Numbers:
192+
/// ```dart
193+
/// final positiveControl = FormControl<String>(
194+
/// validators: [Validators.number(allowNegatives: false)],
195+
/// );
196+
///
197+
/// positiveControl.value = '100';
198+
/// print(positiveControl.valid); // true
199+
///
200+
/// positiveControl.value = '-50';
201+
/// print(positiveControl.valid); // false
202+
/// ```
203+
///
204+
/// ### Allowing Null Values:
205+
/// ```dart
206+
/// final nullableControl = FormControl<String>(
207+
/// validators: [Validators.number(allowNull: true)],
208+
/// );
209+
///
210+
/// nullableControl.value = null;
211+
/// print(nullableControl.valid); // true
212+
///
213+
/// nullableControl.value = '123';
214+
/// print(nullableControl.valid); // true
215+
/// ```
216+
///
217+
/// ### Using with `double` type controls:
218+
/// ```dart
219+
/// final doubleControl = FormControl<double>(
220+
/// validators: [Validators.number(allowedDecimals: 2)],
221+
/// );
222+
///
223+
/// doubleControl.value = 123.45;
224+
/// print(doubleControl.valid); // true
225+
///
226+
/// doubleControl.value = 123.456;
227+
/// print(doubleControl.valid); // false
228+
/// ```
138229
static Validator<dynamic> number({
139230
bool allowNull = false,
140231
int allowedDecimals = 0,

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: reactive_forms
22
description: This is a model-driven approach to handling form inputs and validations, heavily inspired in Angular Reactive Forms.
3-
version: 18.1.0
3+
version: 18.1.1
44
homepage: "https://github.com/joanpablo/reactive_forms"
55

66
environment:

test/src/validators/number_validator_test.dart

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,137 @@ void main() {
8383
NumberValidatorError.invalidDecimals,
8484
);
8585
});
86+
87+
test('FormControl of type String valid decimal numbers with .00', () {
88+
final control = FormControl<String>(
89+
validators: [Validators.number(allowedDecimals: 2)],
90+
);
91+
92+
control.value = '10.00';
93+
94+
expect(control.valid, true);
95+
expect(control.errors.isEmpty, true);
96+
});
97+
98+
test('FormControl of type double valid decimal numbers with .00', () {
99+
final control = FormControl<double>(
100+
validators: [Validators.number(allowedDecimals: 2)],
101+
);
102+
103+
control.value = 10.00;
104+
105+
expect(control.valid, true);
106+
expect(control.errors.isEmpty, true);
107+
});
108+
109+
test(
110+
'FormControl invalid negative number when allowNegatives is false',
111+
() {
112+
final control = FormControl<String>(
113+
validators: [Validators.number(allowNegatives: false)],
114+
);
115+
116+
control.value = '-10';
117+
118+
expect(control.valid, false);
119+
expect(
120+
control.errors[ValidationMessage.number],
121+
NumberValidatorError.unsignedNumber,
122+
);
123+
},
124+
);
125+
126+
test('FormControl invalid with multiple decimal points', () {
127+
final control = FormControl<String>(validators: [Validators.number()]);
128+
129+
control.value = '10.1.0';
130+
131+
expect(control.valid, false);
132+
expect(
133+
control.errors[ValidationMessage.number],
134+
NumberValidatorError.invalidDecimals,
135+
);
136+
});
137+
138+
test('FormControl valid with leading/trailing spaces', () {
139+
final control = FormControl<String>(validators: [Validators.number()]);
140+
141+
control.value = ' 10 ';
142+
143+
expect(control.valid, true);
144+
});
145+
146+
test('FormControl invalid with invalid characters', () {
147+
final control = FormControl<String>(validators: [Validators.number()]);
148+
149+
control.value = '10a.5';
150+
151+
expect(control.valid, false);
152+
expect(
153+
control.errors[ValidationMessage.number],
154+
NumberValidatorError.invalidDecimals,
155+
);
156+
});
157+
158+
test('FormControl invalid with only spaces', () {
159+
final control = FormControl<String>(validators: [Validators.number()]);
160+
161+
control.value = ' ';
162+
163+
expect(control.valid, false);
164+
expect(
165+
control.errors[ValidationMessage.number],
166+
NumberValidatorError.invalidNumber,
167+
);
168+
});
169+
170+
test('FormControl invalid with a single dot', () {
171+
final control = FormControl<String>(validators: [Validators.number()]);
172+
173+
control.value = '.';
174+
175+
expect(control.valid, false);
176+
expect(
177+
control.errors[ValidationMessage.number],
178+
NumberValidatorError.invalidDecimals,
179+
);
180+
});
181+
182+
test('FormControl invalid with a dot at the end', () {
183+
final control = FormControl<String>(validators: [Validators.number()]);
184+
185+
control.value = '10.';
186+
187+
expect(control.valid, false);
188+
expect(
189+
control.errors[ValidationMessage.number],
190+
NumberValidatorError.invalidDecimals,
191+
);
192+
});
193+
194+
test(
195+
'FormControl invalid with invalid characters and no decimal point',
196+
() {
197+
final control = FormControl<String>(validators: [Validators.number()]);
198+
199+
control.value = '1a0';
200+
201+
expect(control.valid, false);
202+
expect(
203+
control.errors[ValidationMessage.number],
204+
NumberValidatorError.invalidNumber,
205+
);
206+
},
207+
);
208+
209+
test('FormControl valid with a dot at the beginning', () {
210+
final control = FormControl<String>(
211+
validators: [Validators.number(allowedDecimals: 1)],
212+
);
213+
214+
control.value = '.5';
215+
216+
expect(control.valid, true);
217+
});
86218
});
87219
}

0 commit comments

Comments
 (0)