reactive_forms icon indicating copy to clipboard operation
reactive_forms copied to clipboard

How to check only one checkbox?

Open taskindrp opened this issue 2 years ago • 11 comments

I have array of checkboxes, I need to make it checkable only one of them.

I tried like this but I can check both of them:

final form = fb.group({
  'question': FormArray<bool>(
            [
              FormControl<bool>(value: false),
              FormControl<bool>(value: false),
            ],
});
ReactiveFormArray<bool>(
                      formArrayName: entry.key,
                      builder: (_, array, __) {
                        return Column(
                          children: array.value!
                              .asMap()
                              .entries
                              .map(
                                (e) => ReactiveCheckboxListTile(
                                  key: ValueKey('question${e.key}'),
                                  formControlName: '${e.key}',
                                  title: Text(e.key == 1 ? 'No' : 'Yes'),
                                ),
                              )
                              .toList(),
                        );
                      },
                    ),

CleanShot 2022-07-29 at 09 13 34

taskindrp avatar Jul 29 '22 06:07 taskindrp

You need a radio buttons Checkboxes allow to check all of them. So basically you are using wrong control to achieve you goals. This is why you have issues and your users will be confused. In other case is you still want to achieve this behavior use listener from reactive_forms_lbc package and combining prev and current value - reset the undesired fields to false. But better use radio.

vasilich6107 avatar Jul 29 '22 19:07 vasilich6107

But better use radio.

tried with Radio but I can check both of them, should the control name should be same?

taskindrp avatar Aug 01 '22 06:08 taskindrp

Hi @taskindrp,

Here is a simple code example:

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

class RadioSample extends StatelessWidget {
  const RadioSample({Key? key}) : super(key: key);

  static const questionField = 'question';

  FormGroup get form => FormGroup({
        questionField: FormControl<bool>(value: false),
      });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ReactiveFormBuilder(
        form: () => form,
        builder: (context, form, child) {
          return Column(
            children: [
              ReactiveRadioListTile(
                formControlName: questionField,
                title: const Text('Yes'),
                value: true,
              ),
              ReactiveRadioListTile(
                formControlName: questionField,
                title: const Text('No'),
                value: false,
              ),
            ],
          );
        },
      ),
    );
  }
}

This is a very simple sample. I have used ReactiveFormBuilder, but the result is the same if you use ReactiveForm in combination with a StatefulWidget or if you use ReactiveForm in combination with a State Management Library (We always recommend this last approach for a more robust Flutter applications).

Notice that the FormControl is only one, the same control in bound to both ReactiveRadioListTile.

I hope this clarifies your questions.

Using checkboxes and an array is also possible to implement but requires more code a little bit of complexity because you need to manually handle turn off/on all the checkboxes. So we recommend using Radios, it is simpler and intuitive to UI users.

joanpablo avatar Aug 01 '22 11:08 joanpablo

Hi, @taskindrp any update on this issue? Was it useful the code above? Do you still want to use checkboxes or the radio buttons is enough for you?

joanpablo avatar Aug 07 '22 14:08 joanpablo

Hi, @joanpablo

I used checkboxes as suggested by @vasilich6107 using reactive_forms_lbc package.

In other case is you still want to achieve this behavior use listener from reactive_forms_lbc package and combining prev and current value - reset the undesired fields to false.

also tried your implementation @joanpablo but still could check both radio buttons.

taskindrp avatar Aug 08 '22 05:08 taskindrp

also tried your implementation @joanpablo but still could check both radio buttons.

Hi @taskindrp,

I'm glad that you have solved the issue using reactive_forms_lbc, but I want to say that the implementation I had shown you above using ReactiveRadioListTile is 100% fully functional.

I will create 2 more Fully Flutter App examples and I will post you here with a video so you can understand the implementation from scratch.

Are you using any specific State Management Library?

joanpablo avatar Aug 08 '22 10:08 joanpablo

Are you using any specific State Management Library?

Yes, riverpod but it is not connected to the form.

taskindrp avatar Aug 08 '22 10:08 taskindrp

Another question that I have for you:

What exactly is your expected result:

1- One answer to all questions, something like: { answer: 'Option1' }

2- Or do you want to get an array of booleans like:

{ answers: [false, true] }

joanpablo avatar Aug 08 '22 11:08 joanpablo

Like { answers: [false, true] } also { answers: [false, true, false] } etc. number of checkboxes can grow dynamically.

taskindrp avatar Aug 08 '22 11:08 taskindrp

Hi @taskindrp

sorry the delay. This is a fully functional version:

chrome-capture-2022-7-8

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

class Question {
  final String label;

  const Question({
    required this.label,
  });
}

class QuestionsRepository {
  Future<List<Question>> loadQuestions() {
    return Future.delayed(
      const Duration(seconds: 3),
      () => [
        const Question(label: 'Yes'),
        const Question(label: 'A lot'),
        const Question(label: 'The best ever'),
        const Question(label: 'I only dream with Flutter'),
      ],
    );
  }
}

enum QuestionsState {
  init,
  loading,
  loaded,
}

class FormFields {
  static const answers = 'answers';
}

class OptionsCubit extends Cubit<QuestionsState> {
  final QuestionsRepository repository;

  final List<Question> questions = [];

  final FormGroup form = FormGroup({
    FormFields.answers: FormArray<bool>([], validators: [
      Validators.contains([true])
    ]),
  });

  OptionsCubit(this.repository) : super(QuestionsState.init);

  FormArray<bool> get answers =>
      form.control(FormFields.answers) as FormArray<bool>;

  bool get isLoading => state == QuestionsState.loading;

  void loadQuestions() async {
    if (isLoading) {
      return;
    }

    emit(QuestionsState.loading);

    questions.addAll(await repository.loadQuestions());
    createControlsFromQuestions();

    emit(QuestionsState.loaded);
  }

  void createControlsFromQuestions() {
    answers.addAll(
      questions.map((_) => FormControl<bool>(value: false)).toList(),
      emitEvent: false,
    );
  }

  void onControlSelected(FormControl<bool> selectedControl) {
    for (var control in answers.controls) {
      if (control != selectedControl) {
        control.value = false;
      }
    }
  }
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: BlocProvider<OptionsCubit>(
        create: (context) => OptionsCubit(QuestionsRepository()),
        child: Scaffold(
          body: BlocBuilder<OptionsCubit, QuestionsState>(
            builder: (context, state) {
              switch (state) {
                case QuestionsState.loading:
                  return const Loading();
                case QuestionsState.loaded:
                  return const QuestionsForm();
                default:
                  return const LoadButton();
              }
            },
          ),
        ),
      ),
    );
  }
}

class Loading extends StatelessWidget {
  const Loading({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: CircularProgressIndicator(),
    );
  }
}

class LoadButton extends StatelessWidget {
  const LoadButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        child: const Text('Load Questions'),
        onPressed: () => context.read<OptionsCubit>().loadQuestions(),
      ),
    );
  }
}

class QuestionsForm extends StatelessWidget {
  const QuestionsForm({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final provider = context.read<OptionsCubit>();

    return ReactiveForm(
      formGroup: provider.form,
      child: Column(
        children: <Widget>[
          const Text(
            'Do you like Flutter?',
            style: TextStyle(fontSize: 20.0),
          ),
          for (var i = 0; i < provider.questions.length; i++)
            _buildCheckbox(provider, i),
          const FormAnswersIndicator(),
          const FormStatusIndicator(),
        ],
      ),
    );
  }

  Widget _buildCheckbox(OptionsCubit provider, int i) {
    return ReactiveCheckboxListTile(
      title: Text(provider.questions.elementAt(i).label),
      formControlName: '${FormFields.answers}.$i',
      onChanged: (control) {
        if (control.value == true) {
          provider.onControlSelected(control);
        }
      },
    );
  }
}

class FormAnswersIndicator extends StatelessWidget {
  const FormAnswersIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ReactiveValueListenableBuilder(
      formControlName: FormFields.answers,
      builder: (context, control, child) {
        return Text(control.value.toString());
      },
    );
  }
}

class FormStatusIndicator extends StatelessWidget {
  const FormStatusIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ReactiveFormConsumer(
      builder: (context, formGroup, child) {
        return formGroup.valid
            ? const Text('form VALID')
            : const Text('form INVALID', style: TextStyle(color: Colors.red));
      },
    );
  }
}

joanpablo avatar Aug 08 '22 17:08 joanpablo

The previous example was implemented with the following dependencies:

bloc: ^8.0.0
flutter_bloc: ^8.0.0
reactive_forms: ^14.1.0

joanpablo avatar Aug 08 '22 17:08 joanpablo