Skip to content

feat: support controlFactory in both FormGroup and FormArray #477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 101 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,73 @@
# Reactive Forms
# Reactive Forms <!-- omit in toc -->

This is a model-driven approach to handling Forms inputs and validations, heavily inspired in [Angular's Reactive Forms](https://angular.io/guide/reactive-forms).

[![Pub Version](https://img.shields.io/pub/v/reactive_forms)](https://pub.dev/packages/reactive_forms) ![GitHub](https://img.shields.io/github/license/joanpablo/reactive_forms) ![GitHub top language](https://img.shields.io/github/languages/top/joanpablo/reactive_forms) ![flutter tests](https://github.com/joanpablo/reactive_forms/workflows/reactive_forms/badge.svg?branch=master) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a4e40d632feb41b5af624cbd36064c83)](https://www.codacy.com/manual/joanpablo/reactive_forms?utm_source=github.com&utm_medium=referral&utm_content=joanpablo/reactive_forms&utm_campaign=Badge_Grade) [![codecov](https://codecov.io/gh/joanpablo/reactive_forms/branch/master/graph/badge.svg)](https://codecov.io/gh/joanpablo/reactive_forms)

## Table of Contents

- [Table of Contents](#table-of-contents)
- [Getting Started](#getting-started)
- [Minimum Requirements](#minimum-requirements)
- [Installation and Usage](#installation-and-usage)
- [Minimum Requirements](#minimum-requirements)
- [Installation and Usage](#installation-and-usage)
- [Creating a form](#creating-a-form)
- [Default Values](#default-values)
- [How to get/set Form data](#how-to-getset-form-data)
- [Validators](#what-about-validators)
- [What about Validators?](#what-about-validators)
- [Predefined validators](#predefined-validators)
- [FormControl](#formcontrol)
- [FormGroup](#formgroup)
- [FormArray](#formarray)
- [Custom Validators](#custom-validators)
- [Inheriting from `Validator` class:](#inheriting-from-validator-class)
- [Using the `Validators.delegate()` validator:](#using-the-validatorsdelegate-validator)
- [Pattern Validator](#pattern-validator)
- [FormGroup validators](#formgroup-validators)
- [Password and Password Confirmation](#what-about-password-and-password-confirmation)
- [Asynchronous Validators](#asynchronous-validators-sunglasses)
- [What about Password and Password Confirmation?](#what-about-password-and-password-confirmation)
- [Asynchronous Validators :sunglasses:](#asynchronous-validators-sunglasses)
- [Debounce time in async validators](#debounce-time-in-async-validators)
- [Composing Validators](#composing-validators)
- [Groups of Groups](#groups-of-groups-grin)
- [Dynamic forms with FormArray](#dynamic-forms-with-formarray)
- [Composing Validators](#composing-validators)
- [Groups of Groups :grin:](#groups-of-groups-grin)
- [Dynamic forms with **FormArray**](#dynamic-forms-with-formarray)
- [Arrays of Groups](#arrays-of-groups)
- [FormBuilder](#formbuilder)
- [Groups](#groups)
- [Arrays](#arrays)
- [Control](#control)
- [Control state](#control-state)
- [Nested Controls](#nested-controls)
- [Reactive Form Widgets](#reactive-form-widgets)
- [How to customize error messages?](#how-to-customize-error-messages)
- [Reactive Widget level](#1-reactive-widget-level)
- [Global/Application level](#2-globalapplication-level)
- [1. Reactive Widget level.](#1-reactive-widget-level)
- [2. Global/Application level.](#2-globalapplication-level)
- [Validation messages with error arguments:](#validation-messages-with-error-arguments)
- [Parameterized validation messages](#parameterized-validation-messages)
- [When does Validation Messages begin to show up?](#when-does-validation-messages-begin-to-show-up)
- [Touching a control](#touching-a-control)
- [Overriding Reactive Widgets show errors behavior](#overriding-reactive-widgets-show-errors-behavior)
- [Enable/Disable Submit button](#enabledisable-submit-button)
- [Submit Button in a different Widget](#separating-submit-button-in-a-different-widget)
- [ReactiveFormConsumer widget](#using-reactiveformconsumer-widget)
- [Focus/UnFocus a FormControl](#focusunfocus-a-formcontrol)
- [Separating Submit Button in a different Widget:](#separating-submit-button-in-a-different-widget)
- [Using **ReactiveFormConsumer** widget:](#using-reactiveformconsumer-widget)
- [Focus/UnFocus a **FormControl**](#focusunfocus-a-formcontrol)
- [Focus flow between Text Fields](#focus-flow-between-text-fields)
- [Enable/Disable a widget](#how-enabledisable-a-widget)
- [How does ReactiveTextField differs from native TextFormField or TextField?](#how-does-reactivetextfield-differs-from-native-textformfieldhttpsapiflutterdevfluttermaterialtextformfield-classhtml-or-textfieldhttpsapiflutterdevfluttermaterialtextfield-classhtml)
- [Reactive Form Field Widgets](#supported-reactive-form-field-widgets)
- [How Enable/Disable a widget](#how-enabledisable-a-widget)
- [How does **ReactiveTextField** differs from native TextFormField or TextField?](#how-does-reactivetextfield-differs-from-native-textformfield-or-textfield)
- [Supported Reactive Form Field Widgets](#supported-reactive-form-field-widgets)
- [Bonus Field Widgets](#bonus-field-widgets)
- [Other Reactive Forms Widgets](#other-reactive-forms-widgets)
- [Advanced Reactive Field Widgets](#advanced-reactive-field-widgets)
- [ReactiveValueListenableBuilder to listen when value changes in a FormControl](#reactivevaluelistenablebuilder-to-listen-when-value-changes-in-a-formcontrol)
- [ReactiveForm vs ReactiveFormBuilder which one?](#reactiveform-vs-reactiveformbuilder-which-one)
- [Reactive Forms + Provider plugin](#reactive-forms--providerhttpspubdevpackagesprovider-plugin-muscle)
- [Reactive Forms + code generation plugin](#reactive-forms--code-generationhttpspubdevpackagesreactive_forms_generator-)
- [ReactiveTextField](#reactivetextfield)
- [ReactiveDropdownField](#reactivedropdownfield)
- [**ReactiveValueListenableBuilder** to listen when value changes in a **FormControl**](#reactivevaluelistenablebuilder-to-listen-when-value-changes-in-a-formcontrol)
- [**ReactiveForm** vs **ReactiveFormBuilder** which one?](#reactiveform-vs-reactiveformbuilder-which-one)
- [Widget testing](#widget-testing)
- [example component](#example-component)
- [example test](#example-test)
- [Reactive Forms + Provider plugin :muscle:](#reactive-forms--provider-plugin-muscle)
- [Reactive Forms + code generation 🤖](#reactive-forms--code-generation-)
- [How create a custom Reactive Widget?](#how-create-a-custom-reactive-widget)
- [What is not Reactive Forms](#what-is-not-reactive-forms)
- [What is Reactive Forms](#what-is-reactive-forms)
- [What is not **Reactive Forms**](#what-is-not-reactive-forms)
- [What is **Reactive Forms**](#what-is-reactive-forms)
- [Migrate versions](#migrate-versions)

## Getting Started
Expand Down Expand Up @@ -718,32 +732,73 @@ Map<String, dynamic> emptyAddressee(AbstractControl control) {
}
```

You can also pass `controlFactory` to `FormArray` and `FormGroup` to have complete control over the mapping:

```dart
// Given: an empty array of strings
final array = FormArray<String>(
[],
controlFactory: (int index, String? value) => FormControl<String>(
value: value,
validators: [const RequiredValidator()],
),
);

// When: set value to array
array.value = ["[email protected]", "[email protected]", "[email protected]"];

// Then: the array is no longer empty
expect(array.controls.length, 3);

// And: the new controls are all required!
expect(array.controls('0').validators.length, 1);
expect(array.controls('1').validators.length, 1);
expect(array.controls('2').validators.length, 1);
```

## Arrays of Groups

You can also create arrays of groups:

```dart
// an array of groups
final addressArray = FormArray([
FormGroup({
'city': FormControl<String>(value: 'Sofia'),
'zipCode': FormControl<int>(value: 1000),
}),
FormGroup({
'city': FormControl<String>(value: 'Havana'),
'zipCode': FormControl<int>(value: 10400),
}),
]);
final addressArray = FormArray(
[
FormGroup({
'city': FormControl<String>(value: 'Sofia'),
'zipCode': FormControl<int>(value: 1000),
}),
FormGroup({
'city': FormControl<String>(value: 'Havana'),
'zipCode': FormControl<int>(value: 10400),
}),
],
controlFactory: (index, value) {
return FormGroup({
'city': FormControl<String>(),
'zipCode': FormControl<int>(),
})
..value = value;
},
);
```

Another example using **FormBuilder**:

```dart
// an array of groups using FormBuilder
final addressArray = fb.array([
final addressArray = fb.array<Map<String,Object?>>([
fb.group({'city': 'Sofia', 'zipCode': 1000}),
fb.group({'city': 'Havana', 'zipCode': 10400}),
]);
],
[],
[],
(index, value) {
return fb.group({
'city': FormControl<String>(),
'zipCode': FormControl<int>(),
})..value = value as Map<String,Object?>?;
});
```

or just:
Expand All @@ -753,7 +808,15 @@ or just:
final addressArray = fb.array([
{'city': 'Sofia', 'zipCode': 1000},
{'city': 'Havana', 'zipCode': 10400},
]);
],
[],
[],
(index, value) {
return fb.group({
'city': FormControl<String>(),
'zipCode': FormControl<int>(),
})..value = value as Map<String,Object?>?;
});
```

You can iterate over groups as follow:
Expand All @@ -768,6 +831,8 @@ final cities = addressArray.controls
> A common mistake is to declare an _array_ of groups as _FormArray&lt;FormGroup&gt;_.
> An array of _FormGroup_ must be declared as **FormArray()** or as **FormArray&lt;Map&lt;String, dynamic&gt;&gt;()**.

> It's mandatory to use `controlFactory` here if you intend to set the `FormArray.value` to add new elements, as the default behavior would create a `FormControl<Map<String, Object?>>` instead of `FormGroup` (see [#366](https://github.com/joanpablo/reactive_forms/issues/366)).

## FormBuilder

The **FormBuilder** provides syntactic sugar that shortens creating instances of a FormGroup, FormArray and FormControl. It reduces the amount of boilerplate needed to build complex forms.
Expand Down
4 changes: 4 additions & 0 deletions lib/src/models/form_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class FormBuilder {
Map<String, Object> controls, [
List<Validator<dynamic>> validators = const [],
List<AsyncValidator<dynamic>> asyncValidators = const [],
FormGroupControlFactory? controlFactory,
]) {
final map = controls
.map<String, AbstractControl<dynamic>>((String key, Object value) {
Expand Down Expand Up @@ -117,6 +118,7 @@ class FormBuilder {
map,
validators: validators,
asyncValidators: asyncValidators,
controlFactory: controlFactory,
);
}

Expand Down Expand Up @@ -203,6 +205,7 @@ class FormBuilder {
List<Object> value, [
List<Validator<dynamic>> validators = const [],
List<AsyncValidator<dynamic>> asyncValidators = const [],
FormArrayControlFactory<T>? controlFactory,
]) {
return FormArray<T>(
value.map<AbstractControl<T>>((v) {
Expand All @@ -217,6 +220,7 @@ class FormBuilder {
}).toList(),
validators: validators,
asyncValidators: asyncValidators,
controlFactory: controlFactory,
);
}

Expand Down
50 changes: 49 additions & 1 deletion lib/src/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,9 @@ abstract class FormControlCollection<T> extends AbstractControl<T> {
}
}

typedef FormGroupControlFactory = AbstractControl<Object?>? Function(
String key, Object? value);

/// Tracks the value and validity state of a group of FormControl instances.
///
/// A FormGroup aggregates the values of each child FormControl into one object,
Expand All @@ -1109,6 +1112,20 @@ abstract class FormControlCollection<T> extends AbstractControl<T> {
class FormGroup extends FormControlCollection<Map<String, Object?>> {
final Map<String, AbstractControl<dynamic>> _controls = {};

/// A function that maps a value to a control.
/// Used in [updateValue] to create a new control for new values.
final FormGroupControlFactory? controlFactory;

static AbstractControl<Object?>? defaultControlFactory(
String key,
Object? value,
) {
return null;
}

FormGroupControlFactory get controlFactoryOrDefault =>
controlFactory ?? defaultControlFactory;

/// Creates a new FormGroup instance.
///
/// When instantiating a [FormGroup], pass in a [Map] of child controls
Expand Down Expand Up @@ -1149,6 +1166,7 @@ class FormGroup extends FormControlCollection<Map<String, Object?>> {
super.asyncValidators,
super.asyncValidatorsDebounceTime,
bool disabled = false,
this.controlFactory,
}) : assert(
!controls.keys.any((name) => name.contains(_controlNameDelimiter)),
'Control name should not contain dot($_controlNameDelimiter)'),
Expand Down Expand Up @@ -1431,6 +1449,20 @@ class FormGroup extends FormControlCollection<Map<String, Object?>> {
);
}

// Add missing controls via controlFactory, similar to FormArray
final controlFactory = controlFactoryOrDefault;
final newControls = Map.fromEntries(
value.entries
.where((e) => !_controls.containsKey(e.key))
.map((e) => MapEntry(e.key, controlFactory(e.key, e.value)))
.where((e) => e.value != null),
).cast<String, AbstractControl<Object?>>();
if (newControls.isNotEmpty) {
_controls.addAll(newControls);
newControls.forEach((name, control) {
control.parent = this;
});
}
updateValueAndValidity(
updateParent: updateParent,
emitEvent: emitEvent,
Expand Down Expand Up @@ -1608,6 +1640,9 @@ class FormGroup extends FormControlCollection<Map<String, Object?>> {
findControlInCollection(path.split(_controlNameDelimiter));
}

typedef FormArrayControlFactory<T> = AbstractControl<T> Function(
int index, T? value);

/// A FormArray aggregates the values of each child FormControl into an array.
///
/// It calculates its status by reducing the status values of its children.
Expand All @@ -1619,6 +1654,17 @@ class FormGroup extends FormControlCollection<Map<String, Object?>> {
class FormArray<T> extends FormControlCollection<List<T?>> {
final List<AbstractControl<T>> _controls = [];

/// A function that maps a value to a control.
/// Used in [updateValue] to create a new control for the value.
final FormArrayControlFactory<T>? controlFactory;

static AbstractControl<T> defaultControlFactory<T>(int index, T? value) {
return FormControl<T>(value: value);
}

FormArrayControlFactory<T> get controlFactoryOrDefault =>
controlFactory ?? defaultControlFactory;

/// Creates a new [FormArray] instance.
///
/// When instantiating a [FormGroup], pass in a collection of child controls
Expand Down Expand Up @@ -1660,6 +1706,7 @@ class FormArray<T> extends FormControlCollection<List<T?>> {
super.asyncValidators,
super.asyncValidatorsDebounceTime,
bool disabled = false,
this.controlFactory,
}) : super(
disabled: disabled,
) {
Expand Down Expand Up @@ -2091,12 +2138,13 @@ class FormArray<T> extends FormControlCollection<List<T?>> {
}

if (value != null && value.length > _controls.length) {
final controlFactory = controlFactoryOrDefault;
final newControls = value
.toList()
.asMap()
.entries
.where((entry) => entry.key >= _controls.length)
.map((entry) => FormControl<T>(value: entry.value))
.map((entry) => controlFactory(entry.key, entry.value))
.toList();

addAll(
Expand Down
Loading