diff --git a/lib/formz.dart b/lib/formz.dart index 2639574..5c5b7bb 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -157,6 +157,14 @@ class Formz { static bool isPure(List> inputs) { return inputs.every((input) => input.isPure); } + + /// Returns a [Set] of invalid [FormzInput] given a list of [FormzInput] + /// to be validated + static Set> validateGranularly( + List> inputs, + ) { + return inputs.where((input) => input.isNotValid).toSet(); + } } /// Mixin that automatically handles validation of all [FormzInput]s present in @@ -192,6 +200,10 @@ mixin FormzMixin { /// Whether at least one of the [FormzInput]s is dirty. bool get isDirty => !isPure; + /// Returns a [Set] of invalid [FormzInput] by validating the [inputs] + Set> get invalidInputs => + Formz.validateGranularly(inputs); + /// Returns all [FormzInput] instances. /// /// Override this and give it all [FormzInput]s in your class that should be diff --git a/test/formz_test.dart b/test/formz_test.dart index 442fb8a..05a80af 100644 --- a/test/formz_test.dart +++ b/test/formz_test.dart @@ -1,5 +1,6 @@ // Not needed for test files // ignore_for_file: prefer_const_constructors + import 'package:formz/formz.dart'; import 'package:test/test.dart'; @@ -39,6 +40,53 @@ void main() { expect(form.isDirty, isTrue); expect(form.isPure, isFalse); }); + + test('for a form with 2 invalid inputs, returns 2 invalid inputs', () { + final invalidName = NameInput.dirty(); + final invalidPassword = PasswordInput.dirty(); + final form = NamePasswordInputFormzMixin( + name: invalidName, + password: invalidPassword, + ); + final invalidInputs = form.invalidInputs; + + expect(invalidInputs.length, 2); + expect(invalidInputs.contains(invalidName), isTrue); + expect(invalidInputs.contains(invalidPassword), isTrue); + }); + + test( + 'for a form with 1 invalid and 1 valid input, returns 1 invalid input', + () { + final validName = NameInput.dirty(value: 'Name'); + final invalidPassword = PasswordInput.dirty(); + final form = NamePasswordInputFormzMixin( + name: validName, + password: invalidPassword, + ); + final invalidInputs = form.invalidInputs; + + expect(invalidInputs.length, 1); + expect(invalidInputs.contains(invalidPassword), isTrue); + }, + ); + + test( + 'form with only valid inputs, returns empty set as invalid inputs', + () { + final validName = NameInput.dirty(value: 'Name'); + final validPassword = PasswordInput.dirty( + value: 'VeryGoodPassword42', + ); + final form = NamePasswordInputFormzMixin( + name: validName, + password: validPassword, + ); + final invalidInputs = form.invalidInputs; + + expect(invalidInputs.length, 0); + }, + ); }); group('FormzInputErrorCacheMixin', () { @@ -246,6 +294,65 @@ void main() { }); }); + group('validateGranularly', () { + test('returns empty set for empty inputs', () { + expect(Formz.validateGranularly([]), >{}); + }); + + test('returns empty set for valid pure input', () { + expect( + Formz.validateGranularly([NameInput.pure(value: 'joe')]), + >{}, + ); + }); + + test('returns empty set for valid dirty input', () { + expect( + Formz.validateGranularly([NameInput.dirty(value: 'joe')]), + >{}, + ); + }); + + test('returns empty set for multiple valid pure/dirty input', () { + expect( + Formz.validateGranularly([ + NameInput.dirty(value: 'jen'), + NameInput.pure(value: 'bob'), + NameInput.dirty(value: 'alex'), + ]), + >{}, + ); + }); + + test( + 'returns one invalid input when dirty invalid input is provided', + () { + final invalidName = NameInput.dirty(); + expect(Formz.validateGranularly([invalidName]), {invalidName}); + }, + ); + + test('returns one invalid input when pure invalid input is provided', () { + final invalidName = NameInput.pure(); + expect(Formz.validateGranularly([invalidName]), {invalidName}); + }); + + test('returns only invalid inputs for multiple valid/invalid inputs', () { + final invalidNameOne = NameInput.dirty(); + final invalidNameTwo = NameInput.pure(); + final validName = NameInput.dirty(value: 'Joe'); + final result = Formz.validateGranularly([ + invalidNameOne, + validName, + invalidNameTwo, + ]); + expect(result.length, 2); + expect(result.contains(invalidNameOne), isTrue); + expect(result.contains(invalidNameTwo), isTrue); + expect(result.contains(validName), isFalse); + }); + }); + group('FormzSubmissionStatusX', () { test('isInitial returns true', () { expect(FormzSubmissionStatus.initial.isInitial, isTrue); diff --git a/test/helpers/name_input.dart b/test/helpers/name_input.dart index 35990ad..ea91e01 100644 --- a/test/helpers/name_input.dart +++ b/test/helpers/name_input.dart @@ -12,6 +12,28 @@ class NameInput extends FormzInput { } } +enum PasswordValidationError { invalid, empty } + +class PasswordInput extends FormzInput { + const PasswordInput.pure({String value = ''}) : super.pure(value); + const PasswordInput.dirty({String value = ''}) : super.dirty(value); + + static final _passwordRegex = RegExp( + r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$', + ); + + @override + PasswordValidationError? validator(String value) { + if (value.isEmpty) { + return PasswordValidationError.empty; + } else if (!_passwordRegex.hasMatch(value)) { + return PasswordValidationError.invalid; + } + + return null; + } +} + class NameInputFormzMixin with FormzMixin { NameInputFormzMixin({this.name = const NameInput.pure()}); @@ -21,6 +43,19 @@ class NameInputFormzMixin with FormzMixin { List> get inputs => [name]; } +class NamePasswordInputFormzMixin with FormzMixin { + NamePasswordInputFormzMixin({ + this.name = const NameInput.pure(), + this.password = const PasswordInput.pure(), + }); + + final NameInput name; + final PasswordInput password; + + @override + List> get inputs => [name, password]; +} + // Test fixture so allowable // ignore: must_be_immutable class NameInputErrorCacheMixin extends FormzInput