Github Repo : Validator Using Cubit - Generic Concept - Full Code
My goal is to apply form validation using Cubit, and I've successfully made progress in the process. However, a challenge arose while working on the project. There are two TextFormFields, one of which is a standard editable text field while the other is a disabled DropDown. When the disabled text field is selected, its value should be assigned to the first editable field, which is the Address field.
Problem: When the code below is inserted into the AddressInput TextFormField and tapped on Disbale TextField, it will set "Hello" successfully on AddressInput. However, modifying the AddressInput results in different behavior and incorrect functioning. Whenever attempts are made to delete or alter the text, the text field always redirects to the first position.
controller: TextEditingController(
text: state.formFields['address']?.value,
),
I have included the code.
form_field_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';
import 'package:test/common/validator/cubit/generic_form_field_state.dart';
import 'package:test/common/validator/models/optional_input_field.dart';
import 'package:test/common/validator/models/username_input_field.dart';
import '../../../../../common/validator/cubit/generic_form_field_cubit.dart';
final _formFields = <String, FormzInput>{
'address': const UserNameInputField.pure(),
'remarks': const OptionalInputField.pure(),
};
final _cubit = GenericFormFieldCubit<FormzInput>(_formFields);
class FormFieldPage extends StatelessWidget {
const FormFieldPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _cubit,
child: Scaffold(
appBar: AppBar(
title: Text(""),
),
body: _buildFormFields(),
),
);
}
}
Widget _buildFormFields() {
return Column(
children: [
_AddressInput(),
const SizedBox(
height: 10,
),
_RemarksInput()
],
);
}
class _RemarksInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<GenericFormFieldCubit, GenericFormFieldState>(
buildWhen: (previous, current) =>
previous.formFields['remarks'] != current.formFields['remarks'],
builder: (context, state) {
return GestureDetector(
onTap: () {
_cubit.updateField('address', UserNameInputField.custom("Hello"));
},
child: TextField(
enabled: false,
readOnly: true,
key: const Key("personalInfoForm_remarksInput_textField"),
onChanged: (value) {
final name = OptionalInputField.dirty(value);
_cubit.updateField('remarks', name);
},
decoration: const InputDecoration(
labelText: 'Remarks',
),
),
);
},
);
}
}
class _AddressInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<GenericFormFieldCubit, GenericFormFieldState>(
buildWhen: (previous, current) =>
previous.formFields['address'] != current.formFields['address'],
builder: (context, state) {
return TextField(
key: const Key('personalInfoForm_addressInput_textField'),
onChanged: (value) {
final name = UserNameInputField.custom(value,
errorMessage: 'Please enter a address',
requiredMessage: 'This field is required field',
minValue: '5',
maxValue: '20');
_cubit.updateField('address', name);
},
decoration: InputDecoration(
labelText: 'Address',
errorText:
(state.formFields['address'] as UserNameInputField).invalid
? (state.formFields['address'] as UserNameInputField)
.getErrorMessage
: null,
),
);
},
);
}
}
generic_form_cubit.dart
import 'package:bloc/bloc.dart';
import 'package:formz/formz.dart';
import 'package:test/common/validator/cubit/generic_form_field_state.dart';
/// Generic Cubit Class that handles the state of a set of Formz input fields.
/// Accepts a map of initial form field values in its constructor, and initializes the state of the Cubit with this map.
class GenericFormFieldCubit<T extends FormzInput>
extends Cubit<GenericFormFieldState<T>> {
GenericFormFieldCubit(Map<String, T> formFields)
: super(GenericFormFieldState<T>(formFields: formFields));
/// Update the value of a field in the form
/// @param fieldName: the key of the field to update
/// @param fieldValue: the new value to set for the field
void updateField(String fieldName, T fieldValue) {
/// Create a copy of the current fields map with the updated field
final updatedFields = Map<String, T>.from(state.formFields);
updatedFields[fieldName] = fieldValue;
/// Validate the updated fields and emit a new state with the updated fields and status
emit(state.copyWith(
formFields: updatedFields,
status: Formz.validate(updatedFields.values.toList()),
));
}
}
generic_form_feild_state.dart
import 'package:formz/formz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generic_form_field_state.freezed.dart';
/// Store the state of form field
/// Has two properties, @param `formFields` which is a map of form field values with keys as the name of the fields,
/// and @param `status` which is the status of the overall form.
@freezed
abstract class GenericFormFieldState<T extends FormzInput>
with _$GenericFormFieldState<T> {
const factory GenericFormFieldState({
required Map<String, T> formFields,
@Default(FormzStatus.pure) FormzStatus status,
}) = _GenericFormFieldState<T>;
}
username_input_field.dart
import 'package:formz/formz.dart';
enum UserNameValidationError { invalid, empty, isNull }
class UserNameInputField extends FormzInput<String, UserNameValidationError> {
final String? errorMessage;
final String? requiredMessage;
final String? minValue;
final String? maxValue;
const UserNameInputField.pure([String value = ''])
: errorMessage = '',
requiredMessage = '',
minValue = '',
maxValue = '',
super.pure(value);
const UserNameInputField.dirty([String value = ''])
: errorMessage = '',
requiredMessage = '',
minValue = '',
maxValue = '',
super.dirty(value);
UserNameInputField.custom(
String value, {
this.errorMessage,
this.requiredMessage,
this.minValue,
this.maxValue,
}) : super.dirty(value);
@override
UserNameValidationError? validator(String value) {
if (value == null) return UserNameValidationError.isNull;
if (value.isEmpty) return UserNameValidationError.empty;
if (minValue != null && value.length < int.parse(minValue!)) {
return UserNameValidationError.invalid;
}
if (maxValue != null && value.length > int.parse(maxValue!)) {
return UserNameValidationError.invalid;
}
return UserNameValidationError.isNull;
}
}
extension NameInputErrorMessageExtension on UserNameInputField {
String? get getErrorMessage {
if (error == UserNameValidationError.isNull) return null;
if (error == UserNameValidationError.invalid) return errorMessage;
return error == UserNameValidationError.empty ? requiredMessage : null;
}
}
optional_input_field.dart
import 'package:formz/formz.dart';
enum OptionalFieldInputValidationError { isNull }
class OptionalInputField
extends FormzInput<String, OptionalFieldInputValidationError> {
const OptionalInputField.pure([String value = '']) : super.pure(value);
const OptionalInputField.dirty([String value = '']) : super.dirty(value);
@override
OptionalFieldInputValidationError? validator(String value) {
return OptionalFieldInputValidationError.isNull;
}
}
extension OptionalFieldInputErrorMessageExtension on OptionalInputField {
String? get getErrorMessage {
if (error == OptionalFieldInputValidationError.isNull) return null;
return null;
}
}