Open rebaz94 opened 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
@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?
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?
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
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)
@rebaz94 could you try to run example
project with login form. It should work fine despite of OS
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.
@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
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.
@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 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),
],
),
);
}
}
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.
@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.
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.
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.
Thank you for the help. I will try that
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
Please let me know which of the last 2 options I gave you was good to you @rebaz94
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
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
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
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..
Have you assigned a different Key() for each ReactiveTextField?
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!
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),
],
),
);
}
}
Copied you code & test it on Mac still shows same problem
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),
],
),
);
}
}
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
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.
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
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.
Do you see your issue here in this video??
Can you explain what issue you see here?
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
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
here is repo https://github.com/rebaz94/reactive_form_bug
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.
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.
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.
I test and work without problem! Really I'm confused even tested my repo again, does not have any problem
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)
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.
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.
what about adding new property to
FormControl
orReactiveFieldWidget
to reset error when value changed?
would you mind elaborating more on this idea?
@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
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({});
)
Thank you @joanpablo You can close the issue
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
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
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.
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 andmarkAsTouched
and that's work but I think the library it should do that ?Thanks