如何将焦点添加到有验证错误的文本表单字段

问题描述 投票:0回答:3

我的表单中有文本表单字段。当我单击提交按钮时,验证错误显示在文本表单字段下。我想将焦点添加到该特定字段,以便当用户单击“保存”按钮时会弹出该字段。用户不需要向上滚动并输入信息。用户应该能够直接开始打字。在当前情况下,用户会感到困惑并且不知道为什么保存按钮不起作用。 Bcoz 直到向上滚动,错误才未知。

 TextFormField(
                  controller: NoteController,
                   validator: (String value){
                    if(value.isEmpty)
                      {
                        return "Note can't be null";
                      }
                    else
                      return null;
                   },
                  decoration: InputDecoration(
                      border: OutlineInputBorder(
                        borderSide: const BorderSide(width: 2.0),)),
                  keyboardType: TextInputType.multiline,
                  minLines: 5,
                  maxLines: 5, 
                  onChanged: (value) {
                    this.note.note = value;
                  },

                ),

bool validateAndSave() {
    final form = globalFormKey.currentState;
    if (form.validate()) {
      form.save();
      return true;
    }
    return false;
  }

void _save() async {
    if (validateAndSave()) {
}
}
flutter validation setfocus
3个回答
1
投票

您可以使用 FocusNode 和 TextField 来实现这一点。你可以在这里读更多关于它的内容 https://flutter.dev/docs/cookbook/forms/focus

让我向您展示一个示例(尝试过并且有效):

构建方法之前:

  final FocusNode noteFocus = FocusNode();

在构建方法中:

 Form(
        key: _formKey,
        child: Column(
          children: [
            TextFormField(
              // Add FocusNode to TextFieldForm
              focusNode: noteFocus,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  // Request focus in case of error
                  noteFocus.requestFocus();
                  return "Note can't be null";
                }
                return null;
              },
            ),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  print('Validated');
                }
              },
              child: Text('Submit'),
            )
          ],
        ),
      ),

如果您还需要什么,请告诉我:)


0
投票

像这样使用 FocusNode。

FocusNode focusNode = FocusNode();

TextFormField(
                  controller: NoteController,
                  focusNode: focusNode,
                   validator: (String value){
                    if(value.isEmpty)
                      {
                        Focus.of(context).requestFocus(focusNode);
                        return "Note can't be null";
                      }
                    else
                      return null;
                   },
                  decoration: InputDecoration(
                      border: OutlineInputBorder(
                        borderSide: const BorderSide(width: 2.0),)),
                  keyboardType: TextInputType.multiline,
                  minLines: 5,
                  maxLines: 5, 
                  onChanged: (value) {
                    this.note.note = value;
                  },

                ),

0
投票

这就是我的整个实现,正是这样做的。最好的部分是它在第一个输入(TextFormField)中设置焦点always,该输入在执行验证器后包含错误。

验证屏幕

import 'package:flutter_267_chat/flutter_resources.dart';
import 'package:flutter_267_chat/external_resources.dart';
import 'package:flutter_267_chat/custom_resources.dart';

final _firebase = FirebaseAuth.instance;

class AuthScreen extends StatefulWidget {
  final String title;
  final bool isSignup;

  const AuthScreen({super.key, this.title = 'Chatty', this.isSignup = true});

  @override
  State<AuthScreen> createState() => _AuthScreenState();
}

class _AuthScreenState extends State<AuthScreen> {
  bool _isLogin = true;
  final _formKey = GlobalKey<FormState>();
  var _isAuthenticating = false;

  var _enteredEmail = '';
  var _enteredUsername = '';
  var _enteredPassword = '';
  File? _selectedImageFile;
  String imageError = '';

  FocusNode focusNodeEmail = FocusNode();
  FocusNode focusNodeUsername = FocusNode();
  FocusNode focusNodePassword = FocusNode();
  List<Map<String, dynamic>> inputsWithErrorsFocusNodes = [];

  void onPickImage(File? aImage) => _selectedImageFile = aImage;

  void toggleAuth() => setState(() => _isLogin = !_isLogin);

  void _logIn() async {
    setState(() => _isAuthenticating = true);
    try {
      final UserCredential userCredential = await _firebase.signInWithEmailAndPassword(email: _enteredEmail, password: _enteredPassword);
    } on FirebaseAuthException catch (error) {
      if (mounted) {
        String? errorMessage;
        if (error.code == 'INVALID_LOGIN_CREDENTIALS') {
          errorMessage = 'Invalid login credentials';
        }
        NotificationsHelper.showSnackBarMessage(context, errorMessage ?? error.message ?? 'Authentication failed');
      }
    }
    if (mounted) setState(() => _isAuthenticating = false);
  }

  void _signUp() async {
    setState(() => _isAuthenticating = true);
    try {
      final UserCredential userCredential = await _firebase.createUserWithEmailAndPassword(email: _enteredEmail, password: _enteredPassword);
      final String userImageFileName = '${userCredential.user!.uid}.jpg';
      final Reference storageReference = FirebaseStorage.instance.ref().child('user_images').child(userImageFileName);
      await storageReference.putFile(_selectedImageFile!);
      final String uploadedImageUrl = await storageReference.getDownloadURL();

      final DocumentReference<Map<String, dynamic>> userReference = FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid);
      final Map<String, dynamic> userData = {
        'username': _enteredUsername,
        'email': userCredential.user!.email,
        'image_url': uploadedImageUrl,
      };
      await userReference.set(userData);
    } on FirebaseAuthException catch (error) {
      if (mounted) {
        if (error.code == 'email-already-in-use') {
          //
        }
        NotificationsHelper.showSnackBarMessage(context, error.message ?? 'Authentication failed');
      }
    }

    if (mounted) setState(() => _isAuthenticating = false);
  }

  void focusInputsWithErrors() {
    if (inputsWithErrorsFocusNodes.isNotEmpty) {
      inputsWithErrorsFocusNodes.sort((a, b) => (b['index']).compareTo(a['index']));
      inputsWithErrorsFocusNodes = inputsWithErrorsFocusNodes.reversed.toList();
      final Map<String, dynamic> firstInputFocusNodeMap = inputsWithErrorsFocusNodes.first;
      final FocusNode firstInputFocusNode = firstInputFocusNodeMap['node'];
      firstInputFocusNode.requestFocus();
    }
  }

  void _submit() async {
    inputsWithErrorsFocusNodes.clear();
    setState(() => imageError = _selectedImageFile == null ? 'Must pick a picture' : '');

    final bool formIsValid = _formKey.currentState!.validate();
    final bool signUpImageIsNull = !_isLogin && _selectedImageFile == null;
    if (!formIsValid || signUpImageIsNull) {
      focusInputsWithErrors();
      return;
    }

    _formKey.currentState!.save();
    _isLogin ? _logIn() : _signUp();

    if (!context.mounted) return;
  }

  void _resetForm() {
    setState(() => imageError = '');
    _formKey.currentState!.reset();
    inputsWithErrorsFocusNodes.clear();
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData themeData = Theme.of(context);
    final TextTheme textTheme = themeData.textTheme;
    final ColorScheme colorScheme = themeData.colorScheme;
    final emailFocusNodeMap = {'node': focusNodeEmail, 'index': 1};
    final usernameFocusNodeMap = {'node': focusNodeUsername, 'index': 2};
    final passwordFocusNodeMap = {'node': focusNodePassword, 'index': 3};

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      backgroundColor: colorScheme.primary,
      body: Center(
        child: SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // Logo
              Container(
                margin: EdgeInsets.only(top: 30, bottom: 20, left: 20, right: 20),
                width: 140,
                child: Image.asset('assets/images/chat.png'),
              ),

              // Auth Card with the form
              Card(
                margin: EdgeInsets.all(20),
                child: SingleChildScrollView(
                  child: Padding(
                    padding: EdgeInsets.all(16),
                    child: Form(
                        key: _formKey,
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            // User Image Picker
                            // if (!_isLogin) UserImagePicker(onPickImage: (d) {}),
                            AnimatedSwitcher(
                              duration: const Duration(milliseconds: 500),
                              // reverseDuration: const Duration(milliseconds: 500),
                              child: !_isLogin
                                  ? Column(
                                      children: [
                                        UserImagePicker(onPickImage: onPickImage),
                                        SizedBox(height: 8),
                                        // Image Error
                                        Text(imageError, style: TextStyle(color: Color(0xFFBA1A1A))),
                                      ],
                                    )
                                  : null,
                            ),

                            // Email Input
                            TextFormField(
                              decoration: InputDecoration(labelText: 'Email address', helperText: ' '),
                              focusNode: focusNodeEmail,
                              keyboardType: TextInputType.emailAddress,
                              autocorrect: false,
                              textCapitalization: TextCapitalization.none,
                              validator: (String? value) {
                                if (value == null || value.trim().isEmpty) {
                                  inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
                                  return 'The email can not be blank';
                                }
                                final bool isValidEmail = EmailValidator.validate(value);
                                if (!isValidEmail) {
                                  inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
                                  return 'The email must be valid';
                                }

                                return null;
                              },
                              onSaved: (String? value) {
                                _enteredEmail = value!.trim();
                              },
                              onFieldSubmitted: (String? value) => _submit(),
                            ),

                            // Username Input
                            AnimatedSwitcher(
                              duration: const Duration(milliseconds: 500),
                              // reverseDuration: const Duration(milliseconds: 900),
                              child: !_isLogin
                                  ? TextFormField(
                                      decoration: InputDecoration(labelText: 'Username', helperText: ' '),
                                      enableSuggestions: false,
                                      focusNode: focusNodeUsername,
                                      keyboardType: TextInputType.name,
                                      // autocorrect: false,
                                      validator: (String? value) {
                                        if (value == null || value.trim().isEmpty) {
                                          inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);

                                          return 'The username can not be blank';
                                        }
                                        if (value.trim().length < 3) {
                                          inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);

                                          return 'The username must be at least 3 characters long';
                                        }

                                        return null;
                                      },
                                      onSaved: (String? value) {
                                        _enteredUsername = value!;
                                      },
                                      onFieldSubmitted: (String? value) => _submit(),
                                    )
                                  : null,
                            ),

                            // Password Input
                            TextFormField(
                              decoration: InputDecoration(labelText: 'Password', helperText: ' '),
                              focusNode: focusNodePassword,
                              // autocorrect: false,
                              obscureText: true,
                              obscuringCharacter: '*',
                              validator: (String? value) {
                                if (value == null || value.trim().isEmpty) {
                                  inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);

                                  return 'The password can not be blank';
                                }
                                if (value.trim().length < 6) {
                                  inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);
                                  return 'The password must be at least 6 characters long';
                                }

                                return null;
                              },
                              onSaved: (String? value) {
                                _enteredPassword = value!;
                              },
                              onFieldSubmitted: (String? value) => _submit(),
                            ),

                            // SizedBox(height: 12),

                            Stack(
                              children: [
                                // Submit & Reset buttons
                                Opacity(
                                  opacity: _isAuthenticating ? 0 : 1,
                                  child: Row(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: [
                                      // Submit button
                                      ElevatedButton(
                                        onPressed: _submit,
                                        style: ElevatedButton.styleFrom(backgroundColor: colorScheme.primaryContainer),
                                        child: Text(_isLogin ? 'Log in' : 'Create account'),
                                      ),
                                      SizedBox(width: 12),
                                      // Reset button
                                      ElevatedButton(
                                        onPressed: _resetForm,
                                        style: ElevatedButton.styleFrom(backgroundColor: colorScheme.primaryContainer),
                                        child: Text('Reset'),
                                      ),
                                    ],
                                  ),
                                ),

                                if (_isAuthenticating)
                                  Row(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: [
                                      CircularProgressIndicator(),
                                    ],
                                  ),
                              ],
                            ),

                            // SizedBox(height: 12),

                            // Auth Toggling button
                            Opacity(
                              opacity: _isAuthenticating ? 0 : 1,
                              child: TextButton(
                                onPressed: toggleAuth,
                                child: Text(
                                  _isLogin ? 'No account? Sign up' : 'Have an account? Log in',
                                  style: textTheme.labelSmall,
                                ),
                              ),
                            ),
                          ],
                        )),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在此示例中,我们有三个 TextFormField(电子邮件、用户名和密码)。重要步骤如下。

第 1 步: 在 StatefulWidget State 类中声明变量。

  FocusNode focusNodeEmail = FocusNode();
  FocusNode focusNodeUsername = FocusNode();
  FocusNode focusNodePassword = FocusNode();
  List<Map<String, dynamic>> inputsWithErrorsFocusNodes = [];

第2步:将每个FocusNode与其各自的TextFormField链接起来并触发填写验证。

                    // Email Input
                    TextFormField(
                      decoration: InputDecoration(labelText: 'Email address', helperText: ' '),
                      focusNode: focusNodeEmail,
                      keyboardType: TextInputType.emailAddress,
                      autocorrect: false,
                      textCapitalization: TextCapitalization.none,
                      validator: (String? value) {
                        if (value == null || value.trim().isEmpty) {
                          inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
                          return 'The email can not be blank';
                        }
                        final bool isValidEmail = EmailValidator.validate(value);
                        if (!isValidEmail) {
                          inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
                          return 'The email must be valid';
                        }

                        return null;
                      },
                      onSaved: (String? value) {
                        _enteredEmail = value!.trim();
                      },
                      onFieldSubmitted: (String? value) => _submit(),
                    ),

                    // Username Input
                    AnimatedSwitcher(
                      duration: const Duration(milliseconds: 500),
                      // reverseDuration: const Duration(milliseconds: 900),
                      child: !_isLogin
                          ? TextFormField(
                              decoration: InputDecoration(labelText: 'Username', helperText: ' '),
                              enableSuggestions: false,
                              focusNode: focusNodeUsername,
                              keyboardType: TextInputType.name,
                              // autocorrect: false,
                              validator: (String? value) {
                                if (value == null || value.trim().isEmpty) {
                                  inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);

                                  return 'The username can not be blank';
                                }
                                if (value.trim().length < 3) {
                                  inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);

                                  return 'The username must be at least 3 characters long';
                                }

                                return null;
                              },
                              onSaved: (String? value) {
                                _enteredUsername = value!;
                              },
                              onFieldSubmitted: (String? value) => _submit(),
                            )
                          : null,
                    ),

                    // Password Input
                    TextFormField(
                      decoration: InputDecoration(labelText: 'Password', helperText: ' '),
                      focusNode: focusNodePassword,
                      // autocorrect: false,
                      obscureText: true,
                      obscuringCharacter: '*',
                      validator: (String? value) {
                        if (value == null || value.trim().isEmpty) {
                          inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);

                          return 'The password can not be blank';
                        }
                        if (value.trim().length < 6) {
                          inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);
                          return 'The password must be at least 6 characters long';
                        }

                        return null;
                      },
                      onSaved: (String? value) {
                        _enteredPassword = value!;
                      },
                      onFieldSubmitted: (String? value) => _submit(),
                    ),

第三步:调用submit时,必须先清空已有的List,然后才能验证。

  void _submit() async {
    inputsWithErrorsFocusNodes.clear();
    setState(() => imageError = _selectedImageFile == null ? 'Must pick a picture' : '');

    final bool formIsValid = _formKey.currentState!.validate();
    final bool signUpImageIsNull = !_isLogin && _selectedImageFile == null;
    if (!formIsValid || signUpImageIsNull) {
      focusInputsWithErrors();
      return;
    }

    _formKey.currentState!.save();
    _isLogin ? _logIn() : _signUp();

    // final newPlace = PlaceModel(title: _enteredTitle, image: selectedImage!, location: selectedPlaceLocation!);
    // Navigator.of(context).pop(newPlace);

    if (!context.mounted) return;
  }

第 4 步: 这是排序、反转然后聚焦的地方。

  void focusInputsWithErrors() {
    if (inputsWithErrorsFocusNodes.isNotEmpty) {
      inputsWithErrorsFocusNodes.sort((a, b) => (b['index']).compareTo(a['index']));
      inputsWithErrorsFocusNodes = inputsWithErrorsFocusNodes.reversed.toList();
      final Map<String, dynamic> firstInputFocusNodeMap = inputsWithErrorsFocusNodes.first;
      final FocusNode firstInputFocusNode = firstInputFocusNodeMap['node'];
      firstInputFocusNode.requestFocus();
    }
  }

第 5 步: 当然,我们还必须清除重置中的地图列表。

  void _resetForm() {
    setState(() => imageError = '');
    _formKey.currentState!.reset();
    inputsWithErrorsFocusNodes.clear();
  }

视觉效果:

© www.soinside.com 2019 - 2024. All rights reserved.