Skip to content

About

A unified form representation in Dart used at Very Good Ventures πŸ¦„

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Β 
Β 

Repository files navigation

πŸ“ Formz

Very Good Ventures Very Good Ventures

Developed with πŸ’™ by Very Good Ventures πŸ¦„

ci coverage pub package License: MIT style: very good analysis


A unified form representation in Dart. Formz aims to simplify form representation and validation in a generic way.

Create a FormzInput

import 'package:formz/formz.dart';

// Define input validation errors
enum NameInputError { empty }

// Extend FormzInput and provide the input type and error type.
class NameInput extends FormzInput<String, NameInputError> {
  // Call super.pure to represent an unmodified form input.
  const NameInput.pure() : super.pure('');

  // Call super.dirty to represent a modified form input.
  const NameInput.dirty({String value = ''}) : super.dirty(value);

  // Override validator to handle validating a given input value.
  @override
  NameInputError? validator(String value) {
    return value.isEmpty ? NameInputError.empty : null;
  }
}

Interact with a FormzInput

const name = NameInput.pure();
print(name.value); // ''
print(name.isValid); // false
print(name.error); // NameInputError.empty
print(name.displayError); // null

const joe = NameInput.dirty(value: 'joe');
print(joe.value); // 'joe'
print(joe.isValid); // true
print(joe.error); // null
print(name.displayError); // null

Validate Multiple FormzInput Items

const validInputs = <FormzInput>[
  NameInput.dirty(value: 'jan'),
  NameInput.dirty(value: 'jen'),
  NameInput.dirty(value: 'joe'),
];

print(Formz.validate(validInputs)); // true

const invalidInputs = <FormzInput>[
  NameInput.dirty(),
  NameInput.dirty(),
  NameInput.dirty(),
];

print(Formz.validate(invalidInputs)); // false

Automatic Validation

class LoginForm with FormzMixin {
  LoginForm({
    this.username = const Username.pure(),
    this.password = const Password.pure(),
  });

  final Username username;
  final Password password;

  @override
  List<FormzInput> get inputs => [username, password];
}

void main() {
  print(LoginForm().isValid); // false
}

Caching validation results

For cases where the validator method has an expensive implementation, consider using the FormzInputErrorCacheMixin mixin to cache the error result and improve performance.

import 'package:formz/formz.dart';

enum EmailValidationError { invalid }

class Email extends FormzInput<String, EmailValidationError>
    with FormzInputErrorCacheMixin {
  Email.pure([super.value = '']) : super.pure();

  Email.dirty([super.value = '']) : super.dirty();

  static final _emailRegExp = RegExp(
    r'^[a-zA-Z\d.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)*$',
  );

  @override
  EmailValidationError? validator(String value) {
    return _emailRegExp.hasMatch(value) ? null : EmailValidationError.invalid;
  }
}

Asynchronous validation

When part of the validation is asynchronous, for example checking if a username is taken or validating a phone number with a plugin, use the FormzInputAsyncValidationMixin mixin. Formz does not run the asynchronous validation itself. You run it wherever you manage your state and store its result in the input through asyncStatus and asyncError.

import 'package:formz/formz.dart';

enum PhoneNumberError { empty, invalid }

class PhoneNumber extends FormzInput<String, PhoneNumberError>
    with FormzInputAsyncValidationMixin {
  const PhoneNumber.pure()
      : asyncStatus = FormzAsyncValidationStatus.initial,
        asyncError = null,
        super.pure('');

  const PhoneNumber.dirty(
    super.value, {
    this.asyncStatus = FormzAsyncValidationStatus.initial,
    this.asyncError,
  }) : super.dirty();

  @override
  final FormzAsyncValidationStatus asyncStatus;

  @override
  final PhoneNumberError? asyncError;

  // The synchronous validator still runs and takes precedence.
  @override
  PhoneNumberError? validator(String value) {
    return value.isEmpty ? PhoneNumberError.empty : null;
  }
}

The input is only valid once asyncStatus is completed and neither validation returned an error. A typical flow in a Bloc looks like this:

Future<void> _onPhoneNumberChanged(
  PhoneNumberChanged event,
  Emitter<MyState> emit,
) async {
  final value = event.value;
  emit(
    state.copyWith(
      phoneNumber: PhoneNumber.dirty(
        value,
        asyncStatus: FormzAsyncValidationStatus.inProgress,
      ),
    ),
  );

  final isValid = await _phoneNumberValidator.isValid(value);

  // Ignore the result if the value changed while validating.
  if (state.phoneNumber.value != value) return;

  emit(
    state.copyWith(
      phoneNumber: PhoneNumber.dirty(
        value,
        asyncStatus: FormzAsyncValidationStatus.completed,
        asyncError: isValid ? null : PhoneNumberError.invalid,
      ),
    ),
  );
}

When combining it with FormzInputErrorCacheMixin, apply FormzInputAsyncValidationMixin last.

About

A unified form representation in Dart used at Very Good Ventures πŸ¦„

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages