Unable to implement FormArray having FormGroups
I am trying to implement a form having a FormArray of FormGroups. However, I am unable to get it working. I could not find a working implementation for the same, in the package examples or the Internet. The problem is with writing Widgets for the FormArray part. I have tried using several approaches, but no success.
Here is the sample code that uses two approaches using formControl and formControlName in a ReactiveTextField. While using formControl, the casting gives the error in
formControl: control as FormControl<String>,
Whereas, in formControlName approach, specifying the name as in qualif.$i.degree gives a problem (where $i refers to index as suggested in the array example).
Please suggest where I am making mistakes or provide a link to a working example.
The code is given here.
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'FormArray Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PersonFormView(personFormGroup));
}
}
class Qualif {
final String degree;
final int year;
const Qualif(this.degree, this.year, {Key? key});
}
class Person {
final String name;
final int age;
final List<Qualif> qualif;
const Person(this.name, this.age, this.qualif, {Key? key});
}
FormGroup personFormGroup = FormGroup({
'name': FormControl<String>(),
'age': FormControl<int>(),
'qualif': FormArray([
FormGroup({
'degree': FormControl<String>(value: 'BE'),
'year': FormControl<int>(value: 2000),
})
]),
});
List<Widget> personFormFields = [
ReactiveTextField(
formControlName: 'name',
decoration: const InputDecoration(
label: Text('Name'),
),
),
ReactiveTextField(
formControlName: 'age',
decoration: const InputDecoration(
label: Text('Age'),
),
),
// --------- using formControl ----------
ReactiveFormArray(
formArrayName: 'qualif',
builder: (context, array, child) => Column(
children: [
for (final control in array.controls)
Column(
children: [
ReactiveTextField(
formControl: control as FormControl<String>, // <== ERROR HERE
decoration: const InputDecoration(
label: Text('Degree'),
),
),
ReactiveTextField(
formControl: control,
decoration: const InputDecoration(
label: Text('Year'),
),
),
],
),
],
),
),
// // --------- using formControlName ---------
// ReactiveFormArray(
// formArrayName: 'qualif',
// builder: (context, array, child) => Column(
// children: [
// for (int i = 0; i < array.controls.length; i++)
// Column(
// children: [
// ReactiveTextField(
// formControlName: 'qualif.$i.degree', // <== ERROR HERE
// decoration: const InputDecoration(
// label: Text('Degree'),
// ),
// ),
// ReactiveTextField(
// formControlName: 'qualif.$i.year',
// // formControl: control,
// decoration: const InputDecoration(
// label: Text('Year'),
// ),
// ),
// ],
// ),
// ],
// ),
// ),
];
class PersonFormView extends StatelessWidget {
final FormGroup form;
const PersonFormView(this.form, {super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ReactiveForm(
formGroup: form,
child: Column(
children: <Widget>[
...personFormFields,
],
),
),
);
}
}
Hi @rsbichkar,
Have you tried to use $i.degree as the formControlName in children widgets of the ReactiveFormArray?
Thanks for a very quick reply. It worked. I am sure this approach will work if for higher levels of nesting.
I'm glad that you have solved the issue. Thanks to you for the issue.
I will close it now, but if you have any other questions don't hesitate to create a new one, or reopen this one.
Thank you very much. I tried to embed a FormArray in another class i.e. Person<-College<-Qualif, where Qualif has a FormArray. It also worked fine.
You're most welcome @rsbichkar
Hello Joan,
In continuation with the earlier problem, I am now trying to render a FormArray having FormGroups. I wish to display the add, delete, move_up, and move_down buttons on individual array elements. The entire code is given below. It has following parts:
- Qualif class along with qualifFormGroup() and qualifFormFields(int i) functions
- Person Class along with personFormGroup() and personFormFields() functions. The personFormFields() uses ReactiveArrayFormFields() function to render the array along with the buttons.
- CircularButton class is used to display each button concisely.
The problem is that the add (delete) button adds (deletes) an element at the end of the array instead of the current element. Also the move_up and move_down buttons do not work at all.
I felt that this has something to do with the array state and used ReactiveFormBuilder instead of ReactiveForm. But it did not help. What is the solution?
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';
class Qualif {
final String degree;
final int year;
const Qualif(this.degree, this.year, {Key? key});
}
FormGroup qualifFormGroup() {
return FormGroup({
'degree': FormControl<String>(),
'year': FormControl<int>(),
});
}
List<Widget> qualifFormFields(int i) {
return [
ReactiveTextField(
formControlName: '$i.degree',
// formControl: control as FormControl<String>,
decoration: const InputDecoration(
label: Text('Degree'),
),
),
ReactiveTextField(
formControlName: '$i.year',
// formControl: control as FormControl<String>,
decoration: const InputDecoration(
label: Text('Year'),
),
),
];
}
class Person {
final String name;
final int age;
final List<Qualif> qualif;
const Person(this.name, this.age, this.qualif, {Key? key});
}
FormGroup personFormGroup() {
return FormGroup({
'name': FormControl<String>(),
'age': FormControl<int>(),
'college': FormGroup({
'name': FormControl<String>(),
'qualif': FormArray([
qualifFormGroup(),
]),
})
});
}
List<Widget> personFormFields() {
return [
ReactiveTextField(
formControlName: 'name',
decoration: const InputDecoration(
label: Text('Name'),
),
),
ReactiveTextField(
formControlName: 'age',
decoration: const InputDecoration(
label: Text('Age'),
),
),
const ReactiveArrayFormFields(qualifFormFields, 'Qualifications'),
];
}
class PersonFormView extends StatelessWidget {
final FormGroup form;
const PersonFormView(this.form, {super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
// child: ReactiveForm(
// formGroup: form,
child: ReactiveFormBuilder(
form: () => form,
builder: (context, form, child) {
return Column(
children: personFormFields(),
);
},
),
),
);
}
}
class ReactiveArrayFormFields<T> extends StatelessWidget {
final Function arrayFields;
final String label;
const ReactiveArrayFormFields(
this.arrayFields,
this.label, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// --------- using formControlName ---------
return ReactiveFormArray(
formArrayName: 'college.qualif',
builder: (context, array, child) => Column(
children: [
// for (final control in array.controls)
for (int i = 0; i < array.controls.length; i++)
Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Container(
color: Colors.grey[200],
child: Column(children: [
...arrayFields(i),
const SizedBox(height: 10),
]),
),
),
// const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularButton(
i == 0 ? null : () => array.insert(i - 1, array.removeAt(i)), Icons.move_up, Colors.purple, Colors.white),
CircularButton(
array.controls.length > 1 ? () => array.removeAt(i) : null, Icons.remove, Colors.red, Colors.white),
CircularButton(() => array.add(qualifFormGroup()), Icons.add, Colors.green, Colors.white),
CircularButton(i == array.controls.length - 1 ? null : () => array.insert(i, array.removeAt(i)), Icons.move_down,
Colors.purple, Colors.white),
],
),
),
],
),
const SizedBox(height: 12),
],
),
],
),
);
}
}
class CircularButton extends StatelessWidget {
const CircularButton(this.onPressed, this.icon, this.bgColor, this.fgColor, {Key? key}) : super(key: key);
final VoidCallback? onPressed;
final IconData icon;
final Color bgColor;
final Color fgColor;
// final Key? key;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(8),
backgroundColor: bgColor, // <-- Button color
foregroundColor: fgColor, // <-- Splash color
),
child: Icon(icon, color: Colors.white),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'FormArray Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PersonFormView(personFormGroup()));
}
}
void main() {
runApp(const MyApp());
}
I am sorry. My bad. There is an error in the PersonFormGroup() function which returns a FormGroup for Person. The erroneous code is given below where the 'college' field remained from the previous implementation.
class Person {
final String name;
final int age;
final List<Qualif> qualif;
const Person(this.name, this.age, this.qualif, {Key? key});
}
FormGroup personFormGroup() {
return FormGroup({
'name': FormControl<String>(),
'age': FormControl<int>(),
'college': FormGroup({
'name': FormControl<String>(),
'qualif': FormArray([
qualifFormGroup(),
]),
})
});
}
It should have been written as
FormGroup personFormGroup() {
return FormGroup({
'name': FormControl<String>(),
'age': FormControl<int>(),
'qualif': FormArray([
qualifFormGroup(),
]),
});
}
I have made the correction and the code is working fine. A slight modification is also required in callbacks for Add/Insert (+) and move_down buttons as well.
I will test the code further and report problems if any.
Thanks for your support and sorry once again for raising this issue. If possible, we can delete last submission. I will close this issue shortly. Shall I provide the final working code so that it can help someone else using this feature? May be it can be added as an Example to the library.
Hi @rsbichkar,
Feel free to post the final code. I would advice you to use Keys in all you children widgets of the ReactiveFormArray. All the widgets that you add/remove should have a Key. You could use an ObjectKey(with the control itself as the object), or ValueKey(with a string as the name and/or index position of the control as value)