Update TextFormField's error outside of the validator

Viewed 1681

According to the TextFormField docs, the only way you can have an erorr display below the TextFormField is by returning an error string from the validator function. However, I have a text input which can only be validated after a call to the server, and the response from the server (if valid) also needs to be used later on. Therefore, I'm only doing it when the user presses Submit. However, if the server returns an invalid response, I need to update the error text, but since I'm outside of the validator, I can't do that.

Am I approaching this right? Is there any way to accomplish this?

TextFormField(
    autofocus: true,
    onSaved: (String value) => passcode = value,
),
SizedBox(50.0),
RaisedButtton(
    child: Text('SUBMIT'),
    onPressed: () async {
        _formKey.currentState.save();

        dynamic response = await someServerCall();

        if (response.token) {
            // Valid, use token
        } else {
            // INVALID, update error text somehow
        }
    }
)

(Everything here has various parents, including Column and Form, but this is basically what I'm doing)

3 Answers

You can use flutter_form_bloc.

Each field have the method addError and you can call anywhere, in your case it would be in the onSubmitting method after receiving the response from the server.

class MyFormBloc extends FormBloc<String, String> {
  final email = TextFieldBloc();

  MyFormBloc() {
    addFieldBlocs(fieldBlocs: [email]);
  }

  @override
  void onSubmitting() async {
   // Awesome logic...
   username.addError('That email is taken. Try another.');
  }
}

You can also have asynchronous validators with debounce time

class MyFormBloc extends FormBloc<String, String> {
  final username = TextFieldBloc(
    asyncValidatorDebounceTime: Duration(milliseconds: 300),
  );

  MyFormBloc() {
    addFieldBlocs(fieldBlocs: [username]);

    username.addAsyncValidators([_checkUsername]);
  }

  Future<String> _checkUsername(String username) async {
    await Future.delayed(Duration(milliseconds: 500));
    if (username.toLowerCase() != 'flutter dev') {
      return 'That username is already taken';
    }
    return null;
  }
}

Here is a small demo that you can run and the tutorial is in the form bloc website

pubspec.yaml

dependencies:
  flutter_form_bloc: ^0.11.0

main.dart

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

void main() => runApp(App());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SubmissionErrorToFieldForm(),
    );
  }
}

class SubmissionErrorToFieldFormBloc extends FormBloc<String, String> {
  final username = TextFieldBloc();

  SubmissionErrorToFieldFormBloc() {
    addFieldBlocs(
      fieldBlocs: [
        username,
      ],
    );
  }

  @override
  void onSubmitting() async {
    print(username.value);

    await Future<void>.delayed(Duration(milliseconds: 500));

    if (username.value.toLowerCase() == 'dev') {
      username.addError(
        'Cached - That username is taken. Try another.',
        isPermanent: true,
      );

      emitFailure(failureResponse: 'Cached error was added to username field.');
    } else {
      username.addError('That username is taken. Try another.');

      emitFailure(failureResponse: 'Error was added to username field.');
    }
  }
}

class SubmissionErrorToFieldForm extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => SubmissionErrorToFieldFormBloc(),
      child: Builder(
        builder: (context) {
          final formBloc =
              BlocProvider.of<SubmissionErrorToFieldFormBloc>(context);

          return Theme(
            data: Theme.of(context).copyWith(
              inputDecorationTheme: InputDecorationTheme(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
              ),
            ),
            child: Scaffold(
              appBar: AppBar(title: Text('Submission Error to Field')),
              body: FormBlocListener<SubmissionErrorToFieldFormBloc, String,
                  String>(
                onSubmitting: (context, state) {
                  LoadingDialog.show(context);
                },
                onSuccess: (context, state) {
                  LoadingDialog.hide(context);

                  Navigator.of(context).pushReplacement(
                      MaterialPageRoute(builder: (_) => SuccessScreen()));
                },
                onFailure: (context, state) {
                  LoadingDialog.hide(context);

                  Scaffold.of(context).showSnackBar(
                      SnackBar(content: Text(state.failureResponse)));
                },
                child: SingleChildScrollView(
                  physics: ClampingScrollPhysics(),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      children: <Widget>[
                        TextFieldBlocBuilder(
                          textFieldBloc: formBloc.username,
                          keyboardType: TextInputType.multiline,
                          decoration: InputDecoration(
                            labelText: 'Username',
                            prefixIcon: Icon(Icons.sentiment_very_satisfied),
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Text('"dev" will add a cached error'),
                        ),
                        RaisedButton(
                          onPressed: formBloc.submit,
                          child: Text('SUBMIT'),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class LoadingDialog extends StatelessWidget {
  static void show(BuildContext context, {Key key}) => showDialog<void>(
        context: context,
        useRootNavigator: false,
        barrierDismissible: false,
        builder: (_) => LoadingDialog(key: key),
      ).then((_) => FocusScope.of(context).requestFocus(FocusNode()));

  static void hide(BuildContext context) => Navigator.pop(context);

  LoadingDialog({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async => false,
      child: Center(
        child: Card(
          child: Container(
            width: 80,
            height: 80,
            padding: EdgeInsets.all(12.0),
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}

class SuccessScreen extends StatelessWidget {
  SuccessScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(Icons.tag_faces, size: 100),
            SizedBox(height: 10),
            Text(
              'Success',
              style: TextStyle(fontSize: 54, color: Colors.black),
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 10),
            RaisedButton.icon(
              onPressed: () => Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                      builder: (_) => SubmissionErrorToFieldForm())),
              icon: Icon(Icons.replay),
              label: Text('AGAIN'),
            ),
          ],
        ),
      ),
    );
  }
}

Add a bool check that you would change depending on the server call

TextFormField(
               validator: (value) {
                   if (hasErrorAfterServerCall)
                          return 'Your Error Message';
                   else
                          return null;
                },
);

After the server call is done you can validate the form again

_formKey.currentState!.validate();

Why don't you add server call inside a validator function. Use a validator inside a TextFormField as given below,

TextFormField( 
                validator: _validateEmail,
                  onSaved: (String value) {
                    email = value;
                  },
                ),


  String _validateEmail(String value) async {
//call to a server inside a validator function
        dynamic response = await someServerCall();
         String  _token="";
        if (response.token) {
            // Valid, use token

           setState((){
       _token = response.token
            });
            return null;
        } else {
            // INVALID, update error text somehow
            return "error";
        }

  }

if validator gets null then it will not show any error, but if it gets any string then it will display that string as an error. Now for the button


RaisedButtton(
    child: Text('SUBMIT'),
    onPressed: (){
       if (_formKey.currentState.validate()){} _formKey.currentState.save();}
)

Related