5

Can we get the list of fields in error from Flutter forms after validation ? This will help developers use focus-nodes to redirect the attention to the field in error.

2 Answers 2

5

I don't think it is possible to get this kind of information from a Form object or a FormState.

But here is a way around to obtain the result you want (focus on the field in error) :

class _MyWidgetState extends State<MyWidget> {
  FocusNode _fieldToFocus;
  List<FocusNode> _focusNodes;

  final _formKey = GlobalKey<FormState>();
  final _numberOfFields = 3;

  String _emptyFieldValidator(String val, FocusNode focusNode) {
    if (val.isEmpty) {
      _fieldToFocus ??= focusNode;
      return 'This field cannot be empty';
    }
    return null;
  }

  @override
  void initState() {
    super.initState();
    _focusNodes =
        List<FocusNode>.generate(_numberOfFields, (index) => FocusNode());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(actions: [
        IconButton(
          icon: Icon(Icons.check),
          onPressed: () {
            if (_formKey.currentState.validate()) {
              print('Valid form');
            } else {
              _fieldToFocus?.requestFocus();
              _fieldToFocus = null;
            }
          },
        ),
      ]),
      body: Form(
        key: _formKey,
        child: Column(children: [
          ...List<TextFormField>.generate(
            _numberOfFields,
            (index) => TextFormField(
              decoration: InputDecoration(hintText: "Field $index"),
              focusNode: _focusNodes[index],
              validator: (val) => _emptyFieldValidator(val, _focusNodes[index]),
            ),
          ),
        ]),
      ),
    );
  }
}

You simply need to create a FocusNode for each one of your fields, thanks to that you will be abla to call requestFocus on a precise field (in your case a field considered as invalid). Then in the validator property of your form field, as it is the method called by the FormState.validate(), you need to set a temporary variable which will contains the right FocusNode. In my example I only set the variable _fieldToFocus if it was not already assigned using the ??= operator. After requesting the focus on the node I set _fieldToFocus back to null so it will still works for another validation.

You can try the full test code I have used on DartPad.

Sorry if I have derived a bit from your question but I still hope this will help you.

Sign up to request clarification or add additional context in comments.

Comments

4

Expanding on Guillaume's answer, I've wrapped the functionality into a reusable class.

You can view a working example on DartPad here: https://www.dartpad.dev/61c4ccddbf29a343c971ee75e60d1038

import 'package:flutter/material.dart';

class FormValidationManager {
  final _fieldStates = Map<String, FormFieldValidationState>();

  FocusNode getFocusNodeForField(key) {
      _ensureExists(key);

      return _fieldStates[key].focusNode;
  }

  FormFieldValidator<T> wrapValidator<T>(String key, FormFieldValidator<T> validator) {
      _ensureExists(key);

      return (input) {
          final result = validator(input);

          _fieldStates[key].hasError = (result?.isNotEmpty ?? false);

          return result;
      };
  }

  List<FormFieldValidationState> get erroredFields =>
      _fieldStates.entries.where((s) => s.value.hasError).map((s) => s.value).toList();

  void _ensureExists(String key) {
      _fieldStates[key] ??= FormFieldValidationState(key: key);
  }

  void dispose() {
    _fieldStates.entries.forEach((s) {
        s.value.focusNode.dispose();
    });
  }
}

class FormFieldValidationState {
  final String key;

  bool hasError;
  FocusNode focusNode;

  FormFieldValidationState({@required this.key})
      : hasError = false,
      focusNode = FocusNode();
}

To use it, create your forms as usual, but add a FormValidationManager to your state class, and then use that instance to wrap your validation methods.

Usage:

class _MyWidgetState extends State<MyWidget> {
  final _formKey = GlobalKey<FormState>();
  final _formValidationManager = FormValidationManager();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
              focusNode: _formValidationManager.getFocusNodeForField('field1'),
              validator: _formValidationManager.wrapValidator('field1', (value) {
                if (value.isEmpty) {
                  return 'Please enter a value';
                }

                return null;
              })),
          TextFormField(
              focusNode: _formValidationManager.getFocusNodeForField('field2'),
              validator: _formValidationManager.wrapValidator('field2', (value) {
                if (value.isEmpty) {
                  return 'Please enter a value';
                }

                return null;
              })),
          ElevatedButton(
              onPressed: () {
                if (!_formKey.currentState.validate()) {
                  _formValidationManager.erroredFields.first.focusNode.requestFocus();
                }
              },
              child: Text('SUBMIT'))
        ],
      ),
    );
  }

  @override
  void dispose() {
    _formValidationManager.dispose();
    super.dispose();
  }
}

1 Comment

What a clever solution! U save my life ❤️. A note for others, remember to call _formValidationManager.dispose() in the dispose() method of your StatefulWidget's State

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.