/fpformz

Functional input validation library based on Fpdart, which is inspired by Formz.

Primary LanguageDartMIT LicenseMIT

License: MIT ci pub package

FPFormz

Functional input validation library based on Fpdart, which is inspired by Formz.

Features

For the most part, FPFormz is similar to the original Formz library, with a few notable differences:

  • FPFormz allows specifying different types for input and validated values, which can be convenient when using non-string type values (e.g. int, enum, value class, etc.).

  • It exposes validated values and errors as functional constructs such as Either or Option, making it easier to manipulate them declaratively.

  • It also provides a way to write validation logic as mixins, which you can combine to handle more complex use cases.

Installation

You can install PFFormz by adding the following entry in your pubsec.yaml:

# pubspec.yaml
dependencies:
  fpformz: ^0.1.3

Getting Started

FormInput And Its Derivatives

To define a validatable input, you need to write a class that extends FormInput<V, I, E> whose generic parameters correspond to the type of resulting value, input value, and potential errors, respectively:

class AgeInput extends FormInput<int, String, ValidationError> {
  const AgeInput.pristine(name, value) : super.pristine(name, value);

  const AgeInput.dirty(name, value) : super.dirty(name, value);

  Either<ValidationError, int> validate(String value) =>
      Either.tryCatch(() => int.parse(value),
              (e, s) => ValidationError(name, '$name should be a number.'));
}

After declaring your input, you can use either pristine or dirty constructor to create an instance:

void example() {
  // To create an unmodified ('pristine') input instance:
  final age = AgeInput.pristine('age', '');

  // Or you can create a modified ('dirty') input instance as below:
  final editedAge = AgeInput.dirty('age', '23');

  print(age.isPristine); // returns 'true'
  print(editedAge.isPristine); // returns 'false'
}

You can access validation information either as a functional construct, or as a nullable:

void example() {
  print(editedAge.isValid); // returns true
  print(editedAge.result); // returns Right(23)
  print(editedAge.resultOrNull); // returns 23
  print(editedAge.error); // returns None
  print(editedAge.errorOrNull); // returns null

  print(age.isValid); // returns false
  print(age.result); // returns Left(ValidationError)
  print(age.resultOrNull); // returns null
  print(age.error); // returns Some(ValidationError)
  print(age.errorOrNull); // returns ValidationError
}

And because most input components treat the user input as a String instance, you can simplify the type signature by extending from StringFormInput:

class NameInput extends StringFormInput<String, ValidationError> {
  const NameInput.pristine(name, value) : super.pristine(name, value);

  const NameInput.dirty(name, value) : super.dirty(name, value);

  @override
  String convert(String value) => value;

  @override
  Either<ValidationError, String> validate(String value) =>
      value.isEmpty
          ? Either.left(ValidationError(name, 'The name cannot be empty.'))
          : super.validate(value);
}

Form

Like with Formz, you can create a form class to host multiple input fields and validate them together:

class RegistrationForm extends Form {

  final NameInput name;
  final EmailInput email;

  const RegistrationForm({
    this.name = const NameInput.pristine('name', ''),
    this.email = const EmailInput.pristine('email', '')
  });

  @override
  get inputs => [name, email];
}

Then you can validate it using a similar API like that of FormInput:

void example() {
  final form = RegistrationForm();

  print(form.isPristine); // returns true
  print(form.isValid); // returns false

  print(form.result); // it 'short circuits' at the first error encountered
  print(form.errors); // but you can get all errors this way. 
}

Form.result returns a map of all validated input values which you can use to invoke a service method:

void example() {
  final form = RegistrationForm();

  final params = form.resultOrNull!;

  service.register(params[form.email.name], params[form.password.name]);

  // Or even like this, provided that the input names match those of the parameters:  
  Function.apply(service.register, [], params);
}

Mixins

You can also write reusable validation logic as a mixin:

@immutable
mixin NonEmptyString<V> on FormInput<V, String, ValidationError> {
  ValidationError get whenEmpty => ValidationError(name, 'Please enter $name.');

  @override
  Either<ValidationError, V> validate(String value) =>
      value.isEmpty ? Either.left(whenEmpty) : super.validate(value);
}

And build a concrete input field by adding them to either BaseFormInput or StringFormInput as shown below:

class EmailInput extends StringFormInput<Email, ValidationError>
    with EmailString, NonEmptyString {
  const EmailInput.pristine(name, value) : super.pristine(name, value);

  const EmailInput.dirty(name, value) : super.dirty(name, value);

  @override
  Email convert(String value) => Email.parse(value);
}

It's recommended to split each validation logic into a separate mixin rather than putting all into an input class to maximise code reuse and achieve separation of concerns (i.e. the 'S' in SOLID principles).

FPFormz also ships with a small collection of ready-to-use mixins like NonEmptyString , StringShorterThan, which might be expanded in future versions.

Additional Information

You can find more code examples in our test cases.