joanpablo / reactive_forms

This is a model-driven approach to handling form inputs and validations, heavily inspired in Angular's Reactive Forms
MIT License
471 stars 90 forks source link

Unable to implement FormArray having FormGroups #350

Open rsbichkar opened 1 year ago

rsbichkar commented 1 year ago

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,
          ],
        ),
      ),
    );
  }
}
joanpablo commented 1 year ago

Hi @rsbichkar,

Have you tried to use $i.degree as the formControlName in children widgets of the ReactiveFormArray?

rsbichkar commented 1 year ago

Thanks for a very quick reply. It worked. I am sure this approach will work if for higher levels of nesting.

joanpablo commented 1 year ago

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.

rsbichkar commented 1 year ago

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.

joanpablo commented 1 year ago

You're most welcome @rsbichkar

rsbichkar commented 1 year ago

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:

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());
}
rsbichkar commented 1 year ago

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.

joanpablo commented 1 year ago

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)