form_companion_generator

Code generator for form_companion_presenter and form_builder_companion_presetner

Usage

(1) Add package reference in dev_dependencies in your pubspec.yaml:

  dev_dependencies:
    form_companion_generator:

(2) Add build.yaml in your package, next to pubspec.yaml:

targets:
  $default:
    builders:
      form_companion_generator:
        enabled: true
        # Bellow generate_for entry is optional, but it is recommended
        # to set exclude/include filters which filter out only target
        # sources which have classes decorated with `@FormCompanion`.
        # This trick improves your build_runner execution time drastically.
        generate_for:
          exclude:
            - to/be/excluded/globs/*.dart
          include:
            - to/be/included/globs/*.dart

(3) Run following command on the top directory of your package sources.

# For one time
dart pub run build_runner build form_companion_generator
# ..or for continuous back-ground execution
dart pub run build_runner watch form_companion_generator

(4) You can find *.fcp.dart files next to your sources which have classes decorated with @FormCompanion annotation.

Features

  • Generates easy and type-safe accessors for PropertyDescritors and their saved values.
  • Generates customizable FormField (or FormBuilderField) factories for each PropertyDescritor.

You can find full-featured example code in github repository, but we will show some code snipets:

Declare your presenter as AsyncNotifier or riverpod 2.x:

NOTE: MyPresenterFormProperties class and properties extension property are generated by form_companion_generator.

@formCompanion
@riverpod
class MyPresenter extends AutoDisposableAsyncNotifier<MyPresenterFormProperties>
  with CompanionPresenterMixin, FormCompanionMixin {
  MyPresenter();

  @override
  FutureOr<MyPresenterFormProperties> build() async {
    final initialValue = await ref.watch(someDependentFutureProvider);
    return (properties.copyWith()
      ..someProperty(initialValue.someProperty)
      // More setter calls here...
      ).build();
  }

  // Some methods or overrides here...
}

Refer property values in the presenter:

NOTE: properties extension property is generated by form_companion_generator.

@override
FutureOr<void> doSubmit() async {
  final someProperty = properties.values.someProperty;
  // Use someProperty here...
}

Get form field in the view (Widget):

NOTE: This example assumes that you use riverpod 2.x and its AsyncNotifier<T> as above example.
In addition, properties extension property is generated by form_companion_generator. Remember this build method as descendant of Form widget.

@override
Widget build(BuildContext context, Ref ref) {
  final state = ref.watch(myPresenterProvider);
  if (state is! AsyncData<MyPresenterFormProperties>) {
    return Text('loading...');
  }

  // state.value returns MyPresenterFormProperties here.
  return Column(
    children: [
      state.value.fields.someProperty(context),
      // More form fields call here...
      ElevatedButton(
        onPressed: state.value.submit(context),
        child: Text('submit'),
      )
    ],
  );
}

Spec

Prereqs

  • Decorate your companion with @formCompanion annotation.
    • If you use FormBuilderCompanionPresenterMixin, then the generator creates codes using FormBuilder; otherwise, it generates sources which only use vanilla Form.
  • You can specify validation mode for the annotation.
  • The generator will read property definitions from a single expression which is passed to initializeCompanionMixin method call in the constructor body. The single expression must be one of the following:
    • Inline PropertyDescritorBuilder construction.
    • Local variable reference, which is PropertyDescritorBuilder type. Note that the generator only tracks its initialization expression.
    • Static field, which is PropertyDescritorBuilder type. Note that the generator only tracks its initialization expression

L10N Support

You can get L10N support via templating feature, described below. For most cases, you can localize form fields with writing your build.yaml to modify three pre-defined named_templates entries: label_template, hint_template, and item_widget_template. For example, you can write your templates with macros (desribed lator) to generate code which retrieve L10N strings via your favorit I18N libraries such as intl, easy_localization, etc.

For example:

# In your build.yaml
builders:
  form_companion_generator:
    enabled: true
    options:
      named_templates:
        # This template assumes easy_localization usage.
        label_template: 'LocaleKeys.#PRESENTER_NAME#_#PROPERTY_NAME#_label.tr()'
        # This template assumes intl usage (with gen-l10n).
        hint_template: 'L10n.of(#BUILD_CONTEXT#).#PRESENTER_NAME#_#PROPERTY_NAME#_hint'

Custom naming

build method name of typed FormProperties builder

A builder of typed FormProperties has build() method to build a new typed FormProperties from it, but when your presenter defines build property, the generator emits build() setter in the builder, so compilation error will be occurred because Dart does not allow member overloading. To handle such situation, you can customize build method name of typed FormProperties builder in your build.yaml. In your build.yaml, you can specify build property under form_properties_builder under your target presenter's name under custom_namings as follows:


# In your build.yaml
builders:
  form_companion_generator:
    enabled: true
    options:
      custom_namings:
        MyPresenter:
          form_properties_builder:
            # Specify method name for default 'build'. It must be differ from all properties of the presenter.
            build: buildFormProperties

Templating

Although form companion generator make out of box code to create FormField from presenter's properties, sometimes you might want to customize generated code. For example, you want to localize label and hint text, or want to customize dropdown items. In many cases, you can do it via optional parameters of generated form field factories, but this method is bother when you customize all or many of fields you use. To solve this case, you can add or replace templates of the generator. Templates can be configured in build.yaml which can be located next to the pubspec.yaml in your application or package.

There are two types of templates -- named_templates and argument_templates, and both of them can be configured as options property in build.yaml.

Argument Templates

Argument templates are map of templates for each arguments for constructors of instanciating FormFields. Its key is name of insntanciating FormField class (note that this name is case sensitive), and this entries' values are also map, the keys of the inner maps are name of parameters of the constructor.

The values are either string or map.

If you specify string value for the parameter, the specified string value will be simple template string, so it will be emitted as the constructor parameter assignment expression. Note that you can use macros for the template string. The macro can be written in #MACRO_NAME# format, where MACRO_NAME is macro name.

If you specify map for the parameter, you can specify following properties:

  • template string. This is same as specify string instead of the map.
  • item_template string, described below.
  • imports, described below.

Note that both of template and item_template properties are omittable. (This is useful to specify extra imports for some reason such as importing dependent types used in static method which is used for default value of function typed parameters and the method is not public.)

When you not configure any template for the parameter, default template (#ARGUMENT#) will be used.

To specify argument template for specific constructor parameter for all FormField subclasses, you can use default key for the outer map's key (they normally represent class name of FormField subclass).

Item Template

As described before, each entries under properties in argument_templates may have item_template property instead of template property. The value of item_template is referred as Item Template.

The item_template represents a tempalte for each item of collection, and it is only available in properties which have "collection-like" field value types, which are one of following:

  • Iterable<E> or its subtypes (including List<E>). Item template should be applied for each items of the Iterable<E> value (thus, the item value type is E).
  • Any enum type. Item template should be applied for each members of the enum type (thus, the item value type is the enum type). Note that if the type is nullable, then null is also included in head of the members.
  • bool. Item template should be applied for [true, false]. Note that if the type is nullable, then null is also included in head, that is the list will be [null, true, false].

If the field value type of the property is not a "collection-like" type, the item template is ignored, so the parameter uses default template.

In addition, some macro keys are only available in Item Templates. See below for details of macro keys.

Example of Argument Templates

The following code list shows example argument_templates in build.yaml:

builders:
  form_companion_generator:
    options:
      argument_templates:
        default:
          autovalidateMode: '#ARGUMENT# ?? #AUTO_VALIDATE_MODE#'
          decoration:
            template: '#ARGUMENT# ?? #DEFAULT_VALUE_COPY_OR_NEW#(labelText: #LABEL_TEMPLATE#, hintText: #HINT_TEMPLATE#)'
            imports: 'package:myapp/src/locale/l10n.dart'

        DropdownButtonFormField:
          items:
            item_template: 'DropdownMenuItem<#ITEM_VALUE_TYPE#>(value: #ITEM_VALUE#, child: #ITEM_WIDGET_TEMPLATE#)'
          hint: '#ARGUMENT# ?? #HINT_TEMPLATE#'
          onChanged:
            template: '#ARGUMENT# ?? c.onChangedCommon'
            imports:
              'c.onChangedCommon': 'package:myapp/src/utils/common_functions.dart'
          hint: '#ARGUMENT# ?? (_) {}'

Note that you can find default template configuration with inspecting build.yaml file which is located next to this readme file.

Named Templates

Named templates are essentially user-defined macros which can be used in argument_templates. Note that keys of named templates must be referred from argument_templates with UPPER_SNEAK_CASING even if the key is defined with lower_sneak_cases.

As you notice, keys of named_templates are case insensitive when they defined.

NOTE: Another named_templates entry reference in named_templates are NOT supported.

Predefined Named Templates

There are three predefined named templates as following table. Note that some of them use macros described later.

key value description
label_template #PROPERTY#.name Default template to be used for labalText of InputDecorator for decorator parameter of FormField.
hint_template null Default template to be used for hintText of InputDecorator for decorator parameter of FormField.
item_widget_template Text(#ITEM_VALUE_STRING#) Default template to be used in predefined item templates for items and options to specify widget parameter for widgets of field items like DropdownMenuItem<T> or FormBuilderOption<T>.

Tip: If you specify hint_template, it is recommended to specify hint argument template for DropdownButtonFormField and FormBuilderDropdown with value '#ARGUMENT# ?? #HINT_TEMPLATE#'.

Example of Named Templates

The following code list shows sample named_templates in build.yaml:

builders:
  form_companion_generator:
    options:
      named_templates:
          label_template:
            template: 'L10n.#PRESENTER_NAME#_#PROPERTY_NAME#_label'
            imports:
              - 'package:intl/intl.dart`
              - 'package:myapp/src/locale/l10n.dart'
          hint_template: 'L10n.#PRESENTER_NAME#_#PROPERTY_NAME#_hint'
            imports:
              - 'package:intl/intl.dart`
              - 'package:myapp/src/locale/l10n.dart'

Imports in Templates

You can specify import directive in imports property of the each argument templates and named templates. imports property can be string or map.

If you specify string value for imports, the string will be treated as a single import URI such as package:intl/intl.dart.

If you specify map, the keys and values must be string. The keys and values will be treated as following:

  • If the key has only ASCII identifier letters ([A-Za-z$][A-Za-z_$0-9]* in regex), then the key represents the type name should be written after show keyword of the import directive.
  • If the key has ASCII lower alpha-numeric letters ([a-z$][a-z_$0-9]* in regex), following 1 dot (.), and ASCII identifier letters ([A-Za-z$][A-Za-z_$0-9]* in regex), then the key represents prefix and the type name. The prefix should be written after as keyword, and the type name should be written after show keyword, of the import directive respectively.
  • The key which has any other format is not allowed.
  • The value is URI of the importing package.

It is recommended to use map format to avoid unepxected name confliction in generated code. Simple (string format) import should be used for localization related import only (names L10n or LocaleKeys should not be conflicted).

Note that if you specify duplicated imports entries in the build.yaml, they are just merged.

Macro

For values under argument_templates and named_templates, you can use following macros, which will be replaced with appropriate values in code generation. Macros can be specfieid with #MACRO_NAME# format in your template values. Note that MACRO_NAME is case sensitive and must be defined in following table. Macro name must be upper sneak casing string, and only ASCII uppercase letters, ASCII numbers, and an underscore are allowed. If the specified macro name is not defined, the code generator will end with error.

See following build.yaml spec for available macros.

build.yaml Spec

You can specify various options via your build.yaml file, which is located to next to pubspec.yaml.

# In your build.yaml
builders:
  form_companion_generator:
    enabled: true
    options:
      # Put options here

Note You can refer general specification of build.yaml in build_package documentation.

Available Options

key type default description
autovalidate_by_default bool true If true, default value of autovalidateMode of form fields will be AutovalidateMode.onUserInteraction. Otherwise, the value will be AutovalidateMode.disabled.
as_part bool false If true, generated *.fcp.dart file will be part of original files (files without .fcp suffix), shares namespaces and imports. This is convinient if you use many 3rd party imports in your properties, but it leads member name conflicts and might lead poor code completion (intellisense) experience.
extra_libraries String, or list of string empty Specify package uri with package:... format which adds hint for generator to find importing libraries. Note that if the library is not referenced actually, the import entry will not be emitted.
uses_enum_name bool or null null If true, #ITEM_VALUE_STRING# for enum will be emitted as #ITEM_VALUE#.name. If false, it will be emitted as #ITEM_VALUE#.toString(), which will write enum type name prefix with separator dot (.) like MyEnum.memberOne. If null, it depends on the language version of source file; if the language version (sdk of environment in pubspec.yaml) is gerator than or equal to 2.15, .name will be used.
named_templates map of string (see previous named_templates section) Defines or overrides named templates. All values are string.
argument_templates (see previous argument_templates section) (see previous argument_templates section) Defines or overrides argument templates. All values are string, or map of string (for item templates).

Macros Available in Tempaltes

In template values, you can use defined named templates or following context specific macros as #MACRO_NAME# format.

key available in description
PRESENTER_NAME any Replaced with static token which is name of the presenter type.
PROPERTY_NAME any Replaced with static token which is name of the property.
PROPERTY_VALUE_TYPE any Replaced with static token which is P of PropertyDescriptor<P, F> for the property.
FIELD_VALUE_TYPE any Replaced with static token which is P of PropertyDescriptor<P, F> for the property.
PROPERTY any Replaced with static token which is local variable identifier of PropertyDescriptor<P, F> for the property.
LABEL_TEMPLATE itemTemplates Replaced with expression resolved for labelTemplate.
HINT_TEMPLATE itemTemplates Replaced with expression resolved for hintTemplate.
ITEM_VALUE itemTemplates Replaced with static token which is local variable identifier which holds current enum member, bool value, or collection item.
ITEM_VALUE_TYPE itemTemplates Replaced with static token which is a type of the #ITEM_VALUE.
ITEM_VALUE_STRING itemTemplates See below "About #ITEM_VALUE_STRING#.

About #ITEM_VALUE_STRING#

#ITEM_VALUE_STRING# macro will be replaced with string representation of #ITEM_VALUE#. This value will vary on the type of items type as following:

item type other condition replacement result
String - #ITEM_VALUE#
String? - #ITEM_VALUE# ?? ''
enum Dart SDK version >= 2.15 or uses_enum_name option is set to true #ITEM_VALUE#.name
enum Dart SDK version < 2.15 or uses_enum_name option is set to false #ITEM_VALUE#.toString()
enum (nullable) Dart SDK version >= 2.15 or uses_enum_name option is set to true #ITEM_VALUE#?.name ?? ''
enum (nullable) Dart SDK version < 2.15 or uses_enum_name option is set to false #ITEM_VALUE#?.toString() ?? ''
other types - #IETM_VALUE#.toString()
other types (nullable) - #IETM_VALUE#?.toString() ?? ''

Breaking Changes

See https://github.com/yfakariya/form_companion_presenter/blob/main/BREAKING_CHANGES.md to check breaking changes.