Skip to content

artflutter/reactive_forms_generator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pub

reactive_forms_generator

ReactiveFormsGenerator

Welcome to ReactiveFormsGenerator, code generator for reactive_forms which will save you tons of time and make your forms type safe.

There is no reason to write code manually! Let the code generation work for you.

Sponsors

Table of Contents

Motivation

One of the goals of this package is to make reactive_forms package even more cool and fun to use.

Let's see what issues this package tries to mitigate.

Here is how typical reactive_forms form looks like

/// form instantiation
FormGroup buildForm() =>
    fb.group(<String, Object>{
      'email': FormControl<String>(
        validators: [Validators.required, Validators.email],
      ),
      'password': ['', Validators.required, Validators.minLength(8)],
      'rememberMe': false,
    });

/// form itself
final form = ReactiveFormBuilder(
  form: buildForm,
  builder: (context, form, child) {
    return Column(
      children: [
        ReactiveTextField<String>(
          formControlName: 'email',
        ),
        const SizedBox(height: 16.0),
        ReactiveTextField<String>(
          formControlName: 'password',
        ),
        const SizedBox(height: 16.0),
        ElevatedButton(
          onPressed: () {
            if (form.valid) {
              print(form.value);
            } else {
              form.markAllAsTouched();
            }
          },
          child: const Text('Sign Up'),
        ),
        ElevatedButton(
          onPressed: () =>
              form.resetState({
                'email': ControlState<String>(value: null),
                'password': ControlState<String>(value: null),
                'rememberMe': ControlState<bool>(value: false),
              }, removeFocus: true),
          child: const Text('Reset all'),
        ),
      ],
    );
  },
);
  1. First issue is String identifiers which are used to define fields. Technically you can extract them into separate class, enum or whatever you like. But this is manual work which you have to do each time you create the form. The other disadvantage is when you refer to any field by this String identifier you loose static type check. There is no way for static analyser to check if some random field name login is suitable to put in particular widget. So you can easily get the form which looks ok but fails to build due to the typo in field names and putting the login field into ReactiveCheckbox field. Isn't it better to let the code generation to do it for you?

  2. Second issue is output which is always Map<String, Object>. It is ok for languages like JS. But for the typed language you would prefer to get the output fom the form like model. And avoid manual type casting like this one.

final document = DocumentInput(
  subTypeId: form.value["subType"] as DocumentSubTypeMixin,
  documentNumber: form.value["documentNumber"] as String,
  countryIsoCode: form.value["country"] as CountryMixin,
  countryOfIssueIsoCode: form.value["country"] as CountryMixin,
  issueDate: form.value["issueDate"] as DateTime,
  vesselId: form.value["vessel"] as VesselMixin,
);

These are two main issues that forced me to write this generator. In the next chapters of documentation you'll see how we define and annotate the model which describes the form state and how easy and elegant it works with a bit of magic from code generation.

How to use

Minimum Requirements

  • Dart SDK: >=2.12.0 <3.0.0
  • Flutter: >= 2.2.0

Installation

To use [reactive_forms_generator], you will need your typical [build_runner]/code-generator setup.
First, install [build_runner] and [reactive_forms_generator] by adding them to your pubspec.yaml file:

# pubspec.yaml
dependencies:
  reactive_forms:
  reactive_forms_annotations:

dev_dependencies:
  build_runner:
  reactive_forms_generator:

This installs three packages:

Ignore lint warnings on generated files

It is likely that the code generated by [reactive_forms_generator] will cause your linter to report warnings.

The solution to this problem is to tell the linter to ignore generated files, by modifying your analysis_options.yaml:

analyzer:
  exclude:
    - "**/*.gform.dart"

Run the generator

Note that like most code-generators, reactive_forms_generator will need you to both import the annotation (meta) and use the part keyword on the top of your files.

Make sure you added required imports before running the generator

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

part 'my_file.gform.dart';

To run the code generator you have two possibilities:

  • If your package depends on Flutter:
    • flutter pub run build_runner build
  • If your package does not depend on Flutter:
    • dart pub run build_runner build

Features

Basics

Let's start from simple login form.

First we need to define our form model

Model

class Basic {
  final String email;

  final String password;

  Basic({this.email = '', this.password = ''});
}

We defined here a simple model with non-nullable email and password fields.

Annotation

The next step is to add annotations to help generator do his job.

import 'package:reactive_forms_annotations/reactive_forms_annotations.dart';

@Rf()
class Basic {
  final String email;

  final String password;

  Basic({
    @RfControl() this.email = '',

    @RfControl() this.password = '',
  });
}

ReactiveFormAnnotation - tells the generator that we want to Form based on this model. FormControlAnnotation - maps fields to control elements.

Validation

The login form should not proceed if there is any empty values. We need to modify our code to add some required validators.

import 'package:example/helpers.dart';
import 'package:reactive_forms_annotations/reactive_forms_annotations.dart';

@Rf()
class Basic {
  final String email;

  final String password;

  Basic({
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.email = '',
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.password = '',
  });
}

As far as we are using annotations - validators should be top level functions or static class fields or const constructors.

Now we are ready to run our form generator. .

Form

Let's build our form based on generated code

final form = BasicFormBuilder(
  // setup form model with initial data
  model: Basic(),
  // form builder
  builder: (context, formModel, child) {
    return Column(
      children: [
        ReactiveTextField<String>(
          formControl: formModel.emailControl,
          validationMessages: {
            ValidationMessage.required: (_) => 'The email must not be empty',
          },
          decoration: const InputDecoration(labelText: 'Email'),
        ),
        const SizedBox(height: 8.0),
        ReactiveTextField<String>(
          formControl: formModel.passwordControl,
          obscureText: true,
          validationMessages: {
            ValidationMessage.required: (_) => 'The password must not be empty',
          },
          textInputAction: TextInputAction.done,
          decoration: const InputDecoration(labelText: 'Password'),
        ),
        const SizedBox(height: 8.0),
        ReactiveBasicFormConsumer(
          builder: (context, form, child) {
            return ElevatedButton(
              child: Text('Submit'),
              onPressed: form.form.valid
                  ? () {
                print(form.model.email);
                print(form.model.password);
              }
                  : null,
            );
          },
        ),
      ],
    );
  },
);

BasicFormBuilder - generated widget that injects form into context ReactiveTextField - bundled text fields ReactiveBasicFormConsumer - generated widget that rebuilds upon form change

You can get access to prefilled form model by calling form.model.[field-name].

Dynamic forms with FormArray

The next example will show how to build dynamic forms. We will create a mailing list which will allow adding new email and basic validation.

Model

The model is pretty simple.

class MailingList {
  final List<String?> emailList;

  MailingList({
    this.emailList = const [],
  });
}

Annotation

The next step is to add annotations to help generator do his job.

import 'package:example/helpers.dart';
import 'package:reactive_forms_annotations/reactive_forms_annotations.dart';

@Rf()
class MailingList {
  final List<String?> emailList;

  MailingList({
    @RfArray(
      validators: const [
        MailingListValidator(),
      ],
    ) this.emailList = const [],
  });
}

ReactiveFormAnnotation - tells the generator that we want to Form based on this model. FormArrayAnnotation - maps fields to control elements.

Validation

The mailing list form should not be valid in two cases - if there are duplicates and if any field is invalid email.

class MailingListValidator extends Validator<dynamic> {
  const MailingListValidator() : super();

  @override
  Map<String, dynamic>? validate(AbstractControl control) {
    final formArray = control as FormArray<String>;
    final emails = formArray.value ?? [];
    final test = <String>{};

    final result = emails.fold<bool>(true,
            (previousValue, element) => previousValue && test.add(element ?? ''));

    return result ? null : <String, dynamic>{'emailDuplicates': true};
  }
}

As far as we are using annotations - validators should be top level functions or static class fields.

Now we are ready to run our form generator.

Form

Let's build our form based on generated code

// create form based on generated widget
final form = MailingListFormBuilder(
  // instantiate with empty model
  model: MailingList(),
  builder: (context, formModel, child) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Expanded(
              // renders list of fields corresponding to added elements
              child: ReactiveFormArray<String>(
                formArray: formModel.emailListControl,
                builder: (context, formArray, child) =>
                    Column(
                      children: formModel.emailListValue
                          .asMap()
                          .map((i, email) {
                        return MapEntry(
                            i,
                            ReactiveTextField<String>(
                              formControlName: i.toString(),
                              validationMessages: {
                                'email': (_) => 'Invalid email',
                              },
                              decoration: InputDecoration(
                                  labelText: 'Email ${i}'),
                            ));
                      })
                          .values
                          .toList(),
                    ),
              ),
            ),
            SizedBox(width: 16),
            // adds new item to the list of fields
            ElevatedButton(
              onPressed: () {
                // add****Item function allows you not only to add values but also alter the FormControl parameters if needed
                // you can change 
                //  - validators,
                //  - asyncValidators,
                //  - asyncValidatorsDebounceTime,
                //  - disabled,
                // validatorsApplyMode - controls how the validators will be applied
                //  - merge - will append to the validators defined in annotation
                //  - override - will override to the validators defined in annotation
                formModel.addEmailListItem('');
              },
              child: const Text('add'),
            )
          ],
        ),
        SizedBox(height: 16),
        // renders error related to the whole list of elements
        ReactiveMailingListFormConsumer(
          builder: (context, form, child) {
            // map error keys to text
            final errorText = {
              'emailDuplicates': 'Two identical emails are in the list',
            };
            final errors = <String, dynamic>{};

            // filter values related to individual text fields
            form.emailListControl.errors.forEach((key, value) {
              final intKey = int.tryParse(key);
              if (intKey == null) {
                errors[key] = value;
              }
            });

            // if there is still erros left - render an error message 
            if (form.emailListControl.hasErrors && errors.isNotEmpty) {
              return Text(errorText[errors.entries.first.key] ?? '');
            } else {
              return Container();
            }
          },
        ),
        SizedBox(height: 16),
        Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            ElevatedButton(
              onPressed: () {
                if (formModel.form.valid) {
                  print(formModel.model);
                } else {
                  formModel.form.markAllAsTouched();
                }
              },
              child: const Text('Sign Up'),
            ),
            ReactiveMailingListFormConsumer(
              builder: (context, form, child) {
                return ElevatedButton(
                  child: Text('Submit'),
                  onPressed: form.form.valid ? () {} : null,
                );
              },
            ),
          ],
        )
      ],
    );
  },
);

Nested forms with FormGroups

The next example will show how to build nested forms. We will create a user profile form with first/last names and home/office addresses. Address will contain city/street/zip fields.

Model

The model will be separated on two parts UserProfile and Address

class UserProfile {
  final String firstName;

  final String lastName;

  final Address? home;

  final Address? office;

  UserProfile({
    this.firstName = '',
    this.lastName = '',
    this.home,
    this.office,
  });
}

class Address {
  final String? street;

  final String? city;

  final String? zip;

  Address({
    this.street,
    this.city,
    this.zip,
  });
}

Annotation

The next step is to add annotations to help generator do his job.

import 'package:example/helpers.dart';
import 'package:reactive_forms_annotations/reactive_forms_annotations.dart';

@Rf()
class UserProfile {
  final String firstName;

  final String lastName;

  final Address? home;

  final Address? office;

  UserProfile({
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.firstName = '',
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.lastName = '',
    this.home,
    this.office,
  });
}

@RfGroup()
class Address {
  final String? street;

  final String? city;

  final String? zip;

  Address({
    @RfControl() this.street,
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.city,
    @RfControl() this.zip,
  });
}

ReactiveFormAnnotation - tells the generator that we want to Form based on this model. FormGroupAnnotation - describes the nested form.

Validation

As far as we are using annotations - validators should be top level functions or static class fields.

Now we are ready to run our form generator. .

Form

Let's build our form based on generated code

// create form based on generated widget
final form = UserProfileFormBuilder(
  model: UserProfile(),
  builder: (context, formModel, child) {
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          ReactiveTextField<String>(
            formControl: formModel.firstNameControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'First name',
            ),
          ),
          const SizedBox(height: 8.0),
          ReactiveTextField<String>(
            formControl: formModel.lastNameControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'Last name',
            ),
          ),
          const SizedBox(height: 24.0),
          Text('Home address', style: TextStyle(fontSize: 18)),
          ReactiveTextField<String>(
            formControl: formModel.homeForm.cityControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'Home city',
            ),
          ),
          const SizedBox(height: 8.0),
          ReactiveTextField<String>(
            formControl: formModel.homeForm.streetControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'Home street',
            ),
          ),
          const SizedBox(height: 8.0),
          ReactiveTextField<String>(
            formControl: formModel.homeForm.zipControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            textInputAction: TextInputAction.done,
            decoration: const InputDecoration(
              labelText: 'Home zip',
            ),
          ),
          const SizedBox(height: 8.0),
          Text('Office address', style: TextStyle(fontSize: 18)),
          const SizedBox(height: 8.0),
          ReactiveTextField<String>(
            formControl: formModel.officeForm.cityControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'Office city',
            ),
          ),
          const SizedBox(height: 8.0),
          ReactiveTextField<String>(
            formControl: formModel.officeForm.streetControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'Office street',
            ),
          ),
          const SizedBox(height: 8.0),
          ReactiveTextField<String>(
            formControl: formModel.officeForm.zipControl,
            validationMessages: {
              ValidationMessage.required: (_) => 'Must not be empty',
            },
            decoration: const InputDecoration(
              labelText: 'Office zip',
            ),
          ),
          ElevatedButton(
            onPressed: () {
              if (formModel.form.valid) {
                print(formModel.model);
              } else {
                formModel.form.markAllAsTouched();
              }
            },
            child: const Text('Sign Up'),
          ),
          ReactiveUserProfileFormConsumer(
            builder: (context, form, child) {
              return ElevatedButton(
                child: Text('Submit'),
                onPressed: form.form.valid
                    ? () {
                  print(form.model.firstName);
                  print(form.model.lastName);
                }
                    : null,
              );
            },
          ),
        ],
      ),
    );
  },
);

Nested forms with array of FormGroups

The next example will show how to build nested forms. We will create a delivery list with simple control for name and form group for address; Address will contain city/street fields.

Model

The model will be separated on three parts DeliveryList, DeliveryPoint and Address

class DeliveryList {
  final List<DeliveryPoint> deliveryList;

  DeliveryList({
    this.deliveryList = const [],
  });
}

class DeliveryPoint {
  final String name;

  final Address? address;

  DeliveryPoint({
    this.name = '',
    this.address,
  });
}

class Address {
  final String? street;

  final String? city;

  Address({
    this.street,
    this.city,
  });
}

Annotation

The next step is to add annotations to help generator do his job.

@Rf()
class DeliveryList {
  final List<DeliveryPoint> deliveryList;

  DeliveryList({
    @RfArray() this.deliveryList = const [],
  });
}

@RfGroup()
class DeliveryPoint {
  final String name;

  final Address? address;

  DeliveryPoint({
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.name = '',
    this.address,
  });
}

@RfGroup()
class Address {
  final String? street;

  final String? city;

  Address({
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.street,
    @RfControl() this.city,
  });
}

ReactiveFormAnnotation - tells the generator that we want to Form based on this model. FormGroupAnnotation - describes the nested form.

Now we are ready to run our form generator. .

Form

Let's build our form based on generated code

// create form based on generated widget
final form = DeliveryListFormBuilder(
  model: DeliveryList(),
  builder: (context, formModel, child) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Expanded(
              child: ReactiveFormArray<Map<String, Object?>>(
                formArray: formModel.deliveryListControl,
                builder: (context, formArray, child) {
                  return Column(
                    children: formModel.deliveryListValue
                        .asMap()
                        .map((i, deliveryPoint) {
                      return MapEntry(
                          i,
                          Column(
                            children: [
                              ReactiveTextField<String>(
                                formControlName: '${i}.name',
                                validationMessages: {
                                  ValidationMessage.required: (_) => 'Must not be empty',
                                },
                                decoration: InputDecoration(
                                  labelText: 'Name ${i}',
                                ),
                              ),
                              ReactiveTextField<String>(
                                formControlName:
                                '${i}.address.street',
                                validationMessages: {
                                  ValidationMessage.required: (_) => 'Must not be empty',
                                },
                                decoration: InputDecoration(
                                  labelText: 'Street ${i}',
                                ),
                              ),
                              ReactiveTextField<String>(
                                formControlName: '${i}.address.city',
                                validationMessages: {
                                  ValidationMessage.required: (_) => 'Must not be empty',
                                },
                                decoration: InputDecoration(
                                  labelText: 'City ${i}',
                                ),
                              ),
                            ],
                          ));
                    })
                        .values
                        .toList(),
                  );
                },
              ),
            ),
            SizedBox(width: 16),
            ElevatedButton(
              onPressed: () {
                formModel.addDeliveryListItem(DeliveryPoint());
              },
              child: const Text('add'),
            )
          ],
        ),
        SizedBox(height: 16),
        SizedBox(height: 16),
        Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            ElevatedButton(
              onPressed: () {
                if (formModel.form.valid) {
                  print(formModel.model);
                } else {
                  formModel.form.markAllAsTouched();
                }
              },
              child: const Text('Sign Up'),
            ),
            ReactiveDeliveryListFormConsumer(
              builder: (context, form, child) {
                return ElevatedButton(
                  child: Text('Submit'),
                  onPressed: form.form.valid ? () {} : null,
                );
              },
            ),
          ],
        )
      ],
    );
  },
);

Freezed

You can annotate models produced by Freezed with additional annotations to generate the form Read more about freezed here

First we need to define our form model

Model

@freezed
class FreezedClass with _$FreezedClass {
  const factory FreezedClass({
    String? id,
    String? name,
    double? year,
  }) = _FreezedClass;

  factory FreezedClass.fromJson(Map<String, dynamic> json) =>
      _$FreezedClassFromJson(json);
}

Annotation

The next step is to add annotations to help generator do his job.

import 'package:reactive_forms_annotations/reactive_forms_annotations.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'freezed_class.g.dart';

part 'freezed_class.freezed.dart';

@freezed
@Rf()
class FreezedClass
    with _$FreezedClass {
  const factory FreezedClass({
    @RfControl() String? id,
    @RfControl() String? name,
    @RfControl() double? year,
  }) = _FreezedClass;

  factory FreezedClass.fromJson(Map<String, dynamic> json) =>
      _$FreezedClassFromJson(json);
}

Validation

You can add validations the same way as in basics example

Form

Let's build our form based on generated code

final form = FreezedClassFormBuilder(
  model: FreezedClass(),
  builder: (context, formModel, child) {
    return Column(
      children: [
        ReactiveTextField<String>(
          formControl: formModel.idControl,
          textInputAction: TextInputAction.next,
          decoration: const InputDecoration(
            labelText: 'ID',
            helperText: '',
            helperStyle: TextStyle(height: 0.7),
            errorStyle: TextStyle(height: 0.7),
          ),
        ),
        const SizedBox(height: 16.0),
        ReactiveTextField<String>(
          formControl: formModel.nameControl,
          textInputAction: TextInputAction.done,
          decoration: const InputDecoration(
            labelText: 'Name',
            helperText: '',
            helperStyle: TextStyle(height: 0.7),
            errorStyle: TextStyle(height: 0.7),
          ),
        ),
        const SizedBox(height: 16.0),
        ReactiveSlider(
          formControl: formModel.yearControl,
          min: 1900,
          max: 2100,
        ),
        ElevatedButton(
          onPressed: () {
            if (formModel.form.valid) {
              print(formModel.model);
              print(formModel.model.year);
            } else {
              formModel.form.markAllAsTouched();
            }
          },
          child: const Text('Sign Up'),
        ),
        ReactiveFreezedClassFormConsumer(
          builder: (context, formModel, child) {
            return ElevatedButton(
              child: Text('Submit'),
              onPressed: formModel.form.valid ? () {} : null,
            );
          },
        ),
      ],
    );
  },
);

Subforms

There could be the case when some of your nested entities will have to have their own subform with subset of fields. Let's refer to our previous example of Nested forms with array of FormGroups

We used DeliveryPoint entity to create a list for points for delivery. If you want to have the set of form classes to be generated for DeliveryPoint just add and additional ReactiveFormAnnotation annotation

@Rf()
@RfGroup()
class DeliveryPoint {
  final String name;

  final Address? address;

  DeliveryPoint({
    @RfControl(
      validators: const [RequiredValidator()],
    ) this.name = '',
    this.address,
  });
}

Animated list

Check the Animated URL list form sample to see how to deal with forms utilizing AnimatedList. There is a small trick which requires passing down the FormControl elements due to the fact that we need to provide same widget to animate removal while actual form item is missing in form.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages