0

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;
  }
}
Rabindra Acharya
  • 1,868
  • 2
  • 18
  • 32

1 Answers1

0

I found a solution to fix the problem. Thank you @kristifor for your help.

    class _AddressInput extends StatelessWidget {
  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<GenericFormFieldCubit, GenericFormFieldState>(
      buildWhen: (previous, current) =>
          previous.formFields['address'] != current.formFields['address'],
      builder: (context, state) {
        final currentValue = state.formFields['address']?.value;
        TextSelection previousSelection = _textEditingController.selection;

        final editValue = TextEditingValue(
          text: currentValue,
          selection: previousSelection,
        );

        _textEditingController.value = editValue;
        return TextFormField(
          controller: _textEditingController,
          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',
            hintText: 'Enter Address',
            errorText:
                (state.formFields['address'] as UserNameInputField).invalid
                    ? (state.formFields['address'] as UserNameInputField)
                        .getErrorMessage
                    : null,
          ),
        );
      },
    );
  }
}
Rabindra Acharya
  • 1,868
  • 2
  • 18
  • 32