Open mrverdant13 opened 1 year ago
Hi @mrverdant13,
ReactiveStatusListenableBuilder
is the one that will rebuild each time the status of the FormArray
changes (i.e. INVALID, VALID, PENDING, DISABLED).
The title of the issue is about showing the errors of the FormArray, but later on in the description of the issue you mention that the ReactiveStatusListenableBuilder
is not working as expected.
Would you mind to elaborate a bit more on the issue, or show us a simple code example to understand what you are trying to accomplish? Thanks
Thanks for the quick response, @joanpablo !
More context here. I am using a nested setup for my form.
```dart
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:reactive_forms/reactive_forms.dart';
class TestForm extends StatelessWidget {
const TestForm({
super.key,
});
@override
Widget build(BuildContext context) {
return ReactiveFormBuilder(
form: () {
final expectedTotalValueFormControl = FormControl
Validate
buttonThis proves that the error in the array is actually present. ```json { "expected": { "expectedTotalValue": { "required": true, "min": { "min": 0, "actual": null } } }, "actual": { "array": { "minLength": { "requiredLength": 1, "actualLength": 0 } } } } ```
**Initial state** ![Screenshot_20230825-125457](https://github.com/joanpablo/reactive_forms/assets/42245236/c97552c9-a2a2-4ad7-9f79-d742de727856) **Actual state after tapping on the `Validate` button** ![Screenshot_20230825-125522](https://github.com/joanpablo/reactive_forms/assets/42245236/5e913c70-a8be-40df-99bc-154f26667c22) **Expected state after tapping on the `Validate` button** (which is properly displayed after a hot reload) > Note: The JSONs is used for demonstration purposes only. They would be replaced with an error widget. ![Screenshot_20230825-125532](https://github.com/joanpablo/reactive_forms/assets/42245236/49dd1f0d-af7c-44e6-bf9c-79c587540765)
Hi @mrverdant13,
Thanks for the example. I'm going to take a look to understand what's going on.
Hi @mrverdant13,
I have tested your example and it works correctly, as expected. The errors of the Form Array are displayed correctly.
There are a lot of things to improve and simplify in that code that you posted here. But it works correctly.
I recommend you to use dirty
or pristine
if instead of touched
if you want to see the errors sooner.
if (formArray.pristine || formArray.valid) {
return const SizedBox.shrink();
}
this is the complete code (same as yours but simplified):
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:reactive_forms/reactive_forms.dart';
import 'package:reactive_forms_example/sample_screen.dart';
class TestForm extends StatelessWidget {
const TestForm({
super.key,
});
@override
Widget build(BuildContext context) {
return SampleScreen(
body: ReactiveFormBuilder(
form: () {
final expectedTotalValueFormControl = FormControl<int>(
validators: [
Validators.required,
Validators.min(0),
],
);
final expectedTotalValueFormGroup = FormGroup({
'expectedTotalValue': expectedTotalValueFormControl,
});
final valuesFormArray = FormArray(
[],
validators: [
Validators.required,
Validators.minLength(1),
],
);
final valuesFormGroup = FormGroup({
'array': valuesFormArray,
});
final globalFormGroup = FormGroup({
'expected': expectedTotalValueFormGroup,
'actual': valuesFormGroup,
});
return globalFormGroup;
},
builder: (context, form, child) {
return const TestFormBody();
},
),
);
}
}
class TestFormBody extends StatefulWidget {
const TestFormBody({
super.key,
});
@override
State<TestFormBody> createState() => _TestFormBodyState();
}
class _TestFormBodyState extends State<TestFormBody> {
late final StreamSubscription<dynamic> subscription;
static const jsonEncoder = JsonEncoder.withIndent(' ');
@override
void initState() {
super.initState();
Future(() async {
final formGroup = ReactiveForm.of(context, listen: false)! as FormGroup;
subscription =
formGroup.control('expected.expectedTotalValue').valueChanges.listen(
(expectedTotalValue) {
if (expectedTotalValue is! int) return;
final valuesFormArray =
formGroup.control('actual.array') as FormArray;
final validators = valuesFormArray.validators;
valuesFormArray.setValidators(
[
...validators,
Validators.delegate(
(_) {
num actualTotalValue = 0;
if (valuesFormArray.controls.isNotEmpty) {
actualTotalValue = valuesFormArray.controls.map<num>(
(control) {
if (control is! FormGroup) return 0;
final value = control.control('value').value;
if (value is! num) return 0;
return value;
},
).reduce((value, element) => value + element);
}
print(
'actualTotalValue: $actualTotalValue - value: $expectedTotalValue',
);
if (actualTotalValue == expectedTotalValue) return null;
return {
ValidationMessage.mustMatch: {
'expected': expectedTotalValue,
'actual': actualTotalValue,
},
};
},
),
],
autoValidate: true,
);
},
);
return subscription.cancel;
});
}
@override
void dispose() {
subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final formGroup = ReactiveForm.of(context, listen: false)! as FormGroup;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(15),
child: ReactiveTextField<int>(
decoration: const InputDecoration(
labelText: 'Expected Total Value',
),
formControlName: 'expected.expectedTotalValue',
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
),
const Divider(),
Column(
children: [
ReactiveFormArray(
formArrayName: 'actual.array',
builder: (context, formArray, child) {
final groups = formArray.controls;
return Column(
children: [
for (final group in groups)
if (group is FormGroup)
ReactiveForm(
key: ValueKey(group),
formGroup: group,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 7.5,
),
child: Row(
children: [
Expanded(
child: ReactiveTextField<int>(
decoration: const InputDecoration(
labelText: 'Value',
),
formControlName: 'value',
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 15),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
formArray.removeAt(groups.indexOf(group));
},
),
],
),
),
),
],
);
},
),
ReactiveStatusListenableBuilder(
formControlName: 'actual.array',
builder: (context, formArray, child) {
if (formArray.pristine || formArray.valid) {
return const SizedBox.shrink();
}
return const Text(
'Array invalid',
style: TextStyle(
color: Colors.red,
),
);
},
),
ElevatedButton.icon(
onPressed: () {
final formArray = formGroup.control('actual.array');
if (formArray is! FormArray) return;
formArray.add(
FormGroup({
'value': FormControl<int>(
validators: [
Validators.required,
Validators.min(0),
],
),
}),
);
},
icon: const Icon(Icons.add),
label: const Text('Add group'),
),
ElevatedButton.icon(
onPressed: () {
final errorsJson = jsonEncoder.convert(formGroup.errors);
print(errorsJson);
formGroup.markAllAsTouched();
},
icon: const Icon(Icons.check),
label: const Text('Validate'),
),
],
),
],
);
}
}
Hey there! First, thanks for this useful package!!!
I am trying to display a custom widget with the error details of a
FormArray
to which a validator has been added withsetValidators
as follows:I've tried with
ReactiveValueListenableBuilder
,ReactiveFormArray
andReactiveStatusListenableBuilder
, but none of them seem to update the UI reactively. The only time when the UI seem to be updated is when theFormArray
internalcontrols
list get updated.I am not completely sure that's a bug or if that is the expected behaviour.