reactive_forms icon indicating copy to clipboard operation
reactive_forms copied to clipboard

Changing focus does not validate the control

Open rebaz94 opened this issue 3 years ago • 46 comments

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

rebaz94 avatar Jul 23 '22 16:07 rebaz94

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

vasilich6107 avatar Jul 24 '22 12:07 vasilich6107

@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?

rebaz94 avatar Jul 24 '22 20:07 rebaz94

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?

joanpablo avatar Jul 24 '22 20:07 joanpablo

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: '[email protected]',
  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

class SwiftyFormBuilder<N extends BaseStateNotifier<M, A>, 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>(/*M*/ dynamic result) {
    return result as FormGroup;
  }

  static void defaultOnSetupArgs(BaseStateNotifier notifier, WidgetRef ref) {}

  final StateNotifierProviderOverrideMixin<StateNotifier<Result<M>>, Result<M>> 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<M> value)? onChange;

  final FormGroup Function(M data) formSelector;

  BaseStateNotifier<M, Object> notifier(WidgetRef ref) {
    return ref.read(provider.notifier) as BaseStateNotifier<M, Object>;
  }

  SwiftyForm<M, Object> swiftyForm(WidgetRef ref) {
    return ref.read(provider.notifier) as SwiftyForm<M, Object>;
  }

  @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<Result<M>>(provider, (_, value) {
        onChange!.call(context, value);
      });
    }
    return content;
  }

  String errorMessage(Result<M> state) {
    return state.maybeWhen(
      (data, _) => 'Failed',
      noInternet: () => 'No Internet',
      orElse: () => 'Failed to load data, please try again',
    );
  }
}

rebaz94 avatar Jul 24 '22 21:07 rebaz94

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 avatar Jul 24 '22 21:07 vasilich6107

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

vasilich6107 avatar Jul 24 '22 21:07 vasilich6107

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 avatar Jul 24 '22 21:07 rebaz94

@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

rebaz94 avatar Jul 24 '22 23:07 rebaz94

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 avatar Jul 25 '22 10:07 joanpablo

@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?

joanpablo avatar Jul 25 '22 10:07 joanpablo

@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

rebaz94 avatar Jul 25 '22 11:07 rebaz94

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 avatar Aug 07 '22 22:08 joanpablo

@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.

joanpablo avatar Aug 07 '22 22:08 joanpablo

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.

rebaz94 avatar Aug 08 '22 15:08 rebaz94

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.

joanpablo avatar Aug 08 '22 17:08 joanpablo

Thank you for the help. I will try that

rebaz94 avatar Aug 08 '22 17:08 rebaz94

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 avatar Aug 08 '22 17:08 joanpablo

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

joanpablo avatar Aug 08 '22 17:08 joanpablo

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

rebaz94 avatar Aug 08 '22 18:08 rebaz94

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 avatar Aug 08 '22 19:08 joanpablo

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

joanpablo avatar Aug 08 '22 19:08 joanpablo

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..

rebaz94 avatar Aug 08 '22 20:08 rebaz94

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

joanpablo avatar Aug 08 '22 20:08 joanpablo

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!

rebaz94 avatar Aug 08 '22 20:08 rebaz94

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

joanpablo avatar Aug 08 '22 20:08 joanpablo

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 avatar Aug 08 '22 20:08 rebaz94

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),
        ],
      ),
    );
  }
}

rebaz94 avatar Aug 08 '22 20:08 rebaz94

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 avatar Aug 08 '22 20:08 joanpablo

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.

joanpablo avatar Aug 08 '22 20:08 joanpablo

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

rebaz94 avatar Aug 08 '22 21:08 rebaz94