artflutter / reactive_forms_generator

Other
82 stars 22 forks source link

Data are not binding #122

Closed marcmacias96 closed 1 year ago

marcmacias96 commented 1 year ago

Screenshot

Debug Screen
Screenshot 2023-06-25 at 16 29 40 image

Model


@freezed
@ReactiveFormAnnotation()
class Credentials with _$Credentials {
  const factory Credentials({
    @FormControlAnnotation(validators: [EmailValidator()])
     String? email,
    @FormControlAnnotation(validators: [RequiredValidator()])
     String? password,
    @FormControlAnnotation(validators: [RequiredValidator()])
     bool? rememberMe,
  }) = _Credentials;
}

State

@freezed
class LoginState with _$LoginState {
  const factory LoginState({
    FlowState? status,
    String? errorMessage,
    required Credentials form,
  }) = _LoginState;

  factory LoginState.initial() => LoginState(
        form: Credentials(),
      );
}

Widget

CredentialsFormBuilder(
          model: state.form,
          builder: (
            BuildContext context,
            CredentialsForm formModel,
            Widget? child,
          ) {
            return Padding(
              padding: const EdgeInsets.all(12.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  ReactiveTextField<String>(
                    formControl: formModel.emailControl,
                    keyboardType: TextInputType.emailAddress,
                    style: Theme.of(context).textTheme.bodyLarge,
                    decoration: decoration(context),
                  ),
                  SizedBox(
                    height: MediaQuery.of(context).size.height * 0.03,
                  ),
                  ReactiveTextField<String>(
                    formControl: formModel.passwordControl,
                    keyboardType: TextInputType.text,
                    obscureText: _passwordConfVisible,
                    style: Theme.of(context).textTheme.bodyLarge,
                    decoration: passDecorator(context),
                  ),
                  SizedBox(
                    height: MediaQuery.of(context).size.height * 0.033,
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Row(
                        children: [
                          ReactiveSwitch.adaptive(
                            formControl: formModel.rememberMeControl,
                          ),
                          Text(
                            LoginStrings.recuerdame,
                            style: Theme.of(context)
                                .textTheme
                                .labelLarge!
                                .copyWith(
                                  color: Theme.of(context).colorScheme.primary,
                                ),
                          ),
                        ],
                      ),
                      TextButton(
                        onPressed: () {
                          GoRouter.of(context).push(forgotPasswordRoute);
                        },
                        child: Text(
                          LoginStrings.recuperarSesion,
                          style: Theme.of(context)
                              .textTheme
                              .labelLarge!
                              .copyWith(
                                color: Theme.of(context).colorScheme.primary,
                              ),
                        ),
                      ),
                    ],
                  ),

                  // Iniciar sesión button
                  SizedBox(
                    height: MediaQuery.of(context).size.height * 0.066,
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      ReactiveCredentialsFormConsumer(
                          builder: (context, formModel, child) {
                        return FilledButton(
                          style: ButtonStyle(
                            backgroundColor: MaterialStateColor.resolveWith(
                              (states) => Theme.of(context)
                                  .colorScheme
                                  .tertiaryContainer,
                            ),
                          ),
                          onPressed: (formModel.form.valid ||
                                  state.status is ContentState)
                              ? () {
                                  context.read<LoginCubit>().startSession();
                                }
                              : () {
                                  formModel.form.markAllAsTouched();
                                },
                          child: SizedBox(
                            width: MediaQuery.of(context).size.width * 0.66,
                            height: 40,
                            child: Center(
                              child: Text(
                                state.status is LoadingState
                                    ? LoginStrings.loading
                                    : LoginStrings.iniciarSesion,
                                style: Theme.of(context).textTheme.labelLarge,
                              ),
                            ),
                          ),
                        );
                      }),
                    ],
                  ),
                ],
              ),
            );
          },
        ),

Note

Don't work without frezzed too

vasilich6107 commented 1 year ago
image
vasilich6107 commented 1 year ago

here is a code

CredentialsFormBuilder(
        // model: state.form,
        builder: (
          BuildContext context,
          CredentialsForm formModel,
          Widget? child,
        ) {
          return Padding(
            padding: const EdgeInsets.all(12.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ReactiveTextField<String>(
                  formControl: formModel.emailControl,
                  keyboardType: TextInputType.emailAddress,
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.03,
                ),
                ReactiveTextField<String>(
                  formControl: formModel.passwordControl,
                  keyboardType: TextInputType.text,
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.033,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Row(
                      children: [
                        ReactiveSwitch.adaptive(
                          formControl: formModel.rememberMeControl,
                        ),
                        Text(
                          'LoginStrings.recuerdame',
                          style: Theme.of(context)
                              .textTheme
                              .labelLarge!
                              .copyWith(
                                color: Theme.of(context).colorScheme.primary,
                              ),
                        ),
                      ],
                    ),
                    TextButton(
                      onPressed: () {},
                      child: Text(
                        'LoginStrings.recuperarSesion',
                        style: Theme.of(context).textTheme.labelLarge!.copyWith(
                              color: Theme.of(context).colorScheme.primary,
                            ),
                      ),
                    ),
                  ],
                ),

                // Iniciar sesión button
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.066,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ReactiveCredentialsFormConsumer(
                        builder: (context, formModel, child) {
                      return FilledButton(
                        style: ButtonStyle(
                          backgroundColor: MaterialStateColor.resolveWith(
                            (states) =>
                                Theme.of(context).colorScheme.tertiaryContainer,
                          ),
                        ),
                        // onPressed: (formModel.form.valid ||
                        //     state.status is ContentState)
                        //     ? () {
                        //   context.read<LoginCubit>().startSession();
                        // }
                        //     : () {
                        //   formModel.form.markAllAsTouched();
                        // },
                        onPressed: () {
                          formModel.form.markAllAsTouched();
                          print(formModel.model);
                        },
                        child: SizedBox(
                          width: MediaQuery.of(context).size.width * 0.66,
                          height: 40,
                          child: Center(
                            child: Text('asdfasdf'),
                          ),
                        ),
                      );
                    }),
                  ],
                ),
              ],
            ),
          );
        },
      )
marcmacias96 commented 1 year ago

@vasilich6107 It is correct what you show me, but what I don't understand is why it only mutates the formModel instance inside the constructor and why it doesn't mutate the instance I have in my Cubit, it is supposed to be connected with my Credentials model, as it happens when I declare my fb.group without using reactive_forms_generator.

marcmacias96 commented 1 year ago
cubit-state builder
image image
vasilich6107 commented 1 year ago

It should not mutate your cubit. It is designed to work as immutable

You have form. You init the form You enter some values At the end you are getting result fromModel.model

You have to mutate your cubit by yourself

marcmacias96 commented 1 year ago

@vasilich6107 But doesn't this kind of break one of the goals of reactive_forms ? which is to keep my form connected to my state manager? Or can you explain to me the context of why this changes, it doesn't seem logical to me.

// Using ReactiveForm in a StatelessWidget and resolve the FormGroup from a provider
class SignInForm extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<SignInViewModel>(context, listen: false);

    return ReactiveForm(
      formGroup: viewModel.form,
      child: ReactiveTextField(
        formControlName: 'email',
      ),
    );
  }
}
marcmacias96 commented 1 year ago

That being the case I would rather stick with pure reactive_forms and just do a Credentials.fromJson() and solve what this package does, but in my Cubit in one line, I hope you get my point.

vasilich6107 commented 1 year ago

I didn't hear about this goal of RF Anyway - magical object mutation considered as antipattern Could you clarify why do you need this kind of featyure?

vasilich6107 commented 1 year ago

and solve what this package does

This is not the goal of the package. This package provides typed entities for the forms.

If you are comfortable working with untyped objects - you do not need this package

vasilich6107 commented 1 year ago

RF will also create controls from you Map<String, dynamic> and will maintain it's own structure I'm not sure that you'll be able to get the result you are trying to reach

vasilich6107 commented 1 year ago

which is to keep my form connected to my state manager?

RF is a state manager of it's own state. It is not meant to be connected to third party state management

marcmacias96 commented 1 year ago

I explain the context

I tried reactive_forms without reactive_forms_builder and I was thrilled, but once I made my solution I realized its shortcomings and is that in the end the model I get is a Map<String,dynamic> so the generation of typed seemed to me spectacular, I proceeded to migrate all the code to reactive_forms_builder and happened what I showed you, I stopped mutating the instance that I had in the state of the cubit, and what you tell me is that now the class that generates my FormGrup is immutable and this changes as I had structured my code.

First Solution

State of Cubit

@freezed
class LoginState with _$LoginState {
  const factory LoginState({
    FlowState? status,
    String? errorMessage,
    required FormGroup form,
  }) = _LoginState;

  factory LoginState.initial() => LoginState(
      form: fb.group({
        'email': [ Validators.required, Validators.email],
        'password': [Validators.required, Validators.minLength(6)],
        'remember': true,
      }));
}

Cubit method

  Future<void> startSession() async {
    emit(state.copyWith(status: LoadingState()));
    var response = await _repository.signInWithEmailAndPassword(
      email: state.form.value['email'] as String,
      password: state.form.value['password'] as String,
    );
    ........

Widget

BlocProvider(
      create: (context) => getIt<LoginCubit>(),
      child: BlocConsumer<LoginCubit, LoginState>(
        listener: (_, state) {
          if (state.status is ContentState) {
            Future.delayed(const Duration(milliseconds: 200), () {
              GoRouter.of(context).replace(menu);
            });
          } else if (state.status is ErrorState) {
            BotToast.showText(
              text: state.status?.getMessage() ?? "Ocurrió un error inesperado",
              contentColor: Colors.red,
              textStyle: const TextStyle(color: Colors.white, fontSize: 18),

            );
          }
        },
        builder: (_, state) => ReactiveForm(
          formGroup: state.form,
          child: Padding(
            padding: const EdgeInsets.all(12.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ReactiveTextField(
                  formControlName: 'email',
                  keyboardType: TextInputType.emailAddress,
                  style: Theme.of(context).textTheme.bodyLarge,
                  decoration: decoration(context),
                  validationMessages: {
                    'required': (_) => LoginStrings.requiredField,
                    'email': (_) => LoginStrings.invalidEmail,
                  },
                ),
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.03,
                ),
                ReactiveTextField(
                  formControlName: 'password',
                  keyboardType: TextInputType.text,
                  obscureText: _passwordConfVisible,
                  style: Theme.of(context).textTheme.bodyLarge,
                  decoration: passDecorator(context),
                  validationMessages: {
                    'required': (_) => LoginStrings.requiredField,
                    'minLength': (_) => LoginStrings.invalidPasswordLength,
                  },
                ),
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.033,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Row(
                      children: [
                        ReactiveSwitch(
                          formControlName: 'remember',
                          activeColor: Theme.of(context).colorScheme.primary,
                        ),
                        Text(
                          LoginStrings.recuerdame,
                          style: Theme.of(context)
                              .textTheme
                              .labelLarge!
                              .copyWith(
                                color: Theme.of(context).colorScheme.primary,
                              ),
                        ),
                      ],
                    ),
                    TextButton(
                      onPressed: () {
                        GoRouter.of(context).push(forgotPasswordRoute);
                      },
                      child: Text(
                        LoginStrings.recuperarSesion,
                        style: Theme.of(context).textTheme.labelLarge!.copyWith(
                              color: Theme.of(context).colorScheme.primary,
                            ),
                      ),
                    ),
                  ],
                ),

                // Iniciar sesión button
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.066,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ReactiveFormConsumer(builder: (context, form, child) {
                      return FilledButton(
                        style: ButtonStyle(
                            backgroundColor: MaterialStateColor.resolveWith(
                                (states) => Theme.of(context)
                                    .colorScheme
                                    .tertiaryContainer)),
                        onPressed: form.valid && state.status is ContentState
                            ? () {
                                context.read<LoginCubit>().startSession();
                              }
                            : () => form.markAllAsTouched(),
                        child: SizedBox(
                          width: MediaQuery.of(context).size.width * 0.66,
                          height: 40,
                          child: Center(
                            child: Text(
                              state.status is LoadingState
                                  ? LoginStrings.loading
                                  : LoginStrings.iniciarSesion,
                              style: Theme.of(context).textTheme.labelLarge,
                            ),
                          ),
                        ),
                      );
                    }),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );

Once I migrated to reactive_forms_generator my login method stopped working because my form model stopped mutating.

What I don't understand is why, now I have to send my mutated object to the block when in my block is the original instance and it is supposed to be connected to it as it happens in reactive_forms.

marcmacias96 commented 1 year ago

I understand that the easy solution is to send the model as a parameter of my envet and mutate it manually, and delete the Credentials instance of my state and just create it in the Widget and that solves everything but I don't know, it's quite strange, it makes me feel that the bilding of properties was lost and became a typed state manager.

marcmacias96 commented 1 year ago

RF is a state manager of it's own state. It is not meant to be connected to third party state management

Reactive Forms + Provider plugin 💪 # Although Reactive Forms can be used with any state management library or even without any one at all, Reactive Forms gets its maximum potential when is used in combination with a state management library like the Provider plugin.

This way you can separate UI logic from business logic and you can define the FormGroup inside a business logic class and then exposes that class to widgets with mechanism like the one Provider plugin brings.

vasilich6107 commented 1 year ago

I understand what you are doing. The overall approach looks like antipattern.

your state should not depend on form.

You have *Consumer widget which is updated on each form change. You can mutate your cubit state in Consumer widget

marcmacias96 commented 1 year ago

I understand that the easy solution is to send the model as a parameter of my envet and mutate it manually, and delete the Credentials instance of my state and just create it in the Widget and that solves everything but I don't know, it's quite strange, it makes me feel that the bilding of properties was lost and became a typed state manager.

So the right thing to do is that?

vasilich6107 commented 1 year ago

I do not know your full flow. Form has it's own state so no need for +1 state management.

When your form is valid you can get model and submit it.

vasilich6107 commented 1 year ago

Let's see what we have - RF - manages state and Riverpod manages the state. 2 state manages for one form)

vasilich6107 commented 1 year ago

Reactive Forms can be used with any state management library or even without any one at all

or even without any one at all is a key to success)

marcmacias96 commented 1 year ago

I understand the point, thank you for explaining, I close it.

github-actions[bot] commented 1 year ago

Hi @marcmacias96! Your issue has been closed. If we were helpful don't forget to star the repo.

Please check our reactive_forms widget section https://github.com/artflutter/reactive_forms_widgets

We would appreciate sponsorship subscription or one time donation https://github.com/sponsors/artflutter

WoodyMKD commented 7 months ago

reactive_forms now has two seperate widgets: https://pub.dev/packages/reactive_forms#reactiveform-vs-reactiveformbuilder-which-one

@vasilich6107 With the generator we are forced to use the stateful widget which keeps the formgroup as state. Is there any way we can get two-way binding & type-safety with our own state management (bloc / provider) ?

I don't consider this anti-pattern especially in a clean architecture where the bloc handles all the presentation logic. In complex forms where the validators often change or there are conditional values (one field value, changes another field value), it would be amazing to handle them through the bloc for example instead of polluting the widgets.

WoodyMKD commented 7 months ago

Reactive Forms can be used with any state management library or even without any one at all

or even without any one at all is a key to success)

The new documentation now prefers using a provider / bloc.

Although Reactive Forms can be used with any state management library or even without any one at all, Reactive Forms gets its maximum potential when is used in combination with a state management library like the Provider plugin.

BenjiFarquhar commented 6 months ago

I've been doing this from my business logic class (Riverpod standard Provider, not a Notifier):

  @override
  void initState(BuildContext context, SignInForm formModel) {
    if (this.formModel == null) {
      // formModel.form.valueChanges.listen((_) {
      this.formModel = formModel;
      // });
    }
  }

I don't need the valueChanges.listen line of code, that's why I commented it out, but depending on your state management package and situation, you may. formModel in initState will always have the latest updates as it is a reference to a location in memory, not a value type. This code is called from the widget FormBuilder.initState.

Eg:

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final vm = ref.watch(signInVmProvider);

    return SignInFormBuilder(
      initState: vm.initState,
      builder: (context, formModel, child) {
        return YourFormControlsHere....