Functional input validation library based on Fpdart, which is inspired by Formz
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
orOption
, 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.0
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.
}
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.