joanpablo / reactive_forms

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

Changing focus does not validate the control #315

Open rebaz94 opened 2 years ago

rebaz94 commented 2 years ago

Hi in the FormGroup, I have two control when the user change the value, it does not validate until moving focus multiple time to other widget, then the error will be shown.

'email': FormControl<String>(
   value: '',
   validators: [
     Validators.email,
   ],
 ),
 'password': FormControl<String>(
   value: '',
   validators: [
     Validators.required,
   ],
 ),

here in the video I tabbed multiple time to move focus around, as you see this cause the login button to be disabled because there is error but in the text field does not show.

https://user-images.githubusercontent.com/11982812/180612904-08bc65de-aab4-41da-a1f8-3623c655639d.mov

is there is anything I can do validate automatically when focus changes. in the docs said changing focus or completing the text will trigger validation.

I tried to use FocusNode for each text field and markAsTouched and that's work but I think the library it should do that ?

Thanks

vasilich6107 commented 2 years ago

hi @rebaz94 Check login form sample in example when you run the example - it works perfectly without any delays in validation https://github.com/joanpablo/reactive_forms/blob/master/example/lib/samples/login_sample.dart

If you still have issues provide a repository with reproduction code

rebaz94 commented 2 years ago

@vasilich6107 I tried multiple time and the code is same and error not shown, it work when you change focus multiple times. In my case I tested on mac, maybe that's the problem?

joanpablo commented 2 years ago

Hi @rebaz94,

Thank you for using Reactive Forms and for the issue.

BTW your UI in the sample video is really nice ;)

In order to be able to help you, would you mind sharing with us a portion of your code, or any other code that allows us to understand: 1-How are you creating the FormGroup? 2-Are you using any State Management library?

rebaz94 commented 2 years ago

Hi @joanpablo Thank you :).

1-How are you creating the FormGroup?

Normally I just create from StateNotifier

2-Are you using any State Management library?

I use Riverpod but does not do any special things, just get FormGroup

I will create the FormGroup like this

FormGroup(
      {
        'name': FormControl<String>(
          value: '',
          validators: [
            Validators.required,
          ],
        ),
        'email': FormControl<String>(
          value: '',
          validators: [
            Validators.required,
            Validators.email,
          ],
        ),
        'password': FormControl<String>(
          value: '',
          validators: [
            Validators.required,
          ],
        ),
      },
    );

and custom text field widget

class LoginField extends StatelessWidget {
  const LoginField({
    Key? key,
    required this.controllerName,
    this.validationMessages,
    this.onTap,
    this.focusNode,
    this.hint,
    this.padding = const EdgeInsets.only(top: 10.0),
    required this.prefixIcon,
  }) : super(key: key);

  final String controllerName;
  final Map<String, String>? validationMessages;
  final VoidCallback? onTap;
  final FocusNode? focusNode;
  final String? hint;
  final EdgeInsetsGeometry padding;
  final Widget prefixIcon;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: padding,
      child: ReactiveTextField(
        formControlName: controllerName,
        validationMessages: (_) => validationMessages ?? {},
        focusNode: focusNode,
        onTap: onTap,
        style: styles.loginFieldStyle,
        maxLines: 1,
        textInputAction: TextInputAction.next,
        decoration: InputDecoration(
          isCollapsed: true,
          contentPadding: const EdgeInsetsDirectional.only(start: 12.0, end: 12.0, top: 16.0, bottom: 16.0),
          errorStyle: const TextStyle(height: 1.3),
          errorMaxLines: 2,
        ),
      ),
    );
  }
}

and usage for the LoginField

LoginField(
  controllerName: 'email',
  hint: 'yours@gmail.com',
  prefixIcon: Icon(
    FontAwesomeIcons.at,
    color: Colors.grey.withOpacity(0.45),
    size: 16.0,
  ),
),

and SwiftyFormBuilder. you can ignore this, it just helper widget, basically return ReactiveForm

```dart class SwiftyFormBuilder, A extends Object, M> extends ConsumerWidget { const SwiftyFormBuilder({ Key? key, required this.provider, this.onSetupArgs = defaultOnSetupArgs, required this.formBuilder, required this.errorBuilder, this.loadingBuilder = defaultLoadingBuilder, this.formSelector = defaultFormSelector, this.onChange, this.ignoreDataAndBuildByState = false, }) : super(key: key); static Widget defaultLoadingBuilder(BuildContext context, WidgetRef ref, _) { return const Center(child: CircularLoadingIndicator()); } static FormGroup defaultFormSelector(/*M*/ dynamic result) { return result as FormGroup; } static void defaultOnSetupArgs(BaseStateNotifier notifier, WidgetRef ref) {} final StateNotifierProviderOverrideMixin>, Result> provider; final void Function(N notifier, WidgetRef ref) onSetupArgs; /// This is indicate that the state of form build by state /// for example if [ignoreDataAndBuildByState] is true and form has previous state, it will ignore it final bool ignoreDataAndBuildByState; final Widget Function( BuildContext context, WidgetRef ref, N notifier, M formInfo, ) formBuilder; final Widget Function( BuildContext context, WidgetRef ref, N notifier, ) loadingBuilder; final Widget Function( BuildContext context, WidgetRef ref, N notifier, String message, ) errorBuilder; final void Function(BuildContext context, Result value)? onChange; final FormGroup Function(M data) formSelector; BaseStateNotifier notifier(WidgetRef ref) { return ref.read(provider.notifier) as BaseStateNotifier; } SwiftyForm swiftyForm(WidgetRef ref) { return ref.read(provider.notifier) as SwiftyForm; } @override Widget build(BuildContext context, WidgetRef ref) { final notifier = (ref.watch(provider.notifier) as N); onSetupArgs(notifier, ref); final state = ref.watch(provider); final formKey = ValueKey(notifier.formKey); final content = state.maybeWhen( (data, _) { return ReactiveForm( key: formKey, formGroup: formSelector(data), child: formBuilder(context, ref, notifier, data), ); }, loading: (dataOrNull) { if (dataOrNull == null || ignoreDataAndBuildByState) { return loadingBuilder(context, ref, notifier); } return ReactiveForm( key: formKey, formGroup: formSelector(dataOrNull), child: formBuilder(context, ref, notifier, dataOrNull), ); }, orElse: () { final dataOrNull = state.dataOrNull; if (dataOrNull == null || ignoreDataAndBuildByState) { return errorBuilder(context, ref, notifier, errorMessage(state)); } return ReactiveForm( key: formKey, formGroup: formSelector(dataOrNull), child: formBuilder(context, ref, notifier, dataOrNull), ); }, ); if (onChange != null) { ref.listen>(provider, (_, value) { onChange!.call(context, value); }); } return content; } String errorMessage(Result state) { return state.maybeWhen( (data, _) => 'Failed', noInternet: () => 'No Internet', orElse: () => 'Failed to load data, please try again', ); } } ```
vasilich6107 commented 2 years ago

This is why I'm always asking to reproduction repo) The local code utilization could have many things that we can't imagine. So instead of trying to guess I prefer to save time for both of us)

vasilich6107 commented 2 years ago

@rebaz94 could you try to run example project with login form. It should work fine despite of OS

rebaz94 commented 2 years ago

This is why I'm always asking to reproduction repo) The local code utilization could have many things that we can't imagine. So instead of trying to guess I prefer to save time for both of us)

@vasilich6107 I put all the code here that I used, there is nothing else to customize or anything I will test the example as you said and let you know.

rebaz94 commented 2 years ago

@vasilich6107 @joanpablo founded that if I use IndexedStack and maintainState is true and share a form like what I did then the form focus will not work properly, so quick fix is to maintainState: false.

return IndexedStack(
  index: currentTab,
  children: [
    Visibility(
      visible: currentTab == 0,
      maintainState: false,
      child: LoginTab(),
    ),
    Visibility(
      visible: currentTab == 1,
      maintainState: false,
      child: RegisterTab(),
    ),
  ],
);

is there is any workaround to use one FormGroup in multi places? if not, its time to close the issue :D Thanks

joanpablo commented 2 years ago

Hi @rebaz94,

Yes, you can use FormGroup in multiple places, that is not the issue. If you ask me, the code you are sharing is unnecessarily complex.

You are creating several ReactiveForm based on conditions, instead of creating just one ReactiveForm.

You are not giving us context about how you are creating the FormGroup. You just copy/paste the definition but not the context of that definition: is it inside a StatefulWidget or StatelessWidget? Is it inside a Controller? Are you creating several FormGroup based on conditions?

Definitely, the complexity of your implementation is giving you some issues. We would like to help you to make your code works, but you will need to bring more context.

Thanks in advance.

joanpablo commented 2 years ago

@rebaz94 take notice that you must have only one instance of FormGroup, the FormGroup is your model, it does not matter how many times you rebuild the UI, but you must not create/destroy repeatedly the FormGroup. If you do that then you are destroying the data of your model, resetting the data, and all the status of the FormGroup.

That's why we always recommend using a State Management Library and declaring the FormGroup inside the Controller/Bloc/ViewModel or if you are declaring the FormGroup inside a Widget it should be a StatefulWidget or use the ReactiveFormBuilder.

Are you sure you are not creating/destroying the FormGroup repeatedly?

rebaz94 commented 2 years ago

@joanpablo I'm using Riverpod to manage state and only create one instance of FormGroup and multiple ReactiveForm. the problem happen when sharing a FormGroup in the widget tree and if you have two active ReactiveForm

here reproduction code

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reactive_forms/reactive_forms.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData.dark(),
      themeMode: ThemeMode.dark,
      home: ProviderScope(
        child: LoginScreenTest(),
      ),
    ),
  );
}

class FormNotifier extends StateNotifier<FormGroup> {
  FormNotifier()
      : super(
          FormGroup(
            {
              'name': FormControl(
                value: '',
                validators: [
                  Validators.required,
                ],
              ),
              'email': FormControl(
                value: '',
                validators: [
                  Validators.required,
                  Validators.email,
                ],
              ),
              'password': FormControl(
                value: '',
                validators: [
                  Validators.required,
                  Validators.maxLength(8),
                ],
              ),
            },
          ),
        );

  static final provider = StateNotifierProvider.autoDispose<FormNotifier, FormGroup>((ref) {
    return FormNotifier();
  });
}

class LoginScreenTest extends StatelessWidget {
  const LoginScreenTest({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        body: Center(
          child: SizedBox(
            width: 400,
            child: Column(
              children: [
                TabBar(
                  tabs: [
                    Tab(text: 'Tab1'),
                    Tab(text: 'Tab2'),
                  ],
                ),
                const _ContentView(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ContentView extends StatelessWidget {
  const _ContentView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: DefaultTabController.of(context)!,
      builder: (context, child) {
        final index = DefaultTabController.of(context)!.index;
        return IndexedStack(
          index: index,
          children: [
            Visibility(
              visible: index == 0,
              maintainState: true,
              child: FirstTab(),
            ),
            Visibility(
              visible: index == 1,
              maintainState: true,
              child: SecondTab(),
            ),
          ],
        );
      },
    );
  }
}

class FirstTab extends ConsumerWidget {
  const FirstTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final form = ref.watch(FormNotifier.provider);

    return ReactiveForm(
      formGroup: form,
      child: Column(
        children: [
          ReactiveTextField(
            formControlName: 'email',
            decoration: InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            formControlName: 'password',
            decoration: InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 10),
        ],
      ),
    );
  }
}

class SecondTab extends ConsumerWidget {
  const SecondTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final form = ref.watch(FormNotifier.provider);

    return ReactiveForm(
      formGroup: form,
      child: Column(
        children: [
          ReactiveTextField(
            formControlName: 'name',
            decoration: InputDecoration(labelText: 'Name'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            formControlName: 'email',
            decoration: InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            formControlName: 'password',
            decoration: InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 10),
        ],
      ),
    );
  }
}

https://user-images.githubusercontent.com/11982812/180764472-23fd9dda-55d0-49fe-be7b-7d9d4ee6b337.mov

joanpablo commented 2 years ago

Hi @rebaz94,

Thanks for giving us all these details they are really useful. I will take a look at the code.

There is just one thing I can tell and this is that a control does not handle the focus on multiple widgets bound to it. In the same way only one control can focus at a time, a control can manages the focus of a control at a time, you can bind a control with multiple widgets but it will handle focus of the last registered recative widget.

I will take a look and see what is the real issue in the above sample code.

joanpablo commented 2 years ago

@rebaz94 In your use case (the first video SignIn and Signup) I advise you to have 2 different FormGroups, one for SignIn and another for SignUp. Anyway, I will try to figure out what is really happening to give you a better explanation.

rebaz94 commented 2 years ago

The problem is the focus that does not trigger when widget invisible but exist in the widget tree. I don't think making two FormGroup solve the problem as the focus does not react to changes until full widget rebuilt.

joanpablo commented 2 years ago

Hi @rebaz94,

Yes, creating 2 separated FormGroup definitely solves the issue.

The problem is that a FormControl can only handle one FocusNode at a time (the last registered ReactiveTextField).

Your first ReactiveTextField (the sign-in) will show the error only when email control.invalid && control.touched but the email control will never be touched unless you touch your second ReactiveTextField (the sign-up). Or the screen is completely rebuilt and forced to register again a new FocusNode with the email control. In that case the control will start handling the sign-in text field.

I will try to find a solution in which a control can handle multiple FocusNodes at the same time but meanwhile use 2 different FormGroups, it doesn't matter if they are nested FormGroups but they must be 2.

rebaz94 commented 2 years ago

Thank you for the help. I will try that

joanpablo commented 2 years ago

Another temporary solution, in case you still want to use the same FormGroup for both views, is to override the default showErrors() for the widgets and use for example control.invalid && control.dirty

That will show the error as soon as you interact with the ReactiveTextFeld (as soon as you start typing).

You can also (optionaly) combine this with another flag. For example, the first time the user enters the email and password you are not going to show errors, but when a user clicks on the button Sign-In then you set the flag to true and rebuild view: So you override the the showErrors() for something like control.invalid && control.dirty && _submitAttempted

joanpablo commented 2 years ago

Please let me know which of the last 2 options I gave you was good to you @rebaz94

rebaz94 commented 2 years ago

Hi @joanpablo I tried the same example above but creating 2 FormGroup. it will show the error as soon as focus change but at the same time if you change to second tab and email is invalid error will show immediately, it should not happen because its from other form group, also its not a good UX and really I don't want to show error as soon as focus change, only show error when form submitted and if form has error show error for invalid field and when became focus again or changed clear the error for that field (I don't know if it possible)

here the video using 2 form group

https://user-images.githubusercontent.com/11982812/183486972-5f3e10e6-78b0-4ec8-9b26-adc48f9fb5aa.mov

joanpablo commented 2 years ago

Hi @rebaz94 I will use your code and will reproduce your use case using 2 diferent form groups. The second tab should not show any error until you interact with it. Remember also that you can use control.invalid && control.dirty && _submitAttempted

And also can use control.invalid && control.dirty && control.touched

joanpablo commented 2 years ago

If the second view (sign-up) is showing the errors without a direct interaction of the user then it is because you are still using the same FormGroup for both views

rebaz94 commented 2 years ago

If the second view (sign-up) is showing the errors without a direct interaction of the user then it is because you are still using the same FormGroup for both views

No, I tested with different FormGroup. Just use the code above and provide a list of form and in each tab use the form you want.

Providing showErrors for every field is not great as you need to maintain form submission state in order to show error or not..

joanpablo commented 2 years ago

Have you assigned a different Key() for each ReactiveTextField?

rebaz94 commented 2 years ago

Have you assigned a different Key() for each ReactiveTextField?

It's same as before after providing Key for both ReactiveTextField and ReactiveForm, also pressing tab does not change focus to password field!

joanpablo commented 2 years ago
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reactive_forms/reactive_forms.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData.dark(),
      themeMode: ThemeMode.dark,
      home: const ProviderScope(
        child: LoginScreenTest(),
      ),
    ),
  );
}

class FormFields {
  static String signIn = 'signIn';
  static String signUp = 'signUp';
}

class FormNotifier extends StateNotifier<FormGroup> {
  FormNotifier()
      : super(
          FormGroup(
            {
              FormFields.signUp: FormGroup({
                'name': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                  ],
                ),
                'email': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.email,
                  ],
                ),
                'password': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.maxLength(8),
                  ],
                ),
              }),
              FormFields.signIn: FormGroup({
                'email': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.email,
                  ],
                ),
                'password': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.maxLength(8),
                  ],
                ),
              })
            },
          ),
        );

  FormGroup get signInForm => state.control(FormFields.signIn) as FormGroup;

  FormGroup get signUnForm => state.control(FormFields.signUp) as FormGroup;

  static final provider =
      StateNotifierProvider.autoDispose<FormNotifier, FormGroup>((ref) {
    return FormNotifier();
  });
}

class LoginScreenTest extends StatelessWidget {
  const LoginScreenTest({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        body: Center(
          child: SizedBox(
            width: 400,
            child: Column(
              children: const [
                TabBar(
                  tabs: [
                    Tab(text: 'Tab1'),
                    Tab(text: 'Tab2'),
                  ],
                ),
                _ContentView(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ContentView extends StatelessWidget {
  const _ContentView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: DefaultTabController.of(context)!,
      builder: (context, child) {
        final index = DefaultTabController.of(context)!.index;
        return IndexedStack(
          index: index,
          children: [
            Visibility(
              visible: index == 0,
              maintainState: true,
              child: const FirstTab(),
            ),
            Visibility(
              visible: index == 1,
              maintainState: true,
              child: const SecondTab(),
            ),
          ],
        );
      },
    );
  }
}

class FirstTab extends ConsumerWidget {
  const FirstTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final form = ref.watch(FormNotifier.provider);

    return ReactiveForm(
      formGroup: form.control(FormFields.signIn) as FormGroup,
      child: Column(
        children: [
          ReactiveTextField(
            key: const Key('sign-in-email'),
            formControlName: 'email',
            decoration: const InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            key: const Key('sign-in-password'),
            formControlName: 'password',
            decoration: const InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 10),
        ],
      ),
    );
  }
}

class SecondTab extends ConsumerWidget {
  const SecondTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final form = ref.watch(FormNotifier.provider);

    return ReactiveForm(
      formGroup: form.control(FormFields.signUp) as FormGroup,
      child: Column(
        children: [
          ReactiveTextField(
            key: const Key('sign-up-name'),
            formControlName: 'name',
            decoration: const InputDecoration(labelText: 'Name'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            key: const Key('sign-up-email'),
            formControlName: 'email',
            decoration: const InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            key: const Key('sign-up-password'),
            formControlName: 'password',
            decoration: const InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 10),
        ],
      ),
    );
  }
}

chrome-capture-2022-7-8

rebaz94 commented 2 years ago

Copied you code & test it on Mac still shows same problem

https://user-images.githubusercontent.com/11982812/183510952-6b72e05e-8a66-47bd-93db-40147bde3f6e.mov

rebaz94 commented 2 years ago

This previous code I test it with Key

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reactive_forms/reactive_forms.dart';

class FormNotifier extends StateNotifier<List<FormGroup>> {
  FormNotifier()
      : super(
          [
            FormGroup(
              {
                'email': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.email,
                  ],
                ),
                'password': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.maxLength(8),
                  ],
                ),
              },
            ),
            FormGroup(
              {
                'name': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                  ],
                ),
                'email': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.email,
                  ],
                ),
                'password': FormControl(
                  value: '',
                  validators: [
                    Validators.required,
                    Validators.maxLength(8),
                  ],
                ),
              },
            )
          ],
        );

  static final provider = StateNotifierProvider.autoDispose<FormNotifier, List<FormGroup>>((ref) {
    return FormNotifier();
  });
}

class LoginScreenTest extends StatelessWidget {
  const LoginScreenTest({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        body: Center(
          child: SizedBox(
            width: 400,
            child: Column(
              children: [
                TabBar(
                  tabs: [
                    Tab(text: 'Tab1'),
                    Tab(text: 'Tab2'),
                  ],
                ),
                const _ContentView(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ContentView extends StatelessWidget {
  const _ContentView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: DefaultTabController.of(context)!,
      builder: (context, child) {
        final index = DefaultTabController.of(context)!.index;
        return IndexedStack(
          index: index,
          children: [
            Visibility(
              visible: index == 0,
              maintainState: true,
              child: FirstTab(),
            ),
            Visibility(
              visible: index == 1,
              maintainState: true,
              child: SecondTab(),
            ),
          ],
        );
      },
    );
  }
}

class FirstTab extends ConsumerWidget {
  const FirstTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final form = ref.watch(FormNotifier.provider).first;

    return ReactiveForm(
      formGroup: form,
      child: Column(
        children: [
          ReactiveTextField(
            key: ValueKey('t1email'),
            formControlName: 'email',
            decoration: InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            key: ValueKey('t1password'),
            formControlName: 'password',
            decoration: InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 10),
        ],
      ),
    );
  }
}

class SecondTab extends ConsumerWidget {
  const SecondTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final form = ref.watch(FormNotifier.provider).last;

    return ReactiveForm(
      formGroup: form,
      child: Column(
        children: [
          ReactiveTextField(
            formControlName: 'name',
            decoration: InputDecoration(labelText: 'Name'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            key: ValueKey('t2email'),
            formControlName: 'email',
            decoration: InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 10),
          ReactiveTextField(
            key: ValueKey('t2password'),
            formControlName: 'password',
            decoration: InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 10),
        ],
      ),
    );
  }
}
joanpablo commented 2 years ago

I have tested on Linux and Web, and I don't believe it is a Platform issue. Can you copy/paste again your code here or share a GitHub project to download?

Ok let me check again your code

joanpablo commented 2 years ago

Your code is not the same that mine, please copy/paste mine and test it. anyway, I will copy/paste yours again and make the adjustments.

rebaz94 commented 2 years ago

Your code is not the same that mine, please copy/paste mine and test it. anyway, I will copy/paste yours again and make the adjustments.

I said before, I am copied your code and test it on Desktop, the problem is same. the difference is not matter about creating FormGroup.

I tested on web and it has same problem

joanpablo commented 2 years ago

Do you still see the problem in the Video I uploaded? I don't understand what problem you refer to. Maybe there is a misunderstanding.

joanpablo commented 2 years ago

Do you see your issue here in this video?? chrome-capture-2022-7-8

Can you explain what issue you see here?

rebaz94 commented 2 years ago

No, in you video there is no problem but I copied your code and test it, error will be shown before interacting the widget.

tested on web

https://user-images.githubusercontent.com/11982812/183515607-9c64914e-cb0d-491e-837d-0cf6fccab3fb.mov

I only talked about this problem in video, also in previous comment I said

I don't want to show error as soon as focus change, only show error when form submitted and if form has error show error for invalid field and when became focus again or changed clear the error for that field (I don't know if it possible)

this is another issue you can ignore it for now

rebaz94 commented 2 years ago

here is repo https://github.com/rebaz94/reactive_form_bug

joanpablo commented 2 years ago

Here is mine. Please git clone my sample repo and just run it (without modifications) and let me know. Meanwhile, I will test your repo.

https://github.com/joanpablo/simple_tab_sample

joanpablo commented 2 years ago

here is repo https://github.com/rebaz94/reactive_form_bug

I have cloned your repo and run it using

flutter run -d chrome

And everything works correctly, fine. The same result as my Video.

rebaz94 commented 2 years ago

Here is mine. Please git clone my sample repo and just run it (without modifications) and let me know. Meanwhile, I will test your repo.

https://github.com/joanpablo/simple_tab_sample

I test and work without problem! Really I'm confused even tested my repo again, does not have any problem

rebaz94 commented 2 years ago

ok using the Key make it work, is it a valid solution for that or a workaround?

what about this?

I don't want to show error as soon as focus change, only show error when form submitted and if form has error show error for invalid field and when became focus again or changed clear the error for that field (I don't know if it possible)

joanpablo commented 2 years ago

I'm glad to hear that the misunderstanding is now solved :sweat_smile:

Now, regarding the other behavior (about only show errors after clicking the submit button), it requires to override the showErrors() method with control.invalid && control.dirty && _submitAttempted. _submitAttempted is a variable that you need to declare somewhere and update it with true after the user clicks the submit button, and then refresh the UI so that errors show up.

rebaz94 commented 2 years ago

Thank you so much. really don't know why this happen, maybe problem with flutter caching the build.

what about adding new property to FormControl or ReactiveFieldWidget to reset error when value changed? this make things a lot easier and its common ui pattern in web, specially when you have a lot of field overriding showErrors for every field is not good.

joanpablo commented 2 years ago

what about adding new property to FormControl or ReactiveFieldWidget to reset error when value changed?

would you mind elaborating more on this idea?

rebaz94 commented 2 years ago

@joanpablo For example when you have a sign in form, user fill email and password and only when you submit it show the error, and latter when you change field the error disappears immediately.

here what I'm talking, so if we have a property like resetErrorWhenChagned, this will make a nice form especially when you have a lot fields

https://user-images.githubusercontent.com/11982812/183536459-f46b8de2-fbf7-4706-8bf0-2d20b5ba9a0b.mov

joanpablo commented 2 years ago

Hi @rebaz94,

I will think about it, thanks for the suggestion. I'm planning to include something like autovalidate: true|false to the FormGroup.

Meanwhile you can override the onChanged of the ReactiveTextField and reset errors.

ReactiveTextField (
    onChanged: (control) => control.setErrors({});
)
rebaz94 commented 2 years ago

Thank you @joanpablo You can close the issue

vicenterusso commented 2 years ago

I will think about it, thanks for the suggestion. I'm planning to include something like autovalidate: true|false to the

Came here to find out how to do that. +1 for autovalidate: true|false

babaralishah commented 1 year ago

Hi I am on Angular

And using the below regex:

'^[a-z0-9]+([._-]?[a-z0-9]+)+@[a-z0-9]+([._-]?[a-z0-9]+)+\\.[a-z]{2,3}$'

But its not fulfilling my requirement, my requirement is below:

saa5@me-d_d_u.co

please anyone tell me, why this regex is not working for my above mentioned case.

Regards Babar Ali Shah