joanpablo / reactive_forms

This is a model-driven approach to handling form inputs and validations, heavily inspired in Angular's Reactive Forms
MIT License
456 stars 83 forks source link

`ReactiveFormArray` doesn't rebuild when `formArray.status` changes due to AsyncValidator #355

Open Giuspepe opened 1 year ago

Giuspepe commented 1 year ago

When using a ReactiveFormArray with asyncValidators in its FormArray, the builder doesn't update when the FormArray.status changes from pending to valid due to the asyncValidators. Instead, it is stuck in pending even though the asyncValidators return immediately. If I remove the asyncValidators and only use synchronous validators, it works as expected.

See the attached screen recording and minimum reproducible example.

When using a ReactiveFormArray with a FormArray which has asyncValidators, I expect ReactiveFormArray.builder to be rebuilt when the FormArray.status changes due to the asyncValidators.

https://user-images.githubusercontent.com/39117631/217793024-093049d2-675e-4ec1-849a-3fff9504017d.mp4

You can see that the FormArray.status does indeed change back to valid if I access FormArray.status in a StreamBuilder listening to FormArray.statusChanged. It's just the ReactiveFormArray.builder that doesn't receive the new FormArray.status. Compare the formArray.status Texts in the video in red (FormArray.status in StreamBuilder) and black (FormArray.status in ReactiveFormArray.builder). The black text stays at ControlStatus.pending when asyncValidators are enabled while it should be in sync with the red text.

main.dart ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:reactive_forms/reactive_forms.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const ProviderScope( child: MaterialApp( title: 'Flutter Demo', home: MyHomePage(), ), ); } } class MyHomePage extends ConsumerWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final formArray = ref.read(myControllerProvider.notifier).formArray; return Scaffold( appBar: AppBar( title: const Text('Demo'), ), body: Center( child: ReactiveFormArray( formArray: formArray, builder: (context, formArray, _) => SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () => formArray.asyncValidators.isEmpty ? ref .read(myControllerProvider.notifier) .setAsyncValidators() : ref .read(myControllerProvider.notifier) .clearAsyncValidators(), child: Text( formArray.asyncValidators.isEmpty ? 'Enable Async Validators' : 'Disable Async Validators', ), ), Text( 'formArray.asyncValidators.isEmpty: ${formArray.asyncValidators.isEmpty}\n' 'formArray.pending: ${formArray.pending}\n' 'formArray.errors: ${formArray.errors}\n' 'formArray.status: ${formArray.status}', style: TextStyle(fontWeight: FontWeight.bold), ), StreamBuilder( stream: formArray.statusChanged, builder: (context, data) => Text( 'Inside formArray.statusChanged StreamBuilder:\n' ' - formArray.status: ${formArray.status}', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.red, ), ), ), for (final control in formArray.controls) Text(control.value.toString()), ], ), ), ), ), floatingActionButton: FloatingActionButton( onPressed: ref.read(myControllerProvider.notifier).update, tooltip: 'Update', child: const Icon(Icons.update), ), ); } } final myControllerProvider = StateNotifierProvider>((ref) { final controller = MyController(); ref.listenSelf((previous, next) { // without clear(), the old formArray.controls would remain when the `next` list is shorter controller.formArray.clear(); controller.formArray.value = [...next]; // trigger validation when setting values programatically controller.formArray.markAllAsTouched(); }); return controller; }); class MyController extends StateNotifier> { MyController() : super([]); void update() => state = [...state, DateTime.now()]; void setAsyncValidators() { formArray.setAsyncValidators(_asyncValidators, autoValidate: true); formArray.markAllAsTouched(); } void clearAsyncValidators() { formArray.clearAsyncValidators(); // formArray.clearAsyncValidators states: // When you add or remove a validator at run time, you must call // **updateValueAndValidity()** for the new validation to take effect. formArray.updateValueAndValidity(); formArray.markAllAsTouched(); } late final formArray = FormArray( [], validators: _validators, asyncValidators: _asyncValidators, ); final _validators = [Validators.minLength(4)]; final _asyncValidators = [_asyncValidator]; } Future?> _asyncValidator( AbstractControl control) async { return null; } ```
pubspec.yaml ``` name: reactive_forms_example environment: sdk: '>=2.19.0 <3.0.0' dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 reactive_forms: ^14.2.0 flutter_riverpod: ^2.1.3 ```
sagnik-sanyal commented 2 months ago

Hi @Giuspepe did you find any workaround for this issue ?

Giuspepe commented 2 months ago

Hi @Giuspepe did you find any workaround for this issue ?

Sorry @sagnik-sanyal, I don't remember looking into this any further as it was a year ago

sagnik-sanyal commented 2 months ago

Thanks man for your quick response

sagnik-sanyal commented 1 month ago

Upvoted this issue , current workaround is to use synchronous validators instead of async validator. @joanpablo Looking forward to hear from you regarding this.