diff --git a/README.md b/README.md deleted file mode 100644 index ade77f9..0000000 --- a/README.md +++ /dev/null @@ -1,216 +0,0 @@ - - netglade - - -Developed with πŸ’š by [netglade][netglade_link] - -[![ci][ci_badge]][ci_badge_link] -[![glade_forms][glade_forms_pub_badge]][glade_forms_pub_badge_link] -[![license: MIT][license_badge]][license_badge_link] -[![style: netglade analysis][style_badge]][style_badge_link] -[![Discord][discord_badge]][discord_badge_link] - ---- - -A universal way to define form validators with support of translations. - -- [πŸ‘€ What is this?](#-what-is-this) -- [πŸš€ Getting started](#-getting-started) - - [GladeInput](#gladeinput) - - [Defining input](#defining-input) - - [StringToValueConverter (valueConverter)](#stringtovalueconverter-valueconverter) - - [StringInput](#stringinput) - - [Dependencies](#dependencies) - - [πŸ“š Adding translation support](#-adding-translation-support) - - [GladeModel](#glademodel) - - [GladeFormBuilder and GladeFormProvider](#gladeformbuilder-and-gladeformprovider) - - [πŸ”¨ Debugging validators](#-debugging-validators) -- [πŸ‘ Contributing](#-contributing) - -## πŸ‘€ What is this? - -Glade forms offer unified way to define reusable form input -with support of fluent API to define input's validators and with support of translation on top of that. - -**TBA DEMO SITE** - - -## πŸš€ Getting started - -Define you model and inputs: - -```dart -class _Model extends GladeModel { - late StringInput name; - late GladeInput age; - late StringInput email; - - @override - List> get inputs => [name, age, email]; - - _Model() { - name = StringInput.required(); - age = GladeInput.intInput(value: 0); - email = StringInput.create((validator) => (validator..isEmail()).build()); - } -} - -``` - -and wire-it up with Form - -```dart -GladeFormBuilder( - create: (context) => _Model(), - builder: (context, model) => Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - children: [ - TextFormField( - initialValue: model.name.value, - validator: model.name.formFieldInputValidator, - onChanged: (v) => model.stringFieldUpdateInput(model.name, v), - decoration: const InputDecoration(labelText: 'Name'), - ), - TextFormField( - initialValue: model.age.stringValue, - validator: model.age.formFieldInputValidator, - onChanged: (v) => model.stringFieldUpdateInput(model.age, v), - decoration: const InputDecoration(labelText: 'Age'), - ), - TextFormField( - initialValue: model.email.value, - validator: model.email.formFieldInputValidator, - onChanged: (v) => model.stringFieldUpdateInput(model.email, v), - decoration: const InputDecoration(labelText: 'Email'), - ), - const SizedBox(height: 10), - ElevatedButton(onPressed: model.isValid ? () {} : null, child: const Text('Save')), - ], - ), - ), -) -``` - -See DEMO site for more, complex, examples. - -### GladeInput -Each form's input is represented by instance of `GladeInput` where `T` is value held by input. -For simplicity we will interchange `input` and `GladeInput`. - -Every input is *dirty* or *pure* based on if value was updated (or not, yet). - -On each input we can defined - - *validator* - Input's value must satistfy validation to be *valid* input. - - *translateError* - If there are validation errors, function for error translations can be provided. - - *inputKey* - For debug purposes and dependencies, each input can have unique name for simple identification. - - *dependencies* - Each input can depend on another inputs for validation. - - *valueConverter* - If input is used by TextField and `T` is not a `String`, value converter should be provided. - - *valueComparator* - Sometimes it is handy to provied `initialValue` which will be never updated after input is mutated. `valueComparator` should be provided to compare `initialValue` and `value` if `T` is not comparable type by default. - - *defaultTranslation* - If error's translations are simple, the default translation settings can be set instead of custom `translateError` method. - -#### Defining input -Most of the time, input is created with `.create()` factory with defined validation, translation and other properties. - -Validation is defined through part methods on ValidatorFactory such as `notNull()`, `satisfy()` and other parts. - -Each validation rule defines - - *value validation*, e.g `notNull()` defines that value can not be null. `satisfy()` defines predicate which has to be true to be valid etc. - - **devErrorMessage** - message which will be displayed if no translation is not provided. - - **key** - Validation error's identification. Usable for translation. - - -This example defines validation that `int` value has to be greater or equal to 18. - -```dart - ageInput = GladeInput.create( - validator: (v) => (v - ..notNull() - ..satisfy( - (value, extra, dependencies) { - return value >= 18; - }, - devError: (_, __) => 'Value must be greater or equal to 18', - key: _ErrorKeys.ageRestriction, - )) - .build(), - value: 0, - valueConverter: GladeTypeConverters.intConverter, - ); -``` - -Order of validation parts matter. By default first failing part stops validation. Pass `stopOnFirstError: false` on `.build()` to validate all parts at once. - -#### StringToValueConverter (valueConverter) -As noted before, if `T` is not a String, a converter from String to `T` has to be provided. - -GladeForms provides some predefined converters such as `IntConverter` and more. See `GladeTypeConverters` for more. - - -#### StringInput -StringInput is specialized variant of GladeInput which has aditional, string focused, validations such as `isEmail` or `isUrl`. - -#### Dependencies -Input can have dependencies on another inputs to allow dependendent validation. -`inputKey` should be assigned for each input to allow dependency work. - -In validation (or translation if needed) just call `dependencies.byKey()` to get dependendent input. - - -### πŸ“š Adding translation support - -Each validation error (and conversion error if any) can be translated. Provide `translateError` fucntion which accepts - -- `error` - Error to translate -- `key` - Error's identification if any -- `devMessage` - Provided `devError` from validator -- `dependencies` - Input's dependencies - -Age example translation -```dart - -translateError: (error, key, devMessage, {required dependencies}) { - if (key == _ErrorKeys.ageRestriction) return LocaleKeys.ageRestriction_under18.tr(); - - if (error.isConversionError) return LocaleKeys.ageRestriction_ageFormat.tr(); - - return devMessage; -}, - -``` - -### GladeModel -GladeModel is base class for Form's model which holds all inputs together. - -For updating concrete input, call `updateInput` or `stringFieldUpdateInput` methods to update its value. GladeModel is ChangeNotifier so all dependant widgets will be rebuilt. - -### GladeFormBuilder and GladeFormProvider -GladeFormProvider is predefined widget to provide GladeFormModel to widget's subtreee. - -Similarly GladeFormBuilder allows to listen to Model's changes and rebuilts its child. - -### πŸ”¨ Debugging validators - -There are some getter and methods on GladeInput / GladeModel which can be used for debugging. - -Use `model.formattedValidationErrors` to get all input's error formatted for simple debugging. - -There is also `GladeModelDebugInfo` widget which displays table of all model's inputs and their properties such as `isValid` or `validation error`. - - - -## πŸ‘ Contributing - -Your contributions are always welcome! Feel free to open pull request. - -[netglade_link]: https://netglade.com/en -[ci_badge]: https://github.com/netglade/glade_forms/actions/workflows/ci.yaml/badge.svg -[ci_badge_link]: https://github.com/netglade/glade_forms/actions -[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg -[license_badge_link]: https://opensource.org/licenses/MIT -[style_badge]: https://img.shields.io/badge/style-netglade_analysis-26D07C.svg -[style_badge_link]: https://pub.dev/packages/netglade_analysis -[glade_forms_pub_badge]: https://img.shields.io/pub/v/glade_forms.svg -[glade_forms_pub_badge_link]: https://pub.dartlang.org/packages/glade_forms -[discord_badge]: https://img.shields.io/discord/1091460081054400532.svg?logo=discord&color=blue -[discord_badge_link]: https://discord.gg/sJfBBuDZy4 \ No newline at end of file diff --git a/README.md b/README.md new file mode 120000 index 0000000..70e7736 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +glade_forms/README.md \ No newline at end of file diff --git a/examples/gallery/README.md b/examples/gallery/README.md index 2b3fce4..ade77f9 100644 --- a/examples/gallery/README.md +++ b/examples/gallery/README.md @@ -1,16 +1,216 @@ -# example + + netglade + -A new Flutter project. +Developed with πŸ’š by [netglade][netglade_link] -## Getting Started +[![ci][ci_badge]][ci_badge_link] +[![glade_forms][glade_forms_pub_badge]][glade_forms_pub_badge_link] +[![license: MIT][license_badge]][license_badge_link] +[![style: netglade analysis][style_badge]][style_badge_link] +[![Discord][discord_badge]][discord_badge_link] -This project is a starting point for a Flutter application. +--- -A few resources to get you started if this is your first Flutter project: +A universal way to define form validators with support of translations. -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +- [πŸ‘€ What is this?](#-what-is-this) +- [πŸš€ Getting started](#-getting-started) + - [GladeInput](#gladeinput) + - [Defining input](#defining-input) + - [StringToValueConverter (valueConverter)](#stringtovalueconverter-valueconverter) + - [StringInput](#stringinput) + - [Dependencies](#dependencies) + - [πŸ“š Adding translation support](#-adding-translation-support) + - [GladeModel](#glademodel) + - [GladeFormBuilder and GladeFormProvider](#gladeformbuilder-and-gladeformprovider) + - [πŸ”¨ Debugging validators](#-debugging-validators) +- [πŸ‘ Contributing](#-contributing) -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +## πŸ‘€ What is this? + +Glade forms offer unified way to define reusable form input +with support of fluent API to define input's validators and with support of translation on top of that. + +**TBA DEMO SITE** + + +## πŸš€ Getting started + +Define you model and inputs: + +```dart +class _Model extends GladeModel { + late StringInput name; + late GladeInput age; + late StringInput email; + + @override + List> get inputs => [name, age, email]; + + _Model() { + name = StringInput.required(); + age = GladeInput.intInput(value: 0); + email = StringInput.create((validator) => (validator..isEmail()).build()); + } +} + +``` + +and wire-it up with Form + +```dart +GladeFormBuilder( + create: (context) => _Model(), + builder: (context, model) => Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + initialValue: model.name.value, + validator: model.name.formFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.name, v), + decoration: const InputDecoration(labelText: 'Name'), + ), + TextFormField( + initialValue: model.age.stringValue, + validator: model.age.formFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.age, v), + decoration: const InputDecoration(labelText: 'Age'), + ), + TextFormField( + initialValue: model.email.value, + validator: model.email.formFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.email, v), + decoration: const InputDecoration(labelText: 'Email'), + ), + const SizedBox(height: 10), + ElevatedButton(onPressed: model.isValid ? () {} : null, child: const Text('Save')), + ], + ), + ), +) +``` + +See DEMO site for more, complex, examples. + +### GladeInput +Each form's input is represented by instance of `GladeInput` where `T` is value held by input. +For simplicity we will interchange `input` and `GladeInput`. + +Every input is *dirty* or *pure* based on if value was updated (or not, yet). + +On each input we can defined + - *validator* - Input's value must satistfy validation to be *valid* input. + - *translateError* - If there are validation errors, function for error translations can be provided. + - *inputKey* - For debug purposes and dependencies, each input can have unique name for simple identification. + - *dependencies* - Each input can depend on another inputs for validation. + - *valueConverter* - If input is used by TextField and `T` is not a `String`, value converter should be provided. + - *valueComparator* - Sometimes it is handy to provied `initialValue` which will be never updated after input is mutated. `valueComparator` should be provided to compare `initialValue` and `value` if `T` is not comparable type by default. + - *defaultTranslation* - If error's translations are simple, the default translation settings can be set instead of custom `translateError` method. + +#### Defining input +Most of the time, input is created with `.create()` factory with defined validation, translation and other properties. + +Validation is defined through part methods on ValidatorFactory such as `notNull()`, `satisfy()` and other parts. + +Each validation rule defines + - *value validation*, e.g `notNull()` defines that value can not be null. `satisfy()` defines predicate which has to be true to be valid etc. + - **devErrorMessage** - message which will be displayed if no translation is not provided. + - **key** - Validation error's identification. Usable for translation. + + +This example defines validation that `int` value has to be greater or equal to 18. + +```dart + ageInput = GladeInput.create( + validator: (v) => (v + ..notNull() + ..satisfy( + (value, extra, dependencies) { + return value >= 18; + }, + devError: (_, __) => 'Value must be greater or equal to 18', + key: _ErrorKeys.ageRestriction, + )) + .build(), + value: 0, + valueConverter: GladeTypeConverters.intConverter, + ); +``` + +Order of validation parts matter. By default first failing part stops validation. Pass `stopOnFirstError: false` on `.build()` to validate all parts at once. + +#### StringToValueConverter (valueConverter) +As noted before, if `T` is not a String, a converter from String to `T` has to be provided. + +GladeForms provides some predefined converters such as `IntConverter` and more. See `GladeTypeConverters` for more. + + +#### StringInput +StringInput is specialized variant of GladeInput which has aditional, string focused, validations such as `isEmail` or `isUrl`. + +#### Dependencies +Input can have dependencies on another inputs to allow dependendent validation. +`inputKey` should be assigned for each input to allow dependency work. + +In validation (or translation if needed) just call `dependencies.byKey()` to get dependendent input. + + +### πŸ“š Adding translation support + +Each validation error (and conversion error if any) can be translated. Provide `translateError` fucntion which accepts + +- `error` - Error to translate +- `key` - Error's identification if any +- `devMessage` - Provided `devError` from validator +- `dependencies` - Input's dependencies + +Age example translation +```dart + +translateError: (error, key, devMessage, {required dependencies}) { + if (key == _ErrorKeys.ageRestriction) return LocaleKeys.ageRestriction_under18.tr(); + + if (error.isConversionError) return LocaleKeys.ageRestriction_ageFormat.tr(); + + return devMessage; +}, + +``` + +### GladeModel +GladeModel is base class for Form's model which holds all inputs together. + +For updating concrete input, call `updateInput` or `stringFieldUpdateInput` methods to update its value. GladeModel is ChangeNotifier so all dependant widgets will be rebuilt. + +### GladeFormBuilder and GladeFormProvider +GladeFormProvider is predefined widget to provide GladeFormModel to widget's subtreee. + +Similarly GladeFormBuilder allows to listen to Model's changes and rebuilts its child. + +### πŸ”¨ Debugging validators + +There are some getter and methods on GladeInput / GladeModel which can be used for debugging. + +Use `model.formattedValidationErrors` to get all input's error formatted for simple debugging. + +There is also `GladeModelDebugInfo` widget which displays table of all model's inputs and their properties such as `isValid` or `validation error`. + + + +## πŸ‘ Contributing + +Your contributions are always welcome! Feel free to open pull request. + +[netglade_link]: https://netglade.com/en +[ci_badge]: https://github.com/netglade/glade_forms/actions/workflows/ci.yaml/badge.svg +[ci_badge_link]: https://github.com/netglade/glade_forms/actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_badge_link]: https://opensource.org/licenses/MIT +[style_badge]: https://img.shields.io/badge/style-netglade_analysis-26D07C.svg +[style_badge_link]: https://pub.dev/packages/netglade_analysis +[glade_forms_pub_badge]: https://img.shields.io/pub/v/glade_forms.svg +[glade_forms_pub_badge_link]: https://pub.dartlang.org/packages/glade_forms +[discord_badge]: https://img.shields.io/discord/1091460081054400532.svg?logo=discord&color=blue +[discord_badge_link]: https://discord.gg/sJfBBuDZy4 \ No newline at end of file diff --git a/examples/gallery/lib/main.dart b/examples/gallery/lib/main.dart index e43807c..6dab352 100644 --- a/examples/gallery/lib/main.dart +++ b/examples/gallery/lib/main.dart @@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:glade_forms_example/generated/locale_loader.g.dart'; -import 'package:glade_forms_example/localization_addon_custom.dart'; -import 'package:glade_forms_example/usecases/age_restricted_example.dart'; -import 'package:glade_forms_example/usecases/complex_object_mapping_example.dart'; -import 'package:glade_forms_example/usecases/quickstart_example.dart'; +import 'package:glade_forms_gallery/generated/locale_loader.g.dart'; +import 'package:glade_forms_gallery/localization_addon_custom.dart'; +import 'package:glade_forms_gallery/usecases/age_restricted_example.dart'; +import 'package:glade_forms_gallery/usecases/complex_object_mapping_example.dart'; +import 'package:glade_forms_gallery/usecases/quickstart_example.dart'; import 'package:widgetbook/widgetbook.dart'; // ignore: prefer-static-class, ok for now diff --git a/examples/gallery/lib/usecases/age_restricted_example.dart b/examples/gallery/lib/usecases/age_restricted_example.dart index 7da320c..5a72897 100644 --- a/examples/gallery/lib/usecases/age_restricted_example.dart +++ b/examples/gallery/lib/usecases/age_restricted_example.dart @@ -3,8 +3,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:glade_forms/glade_forms.dart'; -import 'package:glade_forms_example/generated/locale_keys.g.dart'; -import 'package:glade_forms_example/shared/usecase_container.dart'; +import 'package:glade_forms_gallery/generated/locale_keys.g.dart'; +import 'package:glade_forms_gallery/shared/usecase_container.dart'; class _ErrorKeys { static const String ageRestriction = 'age-restriction'; diff --git a/examples/gallery/lib/usecases/complex_object_mapping_example.dart b/examples/gallery/lib/usecases/complex_object_mapping_example.dart index a83486b..d5b7c64 100644 --- a/examples/gallery/lib/usecases/complex_object_mapping_example.dart +++ b/examples/gallery/lib/usecases/complex_object_mapping_example.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:glade_forms/glade_forms.dart'; -import 'package:glade_forms_example/shared/usecase_container.dart'; +import 'package:glade_forms_gallery/shared/usecase_container.dart'; class _Item { final int id; diff --git a/examples/gallery/lib/usecases/quickstart_example.dart b/examples/gallery/lib/usecases/quickstart_example.dart index 0eade64..c319a51 100644 --- a/examples/gallery/lib/usecases/quickstart_example.dart +++ b/examples/gallery/lib/usecases/quickstart_example.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:glade_forms/glade_forms.dart'; -import 'package:glade_forms_example/shared/usecase_container.dart'; +import 'package:glade_forms_gallery/shared/usecase_container.dart'; class _Model extends GladeModel { late StringInput name; diff --git a/examples/gallery/pubspec.yaml b/examples/gallery/pubspec.yaml index c20f27f..50d9064 100644 --- a/examples/gallery/pubspec.yaml +++ b/examples/gallery/pubspec.yaml @@ -1,6 +1,6 @@ -name: glade_forms_example +name: glade_forms_gallery description: Glade Forms - Interactive example -version: 0.0.1 +version: 1.0.0 publish_to: none environment: @@ -13,8 +13,7 @@ dependencies: flutter_highlighter: ^0.1.1 flutter_hooks: ^0.20.1 flutter_markdown: ^0.6.17+3 - glade_forms: - path: ../../glade_forms + glade_forms: ^1.0.0 provider: ^6.0.2 widgetbook: ^3.3.0 diff --git a/glade_forms/README.md b/glade_forms/README.md new file mode 100644 index 0000000..bd15722 --- /dev/null +++ b/glade_forms/README.md @@ -0,0 +1,222 @@ + + netglade + + +Developed with πŸ’š by [netglade][netglade_link] + +[![ci][ci_badge]][ci_badge_link] +[![glade_forms][glade_forms_pub_badge]][glade_forms_pub_badge_link] +[![license: MIT][license_badge]][license_badge_link] +[![style: netglade analysis][style_badge]][style_badge_link] +[![Discord][discord_badge]][discord_badge_link] + +--- + +A universal way to define form validators with support of translations. + +- [πŸ‘€ What is this?](#-what-is-this) +- [πŸš€ Getting started](#-getting-started) + - [GladeInput](#gladeinput) + - [Defining input](#defining-input) + - [StringToValueConverter (valueConverter)](#stringtovalueconverter-valueconverter) + - [StringInput](#stringinput) + - [Dependencies](#dependencies) + - [πŸ“š Adding translation support](#-adding-translation-support) + - [GladeModel](#glademodel) + - [GladeFormBuilder and GladeFormProvider](#gladeformbuilder-and-gladeformprovider) + - [πŸ”¨ Debugging validators](#-debugging-validators) +- [πŸ‘ Contributing](#-contributing) + +## πŸ‘€ What is this? + +Glade forms offer unified way to define reusable form input +with support of fluent API to define input's validators and with support of translation on top of that. + +**TBA DEMO SITE** + + +## πŸš€ Getting started + +Define you model and inputs: + +```dart +class _Model extends GladeModel { + late StringInput name; + late GladeInput age; + late StringInput email; + + @override + List> get inputs => [name, age, email]; + + _Model() { + name = StringInput.required(); + age = GladeInput.intInput(value: 0); + email = StringInput.create((validator) => (validator..isEmail()).build()); + } +} + +``` + +and wire-it up with Form + +```dart +GladeFormBuilder( + create: (context) => _Model(), + builder: (context, model) => Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + initialValue: model.name.value, + validator: model.name.formFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.name, v), + decoration: const InputDecoration(labelText: 'Name'), + ), + TextFormField( + initialValue: model.age.stringValue, + validator: model.age.formFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.age, v), + decoration: const InputDecoration(labelText: 'Age'), + ), + TextFormField( + initialValue: model.email.value, + validator: model.email.formFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.email, v), + decoration: const InputDecoration(labelText: 'Email'), + ), + const SizedBox(height: 10), + ElevatedButton(onPressed: model.isValid ? () {} : null, child: const Text('Save')), + ], + ), + ), +) +``` + +See DEMO site for more, complex, examples. + +### GladeInput +Each form's input is represented by instance of `GladeInput` where `T` is value held by input. +For simplicity we will interchange `input` and `GladeInput`. + +Every input is *dirty* or *pure* based on if value was updated (or not, yet). + +On each input we can defined + - *validator* - Input's value must satistfy validation to be *valid* input. + - *translateError* - If there are validation errors, function for error translations can be provided. + - *inputKey* - For debug purposes and dependencies, each input can have unique name for simple identification. + - *dependencies* - Each input can depend on another inputs for validation. + - *valueConverter* - If input is used by TextField and `T` is not a `String`, value converter should be provided. + - *valueComparator* - Sometimes it is handy to provied `initialValue` which will be never updated after input is mutated. `valueComparator` should be provided to compare `initialValue` and `value` if `T` is not comparable type by default. + - *defaultTranslation* - If error's translations are simple, the default translation settings can be set instead of custom `translateError` method. + +#### Defining input +Most of the time, input is created with `.create()` factory with defined validation, translation and other properties. + +Validation is defined through part methods on ValidatorFactory such as `notNull()`, `satisfy()` and other parts. + +Each validation rule defines + - *value validation*, e.g `notNull()` defines that value can not be null. `satisfy()` defines predicate which has to be true to be valid etc. + - **devErrorMessage** - message which will be displayed if no translation is not provided. + - **key** - Validation error's identification. Usable for translation. + + +This example defines validation that `int` value has to be greater or equal to 18. + +```dart + ageInput = GladeInput.create( + validator: (v) => (v + ..notNull() + ..satisfy( + (value, extra, dependencies) { + return value >= 18; + }, + devError: (_, __) => 'Value must be greater or equal to 18', + key: _ErrorKeys.ageRestriction, + )) + .build(), + value: 0, + valueConverter: GladeTypeConverters.intConverter, + ); +``` + +Order of validation parts matter. By default first failing part stops validation. Pass `stopOnFirstError: false` on `.build()` to validate all parts at once. + +#### StringToValueConverter (valueConverter) +As noted before, if `T` is not a String, a converter from String to `T` has to be provided. + +GladeForms provides some predefined converters such as `IntConverter` and more. See `GladeTypeConverters` for more. + + +#### StringInput +StringInput is specialized variant of GladeInput which has additional, string related, validations such as `isEmail`, `isUrl`, `maxLength` and more. + +#### Dependencies +Input can have dependencies on another inputs to allow dependendent validation. +`inputKey` should be assigned for each input to allow dependency work. + +In validation (or translation if needed) just call `dependencies.byKey()` to get dependendent input. + + +### πŸ“š Adding translation support + +Each validation error (and conversion error if any) can be translated. Provide `translateError` fucntion which accepts + +- `error` - Error to translate +- `key` - Error's identification if any +- `devMessage` - Provided `devError` from validator +- `dependencies` - Input's dependencies + +Age example translation +```dart + +translateError: (error, key, devMessage, {required dependencies}) { + if (key == _ErrorKeys.ageRestriction) return LocaleKeys.ageRestriction_under18.tr(); + + if (error.isConversionError) return LocaleKeys.ageRestriction_ageFormat.tr(); + + return devMessage; +}, + +``` + +### GladeModel +GladeModel is base class for Form's model which holds all inputs together. + +For updating concrete input, call `updateInput` or `stringFieldUpdateInput` methods to update its value. GladeModel is ChangeNotifier so all dependant widgets will be rebuilt. + +### GladeFormBuilder and GladeFormProvider +GladeFormProvider is predefined widget to provide GladeFormModel to widget's subtreee. + +Similarly GladeFormBuilder allows to listen to Model's changes and rebuilts its child. + +### πŸ”¨ Debugging validators + +There are some getter and methods on GladeInput / GladeModel which can be used for debugging. + +Use `model.formattedValidationErrors` to get all input's error formatted for simple debugging. + +There is also `GladeModelDebugInfo` widget which displays table of all model's inputs and their properties such as `isValid` or `validation error`. + +### Using validators without GladeInput +Is is possible to use GladeValidator without associated GladeInputs. +Just create instance of `` + +```dart + +``` + +## πŸ‘ Contributing + +Your contributions are always welcome! Feel free to open pull request. + +[netglade_link]: https://netglade.com/en +[ci_badge]: https://github.com/netglade/glade_forms/actions/workflows/ci.yaml/badge.svg +[ci_badge_link]: https://github.com/netglade/glade_forms/actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_badge_link]: https://opensource.org/licenses/MIT +[style_badge]: https://img.shields.io/badge/style-netglade_analysis-26D07C.svg +[style_badge_link]: https://pub.dev/packages/netglade_analysis +[glade_forms_pub_badge]: https://img.shields.io/pub/v/glade_forms.svg +[glade_forms_pub_badge_link]: https://pub.dartlang.org/packages/glade_forms +[discord_badge]: https://img.shields.io/discord/1091460081054400532.svg?logo=discord&color=blue +[discord_badge_link]: https://discord.gg/sJfBBuDZy4 \ No newline at end of file diff --git a/glade_forms/example/lib/example.dart b/glade_forms/example/lib/example.dart new file mode 100644 index 0000000..7e2aff4 --- /dev/null +++ b/glade_forms/example/lib/example.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:glade_forms/glade_forms.dart'; + +class _Model extends GladeModel { + late StringInput name; + late GladeInput age; + late StringInput email; + + @override + List> get inputs => [name, age, email]; + + _Model() { + name = StringInput.required(); + age = GladeInput.intInput(value: 0); + email = StringInput.create(validator: (validator) => (validator..isEmail()).build()); + } +} + +class Example extends StatelessWidget { + const Example({super.key}); + + @override + Widget build(BuildContext context) { + return GladeFormBuilder( + create: (context) => _Model(), + builder: (context, model, _) => Padding( + padding: const EdgeInsets.all(32), + child: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + initialValue: model.name.value, + validator: model.name.textFormFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.name, v), + decoration: const InputDecoration(labelText: 'Name'), + ), + TextFormField( + initialValue: model.age.stringValue, + validator: model.age.textFormFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.age, v), + decoration: const InputDecoration(labelText: 'Age'), + ), + TextFormField( + initialValue: model.email.value, + validator: model.email.textFormFieldInputValidator, + onChanged: (v) => model.stringFieldUpdateInput(model.email, v), + decoration: const InputDecoration(labelText: 'Email'), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: model.isValid ? () {} : null, + child: const Text('Save'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/glade_forms/example/pubspec.yaml b/glade_forms/example/pubspec.yaml new file mode 100644 index 0000000..05a73b0 --- /dev/null +++ b/glade_forms/example/pubspec.yaml @@ -0,0 +1,10 @@ +name: glade_forms_example +description: Example project how to use GladeForms +version: 1.0.0 +publish_to: none + +environment: + sdk: ">=2.18.7 <4.0.0" + +dependencies: + glade_forms: ^1.0.0 diff --git a/glade_forms/lib/src/core/glade_error_keys.dart b/glade_forms/lib/src/core/glade_error_keys.dart index 4c53ea3..cc8d25f 100644 --- a/glade_forms/lib/src/core/glade_error_keys.dart +++ b/glade_forms/lib/src/core/glade_error_keys.dart @@ -1,7 +1,11 @@ class GladeErrorKeys { + static const String conversionError = 'value-cant-be-converted'; static const String stringEmpty = 'string-empty-error'; static const String stringNotUrl = 'string-not-url'; static const String stringNotEmail = 'string-not-email'; + static const String stringPatternMatch = 'string-pattern-match'; + static const String stringMinLength = 'string-min-length'; + static const String stringMaxLength = 'string-max-length'; + static const String stringExactLength = 'string-exact-length'; static const String valueIsNull = 'value-is-null'; - static const String conversionError = 'value-cant-be-converted'; } diff --git a/glade_forms/lib/src/core/glade_input.dart b/glade_forms/lib/src/core/glade_input.dart index 243f7bc..e1150d2 100644 --- a/glade_forms/lib/src/core/glade_input.dart +++ b/glade_forms/lib/src/core/glade_input.dart @@ -6,14 +6,14 @@ import 'package:glade_forms/src/core/input_dependencies.dart'; import 'package:glade_forms/src/core/string_to_type_converter.dart'; import 'package:glade_forms/src/core/type_helper.dart'; import 'package:glade_forms/src/validator/validator.dart'; +import 'package:glade_forms/src/validator/validator_result.dart'; typedef ValueComparator = bool Function(T? initial, T? value); -typedef ValidatorFactory = ValidatorInstance Function(GenericValidator v); +typedef ValidatorFactory = ValidatorInstance Function(GladeValidator v); class GladeInput extends ChangeNotifier { + /// Compares initial and current value. @protected - - /// Compares. final ValueComparator? valueComparator; @protected @@ -51,7 +51,7 @@ class GladeInput extends ChangeNotifier { /// Input's value was not changed. bool get isPure => _isPure; - ValidatorErrors? get error => _validator(value); + ValidatorResult? get validatorError => _validator(value); /// [value] is equal to [initialValue]. /// @@ -114,7 +114,7 @@ class GladeInput extends ChangeNotifier { valueComparator: valueComparator, stringTovalueConverter: valueConverter, dependenciesFactory: dependencies ?? () => [], - validatorInstance: validatorInstance ?? GenericValidator().build(), + validatorInstance: validatorInstance ?? GladeValidator().build(), translateError: translateError, defaultTranslations: defaultTranslations, ); @@ -137,7 +137,7 @@ class GladeInput extends ChangeNotifier { valueComparator: valueComparator, stringTovalueConverter: valueConverter, dependenciesFactory: dependencies ?? () => [], - validatorInstance: validatorInstance ?? GenericValidator().build(), + validatorInstance: validatorInstance ?? GladeValidator().build(), translateError: translateError, defaultTranslations: defaultTranslations, ); @@ -158,7 +158,7 @@ class GladeInput extends ChangeNotifier { StringToTypeConverter? valueConverter, InputDependenciesFactory? dependencies, }) { - final validatorInstance = validator?.call(GenericValidator()) ?? GenericValidator().build(); + final validatorInstance = validator?.call(GladeValidator()) ?? GladeValidator().build(); return pure ? GladeInput.pure( @@ -210,7 +210,7 @@ class GladeInput extends ChangeNotifier { dependencies: dependencies, ); - // Predefined GenericInput with predefined `notNull` validation. + /// Predefined GenericInput with predefined `notNull` validation. /// /// In case of need of any aditional validation use [GladeInput.create] directly. factory GladeInput.required({ @@ -283,11 +283,12 @@ class GladeInput extends ChangeNotifier { GladeInput asPure(T value) => copyWith(isPure: true, value: value); - ValidatorErrors? validate() => _validator(value); + ValidatorResult? validate() => _validator(value); - String? translate({String delimiter = '.'}) => _translate(delimiter: delimiter, customError: error); + String? translate({String delimiter = '.'}) => _translate(delimiter: delimiter, customError: validatorError); - String errorFormatted({String delimiter = '|'}) => error?.errors.map((e) => e.toString()).join(delimiter) ?? ''; + String errorFormatted({String delimiter = '|'}) => + validatorError?.errors.map((e) => e.toString()).join(delimiter) ?? ''; /// Shorthand validator for TextFieldForm inputs. /// @@ -308,7 +309,7 @@ class GladeInput extends ChangeNotifier { } on ConvertError catch (formatError) { return formatError.error != null ? _translate(delimiter: delimiter, customError: formatError) - : formatError.devError(value, extra: error); + : formatError.devError(value, extra: validatorError); } } @@ -370,11 +371,11 @@ class GladeInput extends ChangeNotifier { /// Translates input's errors (validation or conversion). String? _translate({String delimiter = '.', Object? customError}) { - final err = customError ?? error; + final err = customError ?? validatorError; if (err == null) return null; - if (err is ValidatorErrors) { + if (err is ValidatorResult) { return _translateGenericErrors(err, delimiter); } @@ -396,7 +397,7 @@ class GladeInput extends ChangeNotifier { return err.toString(); } - ValidatorErrors? _validator(T value) { + ValidatorResult? _validator(T value) { final result = validatorInstance.validate(value); if (result.isValid) return null; @@ -404,7 +405,7 @@ class GladeInput extends ChangeNotifier { return result; } - String _translateGenericErrors(ValidatorErrors inputErrors, String delimiter) { + String _translateGenericErrors(ValidatorResult inputErrors, String delimiter) { final translateErrorTmp = translateError; final defaultTranslationsTmp = this.defaultTranslations; diff --git a/glade_forms/lib/src/core/string_to_type_converter.dart b/glade_forms/lib/src/core/string_to_type_converter.dart index 1462b05..7aa66ef 100644 --- a/glade_forms/lib/src/core/string_to_type_converter.dart +++ b/glade_forms/lib/src/core/string_to_type_converter.dart @@ -38,8 +38,9 @@ class StringToTypeConverter { } on ConvertError { // If _cantConvert were used -> we already thrown an Error. rethrow; - // ignore: avoid_catches_without_on_clauses, has to be generic to catch everything - } catch (e) { + } + // ignore: avoid_catches_without_on_clauses, has to be generic to catch everything + catch (e) { // ignore: avoid-throw-in-catch-block, this method should throw custom exception throw onError(input, e); } diff --git a/glade_forms/lib/src/model/glade_form_mixin.dart b/glade_forms/lib/src/model/glade_form_mixin.dart index 4a0d236..423b583 100644 --- a/glade_forms/lib/src/model/glade_form_mixin.dart +++ b/glade_forms/lib/src/model/glade_form_mixin.dart @@ -17,12 +17,12 @@ mixin GladeFormMixin { String get formattedValidationErrors => inputs.map((e) { if (e.hasConversionError) return '${e.inputKey ?? e.runtimeType} - CONVERSION ERROR'; - if (e.error?.errors.isNotEmpty ?? false) { + if (e.validatorError?.errors.isNotEmpty ?? false) { return '${e.inputKey ?? e.runtimeType} - ${e.errorFormatted()}'; } return '${e.inputKey ?? e.runtimeType} - VALID'; }).join('\n'); - List get errors => inputs.map((e) => e.error).toList(); + List get errors => inputs.map((e) => e.validatorError).toList(); } diff --git a/glade_forms/lib/src/model/glade_model.dart b/glade_forms/lib/src/model/glade_model.dart index 7ef076e..9fb6768 100644 --- a/glade_forms/lib/src/model/glade_model.dart +++ b/glade_forms/lib/src/model/glade_model.dart @@ -3,6 +3,7 @@ import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/model/glade_form_mixin.dart'; abstract class GladeModel extends ChangeNotifier with GladeFormMixin { + /// Updates model's input with String? value using its converter. void stringFieldUpdateInput>(INPUT input, String? value) { if (input.value == value) return; @@ -10,6 +11,7 @@ abstract class GladeModel extends ChangeNotifier with GladeFormMixin { notifyListeners(); } + /// Updates model's input value. void updateInput, T>(INPUT input, T value) { if (input.value == value) return; diff --git a/glade_forms/lib/src/validator/generic_validator.dart b/glade_forms/lib/src/validator/glade_validator.dart similarity index 93% rename from glade_forms/lib/src/validator/generic_validator.dart rename to glade_forms/lib/src/validator/glade_validator.dart index e9726ad..3280f5a 100644 --- a/glade_forms/lib/src/validator/generic_validator.dart +++ b/glade_forms/lib/src/validator/glade_validator.dart @@ -1,18 +1,17 @@ import 'package:glade_forms/src/core/core.dart'; -import 'package:glade_forms/src/core/glade_error_keys.dart'; import 'package:glade_forms/src/validator/part/custom_validation_part.dart'; import 'package:glade_forms/src/validator/part/input_validator_part.dart'; import 'package:glade_forms/src/validator/part/satisfy_predicate_part.dart'; import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; import 'package:glade_forms/src/validator/validator_instance.dart'; -typedef ValidateFunction = GenericValidatorError? Function( +typedef ValidateFunction = GladeValidatorError? Function( T value, { required InputDependencies dependencies, Object? extra, }); -class GenericValidator { +class GladeValidator { List> parts = []; ValidatorInstance build({ diff --git a/glade_forms/lib/src/validator/part/custom_validation_part.dart b/glade_forms/lib/src/validator/part/custom_validation_part.dart index b079f53..2361a60 100644 --- a/glade_forms/lib/src/validator/part/custom_validation_part.dart +++ b/glade_forms/lib/src/validator/part/custom_validation_part.dart @@ -1,9 +1,9 @@ import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/validator/part/input_validator_part.dart'; -import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; class CustomValidationPart extends InputValidatorPart { - final GenericValidatorError? Function( + final GladeValidatorError? Function( T value, { required InputDependencies dependencies, Object? extra, @@ -16,7 +16,7 @@ class CustomValidationPart extends InputValidatorPart { }); @override - GenericValidatorError? validate( + GladeValidatorError? validate( T value, { required Object? extra, InputDependencies dependencies = const [], diff --git a/glade_forms/lib/src/validator/part/input_validator_part.dart b/glade_forms/lib/src/validator/part/input_validator_part.dart index f54d788..af7c1ff 100644 --- a/glade_forms/lib/src/validator/part/input_validator_part.dart +++ b/glade_forms/lib/src/validator/part/input_validator_part.dart @@ -1,5 +1,5 @@ import 'package:glade_forms/src/core/core.dart'; -import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; abstract class InputValidatorPart { // ignore: no-object-declaration, key can be any object @@ -9,7 +9,7 @@ abstract class InputValidatorPart { const InputValidatorPart({required this.dependencies, this.key}); - GenericValidatorError? validate( + GladeValidatorError? validate( T value, { required Object? extra, InputDependencies dependencies = const [], diff --git a/glade_forms/lib/src/validator/part/satisfy_predicate_part.dart b/glade_forms/lib/src/validator/part/satisfy_predicate_part.dart index 8c22b81..773f5d3 100644 --- a/glade_forms/lib/src/validator/part/satisfy_predicate_part.dart +++ b/glade_forms/lib/src/validator/part/satisfy_predicate_part.dart @@ -1,5 +1,6 @@ import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/validator/part/input_validator_part.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; typedef SatisfyPredicate = bool Function(T value, Object? extra, InputDependencies dependencies); @@ -21,7 +22,7 @@ class SatisfyPredicatePart extends InputValidatorPart { }); @override - GenericValidatorError? validate( + GladeValidatorError? validate( T value, { required Object? extra, InputDependencies dependencies = const [], diff --git a/glade_forms/lib/src/validator/regex_patterns.dart b/glade_forms/lib/src/validator/regex_patterns.dart index 0ba35b1..1384ba5 100644 --- a/glade_forms/lib/src/validator/regex_patterns.dart +++ b/glade_forms/lib/src/validator/regex_patterns.dart @@ -2,5 +2,5 @@ class RegexPatterns { static const email = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'; static const urlWithOptionalHttp = r'^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&\(\)\*\+,;=.]+$'; static const urlWithHttp = - r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)'; + r'[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)'; } diff --git a/glade_forms/lib/src/validator/string_validator.dart b/glade_forms/lib/src/validator/string_validator.dart index ff736ac..12eda95 100644 --- a/glade_forms/lib/src/validator/string_validator.dart +++ b/glade_forms/lib/src/validator/string_validator.dart @@ -1,9 +1,19 @@ import 'package:glade_forms/src/core/glade_error_keys.dart'; -import 'package:glade_forms/src/validator/generic_validator.dart'; +import 'package:glade_forms/src/validator/glade_validator.dart'; import 'package:glade_forms/src/validator/regex_patterns.dart'; -import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; + +class StringValidator extends GladeValidator { + StringValidator(); + + /// Given value can't be empty string (or null). + void notEmpty({OnValidateError? devError, Object? extra, Object? key}) => satisfy( + (input, extra, __) => input?.isNotEmpty ?? false, + devError: devError ?? (_, __) => "Value can't be empty", + extra: extra, + key: key ?? GladeErrorKeys.stringEmpty, + ); -class StringValidator extends GenericValidator { /// Checks that value is valid email address. /// /// Used Regex expression `^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`. @@ -21,6 +31,7 @@ class StringValidator extends GenericValidator { final regExp = RegExp(RegexPatterns.email); + // ignore: avoid-non-null-assertion, already asserted to non-null return regExp.hasMatch(x!); }, devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" is not in e-mail format', @@ -30,12 +41,12 @@ class StringValidator extends GenericValidator { /// Checks that value is valid URL address. /// - /// [requireHttpScheme] - if true HTTP(S) is mandatory. - void isUrl({ - bool requireHttpScheme = false, + /// [requiresScheme] - if true HTTP(S) is mandatory. + void isUri({ OnValidateError? devError, Object? extra, bool allowEmpty = false, + bool requiresScheme = false, Object? key, }) => satisfy( @@ -44,20 +55,96 @@ class StringValidator extends GenericValidator { return allowEmpty; } - final regExp = RegExp(requireHttpScheme ? RegexPatterns.urlWithHttp : RegexPatterns.urlWithOptionalHttp); + // ignore: avoid-non-null-assertion, already asserted to non-null + final uri = Uri.tryParse(x!); - return regExp.hasMatch(x!); + if (uri == null) return false; + + if (requiresScheme) return uri.hasScheme; + + return true; }, devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" is not valid URL address', extra: extra, key: key ?? GladeErrorKeys.stringNotUrl, ); - /// Given value can't be empty string (or null). - void notEmpty({OnValidateError? devError, Object? extra, Object? key}) => satisfy( - (input, extra, __) => input?.isNotEmpty ?? false, - devError: devError ?? (_, __) => "Value can't be empty", + /// Matches provided regex [pattern]. + void match({ + required String pattern, + bool multiline = false, + bool caseSensitive = true, + bool dotAll = false, + bool unicode = false, + OnValidateError? devError, + Object? extra, + Object? key, + }) => + satisfy( + (value, extra, dependencies) { + if (value == null) return false; + + final regex = + RegExp(pattern, multiLine: multiline, caseSensitive: caseSensitive, dotAll: dotAll, unicode: unicode); + + return regex.hasMatch(value); + }, + devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" does not match regex', extra: extra, - key: key ?? GladeErrorKeys.stringEmpty, + key: key ?? GladeErrorKeys.stringPatternMatch, + ); + + /// String's length has to be greater or equal to provided [length]. + void minLength({ + required int length, + OnValidateError? devError, + Object? extra, + Object? key, + }) => + satisfy( + (value, extra, dependencies) { + if (value == null) return false; + + return value.length >= length; + }, + devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" is shorter than allowed length $length', + extra: extra, + key: key ?? GladeErrorKeys.stringMinLength, + ); + + /// String's length has to be less or equal to provided [length]. + void maxLength({ + required int length, + OnValidateError? devError, + Object? extra, + Object? key, + }) => + satisfy( + (value, extra, dependencies) { + if (value == null) return false; + + return value.length < length; + }, + devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" is longer than allowed length $length', + extra: extra, + key: key ?? GladeErrorKeys.stringMaxLength, + ); + + /// String's length has to be equal to provided [length]. + void exactLength({ + required int length, + OnValidateError? devError, + Object? extra, + Object? key, + }) => + satisfy( + (value, extra, dependencies) { + if (value == null) return false; + + return value.length == length; + }, + devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" has to be $length long characters.', + extra: extra, + key: key ?? GladeErrorKeys.stringExactLength, ); } diff --git a/glade_forms/lib/src/validator/validator.dart b/glade_forms/lib/src/validator/validator.dart index 75d372b..fafb976 100644 --- a/glade_forms/lib/src/validator/validator.dart +++ b/glade_forms/lib/src/validator/validator.dart @@ -1,7 +1,6 @@ -export 'generic_validator.dart'; +export 'glade_validator.dart'; export 'part/part.dart'; export 'regex_patterns.dart'; export 'string_validator.dart'; export 'validator_error/validator_error.dart'; -export 'validator_errors.dart'; export 'validator_instance.dart'; diff --git a/glade_forms/lib/src/validator/validator_error/generic_validator_error.dart b/glade_forms/lib/src/validator/validator_error/glade_validator_error.dart similarity index 85% rename from glade_forms/lib/src/validator/validator_error/generic_validator_error.dart rename to glade_forms/lib/src/validator/validator_error/glade_validator_error.dart index 856ccbf..5f4de9a 100644 --- a/glade_forms/lib/src/validator/validator_error/generic_validator_error.dart +++ b/glade_forms/lib/src/validator/validator_error/glade_validator_error.dart @@ -5,8 +5,7 @@ import 'package:glade_forms/src/validator/validator_error/value_null_error.dart' /// When validation failed but we already have propert [T] value. typedef OnValidateError = String Function(T? value, Object? extra); -// TODO(petr): Rename to Glade?. -abstract class GenericValidatorError extends GladeInputError with EquatableMixin { +abstract class GladeValidatorError extends GladeInputError with EquatableMixin { /// Error message when translation is not used. Useful for development. final OnValidateError devError; @@ -26,7 +25,7 @@ abstract class GenericValidatorError extends GladeInputError with Equatabl List get props => [value, devError, extra, key, error, isConversionError, isNullError, hasStringEmptyOrNullErrorKey]; - GenericValidatorError({ + GladeValidatorError({ required this.value, OnValidateError? devError, this.extra, @@ -35,7 +34,7 @@ abstract class GenericValidatorError extends GladeInputError with Equatabl ((value, _) => 'Value "${value ?? 'NULL'}" does not satisfy validation. [This is default validation meessage. Consider to set `devErro` to cutomize validation errors]'); - factory GenericValidatorError.cantBeNull(T? value, {Object? extra, Object? key}) => + factory GladeValidatorError.cantBeNull(T? value, {Object? extra, Object? key}) => ValueNullError(value: value, key: key, extra: extra); @override diff --git a/glade_forms/lib/src/validator/validator_error/validator_error.dart b/glade_forms/lib/src/validator/validator_error/validator_error.dart index 1024e99..c8a84c1 100644 --- a/glade_forms/lib/src/validator/validator_error/validator_error.dart +++ b/glade_forms/lib/src/validator/validator_error/validator_error.dart @@ -1,4 +1,4 @@ -export 'generic_validator_error.dart'; +export 'glade_validator_error.dart'; export 'value_error.dart'; export 'value_null_error.dart'; export 'value_satisfy_predicate_error.dart'; diff --git a/glade_forms/lib/src/validator/validator_error/value_error.dart b/glade_forms/lib/src/validator/validator_error/value_error.dart index 02f82a5..e105a0f 100644 --- a/glade_forms/lib/src/validator/validator_error/value_error.dart +++ b/glade_forms/lib/src/validator/validator_error/value_error.dart @@ -1,6 +1,6 @@ -import 'package:glade_forms/src/validator/validator_error/generic_validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; -class ValueError extends GenericValidatorError { +class ValueError extends GladeValidatorError { ValueError({ required super.value, required super.devError, diff --git a/glade_forms/lib/src/validator/validator_error/value_null_error.dart b/glade_forms/lib/src/validator/validator_error/value_null_error.dart index 78a7145..9ca7ffd 100644 --- a/glade_forms/lib/src/validator/validator_error/value_null_error.dart +++ b/glade_forms/lib/src/validator/validator_error/value_null_error.dart @@ -1,6 +1,6 @@ -import 'package:glade_forms/src/validator/validator_error/generic_validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; -class ValueNullError extends GenericValidatorError { +class ValueNullError extends GladeValidatorError { ValueNullError({ required super.value, OnValidateError? devError, diff --git a/glade_forms/lib/src/validator/validator_error/value_satisfy_predicate_error.dart b/glade_forms/lib/src/validator/validator_error/value_satisfy_predicate_error.dart index c06132d..af2e949 100644 --- a/glade_forms/lib/src/validator/validator_error/value_satisfy_predicate_error.dart +++ b/glade_forms/lib/src/validator/validator_error/value_satisfy_predicate_error.dart @@ -1,6 +1,6 @@ -import 'package:glade_forms/src/validator/validator_error/generic_validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; -class ValueSatisfyPredicateError extends GenericValidatorError { +class ValueSatisfyPredicateError extends GladeValidatorError { ValueSatisfyPredicateError({ required super.value, required super.devError, diff --git a/glade_forms/lib/src/validator/validator_instance.dart b/glade_forms/lib/src/validator/validator_instance.dart index 0497c27..b983192 100644 --- a/glade_forms/lib/src/validator/validator_instance.dart +++ b/glade_forms/lib/src/validator/validator_instance.dart @@ -1,10 +1,13 @@ -import 'package:glade_forms/glade_forms.dart'; +import 'package:glade_forms/src/core/core.dart'; +import 'package:glade_forms/src/validator/part/input_validator_part.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; +import 'package:glade_forms/src/validator/validator_result.dart'; class ValidatorInstance { /// Stops validation on first error. final bool stopOnFirstError; - late GladeInput _input; + GladeInput? _input; final List> _parts; ValidatorInstance({ @@ -16,19 +19,19 @@ class ValidatorInstance { void bindInput(GladeInput input) => _input = input; /// Performs validation on given [value]. - ValidatorErrors validate(T value, {Object? extra}) { - final errors = >[]; + ValidatorResult validate(T value, {Object? extra}) { + final errors = >[]; for (final part in _parts) { - final error = part.validate(value, extra: extra, dependencies: _input.dependenciesFactory()); + final error = part.validate(value, extra: extra, dependencies: _input?.dependenciesFactory() ?? []); if (error != null) { errors.add(error); - if (stopOnFirstError) return ValidatorErrors(errors: errors, associatedInput: _input); + if (stopOnFirstError) return ValidatorResult(errors: errors, associatedInput: _input); } } - return ValidatorErrors(errors: errors, associatedInput: _input); + return ValidatorResult(errors: errors, associatedInput: _input); } } diff --git a/glade_forms/lib/src/validator/validator_errors.dart b/glade_forms/lib/src/validator/validator_result.dart similarity index 52% rename from glade_forms/lib/src/validator/validator_errors.dart rename to glade_forms/lib/src/validator/validator_result.dart index 4038de9..a1b8b46 100644 --- a/glade_forms/lib/src/validator/validator_errors.dart +++ b/glade_forms/lib/src/validator/validator_result.dart @@ -1,17 +1,17 @@ import 'package:equatable/equatable.dart'; import 'package:glade_forms/src/core/core.dart'; -import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; +import 'package:glade_forms/src/validator/validator_error/glade_validator_error.dart'; -class ValidatorErrors extends Equatable { - final GladeInput associatedInput; - final List> errors; +class ValidatorResult extends Equatable { + final GladeInput? associatedInput; + final List> errors; bool get isValid => errors.isEmpty; @override List get props => [associatedInput, errors]; - const ValidatorErrors({ + const ValidatorResult({ required this.errors, required this.associatedInput, }); diff --git a/glade_forms/pubspec.yaml b/glade_forms/pubspec.yaml index 59005c7..4338654 100644 --- a/glade_forms/pubspec.yaml +++ b/glade_forms/pubspec.yaml @@ -5,7 +5,6 @@ version: 1.0.0 environment: sdk: ^3.0.0 - flutter: ">=3.0.0" # Add regular dependencies here. dependencies: diff --git a/glade_forms/test/generic_validator_test.dart b/glade_forms/test/generic_validator_test.dart index 11e7d0e..aa85ec6 100644 --- a/glade_forms/test/generic_validator_test.dart +++ b/glade_forms/test/generic_validator_test.dart @@ -5,7 +5,7 @@ import 'package:test/test.dart'; void main() { test('Empty validator', () { - final validator = GenericValidator().build()..bindInput(GladeInput.pure('')); + final validator = GladeValidator().build(); expect(validator.validate('').isValid, isTrue); expect(validator.validate('abc').isValid, isTrue); @@ -14,7 +14,7 @@ void main() { }); test('Validator returns isValid for valid value', () { - final validator = (GenericValidator()..notNull()).build()..bindInput(GladeInput.pure('')); + final validator = (GladeValidator()..notNull()).build(); const input = 'hello'; @@ -24,7 +24,7 @@ void main() { }); test("Value can't be null", () { - final validator = (GenericValidator()..notNull()).build()..bindInput(GladeInput.pure('')); + final validator = (GladeValidator()..notNull()).build(); final result = validator.validate(null); expect(result.isValid, isFalse); @@ -34,22 +34,20 @@ void main() { group('Satisfy predicate', () { test('Should succeed', () { - final validator = (GenericValidator() + final validator = (GladeValidator() ..satisfy( (x, _, __) => x > 10, devError: (_, __) => 'Value must be greater than 10', )) - .build() - ..bindInput(GladeInput.pure(0)); + .build(); expect(validator.validate(20).isValid, isTrue); }); test('Value not valid', () { const onErrorMessage = 'Value must be greater than 10'; - final validator = (GenericValidator()..satisfy((x, _, __) => x > 10, devError: (_, __) => onErrorMessage)) - .build() - ..bindInput(GladeInput.pure(0)); + final validator = + (GladeValidator()..satisfy((x, _, __) => x > 10, devError: (_, __) => onErrorMessage)).build(); final result = validator.validate(5); expect(result.isValid, isFalse); @@ -67,27 +65,25 @@ void main() { group('Combined', () { test('notNull() and satisfy() - succeeds', () { - final validator = (GenericValidator() + final validator = (GladeValidator() ..notNull() ..satisfy( (x, _, __) => x.length >= 5, devError: (_, __) => 'Length must be at least 5', )) - .build() - ..bindInput(GladeInput.pure('')); + .build(); expect(validator.validate('Length input').isValid, isTrue); }); test('notNull() and satisfy() - value is null', () { - final validator = (GenericValidator() + final validator = (GladeValidator() ..notNull() ..satisfy( (x, _, __) => x!.length >= 5, devError: (_, __) => 'Length must be at least 5', )) - .build() - ..bindInput(GladeInput.pure('')); + .build(); final result = validator.validate(null); @@ -98,14 +94,13 @@ void main() { test('notNull() and satisfy() - value does not meet predicate', () { const onErrorMessage = 'Length must be at least 5'; - final validator = (GenericValidator() + final validator = (GladeValidator() ..notNull() ..satisfy( (x, _, __) => x!.length >= 5, devError: (_, __) => onErrorMessage, )) - .build() - ..bindInput(GladeInput.pure('')); + .build(); final result = validator.validate('a'); @@ -124,7 +119,7 @@ void main() { test('Run all parts: notNull() and satisfy() - value is null', () { const onErrorMessage = 'Length must be at least 5'; - final validator = (GenericValidator() + final validator = (GladeValidator() ..notNull() ..satisfy( (x, _, __) => (x?.length ?? 0) >= 5, @@ -152,7 +147,7 @@ void main() { group('IsEmail', () { test('Succeeds', () { - final validator = (StringValidator()..isEmail()).build()..bindInput(GladeInput.pure('')); + final validator = (StringValidator()..isEmail()).build(); final inputs = [ 'abc@gmail.com', @@ -168,7 +163,7 @@ void main() { }); test('Value not valid', () { - final validator = (StringValidator()..isEmail()).build()..bindInput(GladeInput.pure('')); + final validator = (StringValidator()..isEmail()).build(); final inputs = [ 'abc-gmail.com', @@ -188,7 +183,7 @@ void main() { group('Is URL', () { test('Succeeds - http(s) optional', () { - final validator = (StringValidator()..isUrl()).build()..bindInput(GladeInput.pure('')); + final validator = (StringValidator()..isUri()).build(); // Expected Url - Check with HTTP(S). final inputs = [ @@ -208,7 +203,7 @@ void main() { }); test('Succeeds - http(s) mandatory', () { - final validator = (StringValidator()..isUrl(requireHttpScheme: true)).build()..bindInput(GladeInput.pure('')); + final validator = (StringValidator()..isUri(requiresScheme: true)).build(); // Expected Url - Check with HTTP(S). final inputs = [ @@ -225,7 +220,7 @@ void main() { }); test('Value is not url', () { - final validator = (StringValidator()..isUrl(requireHttpScheme: true)).build()..bindInput(GladeInput.pure('')); + final validator = (StringValidator()..isUri(requiresScheme: true)).build(); // Expected Url - Check with HTTP(S). final inputs = [ @@ -259,7 +254,6 @@ void main() { )) .build(), value: 0, - //translateError: (err, key, defMessage, {depedencies}) => key == 'e1' ? 'Hodnota nenΓ­ vΔ›tΕ‘Γ­ jak 10!' : defMessage, translateError: (error, key, devMessage, dependencies) => key == 'e1' ? 'Hodnota nenΓ­ vΔ›tΕ‘Γ­ jak 10!' : devMessage, ); diff --git a/glade_forms/test/string_validator_test.dart b/glade_forms/test/string_validator_test.dart index e69de29..d6f8755 100644 --- a/glade_forms/test/string_validator_test.dart +++ b/glade_forms/test/string_validator_test.dart @@ -0,0 +1,219 @@ +// ignore_for_file: avoid-positional-record-field-access + +import 'package:glade_forms/glade_forms.dart'; +import 'package:test/test.dart'; + +void main() { + group('notEmpty', () { + test('When value is null, notEmpty fails', () { + final validator = (StringValidator()..notEmpty()).build(); + + final result = validator.validate(null); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringEmpty)); + }); + + test('When value is empty, notEmpty fails', () { + final validator = (StringValidator()..notEmpty()).build(); + + final result = validator.validate(''); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringEmpty)); + }); + + test('When value is not empty or null, notEmpty pass', () { + final validator = (StringValidator()..notEmpty()).build(); + + final result = validator.validate('test x'); + + expect(result.isValid, isTrue); + }); + }); + + group('isEmail', () { + test('When value is null, isEmail() fails', () { + final validator = (StringValidator()..isEmail()).build(); + + final result = validator.validate(null); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringNotEmail)); + }); + + test('When value is empty, isEmail() fails', () { + final validator = (StringValidator()..isEmail()).build(); + + final result = validator.validate(''); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringNotEmail)); + }); + + for (final testCase in [ + ('test.user@gmail.com', true), + ('test124@x.com', true), + ('a.213.cz@gmail.com', true), + ('test.user@gmail', false), + ('@x.com', false), + ('a.213.czgmail.com', false), + ]) { + test('When email is ${testCase.$1}, isEmail() ${testCase.$2 ? 'pass' : 'fails'}', () { + final validator = (StringValidator()..isEmail()).build(); + + final result = validator.validate(testCase.$1); + + expect(result.isValid, equals(testCase.$2)); + }); + } + }); + + group('isUrl', () { + test('When value is null, isUrl() fails', () { + final validator = (StringValidator()..isUri()).build(); + + final result = validator.validate(null); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringNotUrl)); + }); + + test('When value is empty, isUrl() fails', () { + final validator = (StringValidator()..isUri()).build(); + + final result = validator.validate(''); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringNotUrl)); + }); + + // URL, requires HTTP, expected result + for (final testCase in [ + ('https://x.test.com', true, true), + ('http://test.com/test', true, true), + ('file://sdsasa.asdas.com', false, true), + ('test.com/asdsa?qqe=sa44%20sda', false, true), + ('@x.sada/https://file:sadad', true, false), + ('sadscom:/192.168.1.1', false, true), + ]) { + test('When URL is ${testCase.$1}, isUrl(http: ${testCase.$2}) ${testCase.$3 ? 'pass' : 'fails'}', () { + final validator = (StringValidator()..isUri(requiresScheme: testCase.$2)).build(); + + final result = validator.validate(testCase.$1); + + expect(result.isValid, equals(testCase.$3)); + }); + } + }); + + group('exactLength()', () { + test('exactLength() pass', () { + final validator = (StringValidator()..exactLength(length: 4)).build(); + + final result = validator.validate('abcd'); + + expect(result.isValid, isTrue); + }); + + test('exactLength(), null value fails', () { + final validator = (StringValidator()..exactLength(length: 4)).build(); + + final result = validator.validate(null); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringExactLength)); + }); + + test('exactLength(), empty value fails', () { + final validator = (StringValidator()..exactLength(length: 4)).build(); + + final result = validator.validate(''); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringExactLength)); + }); + + test('exactLength() fails', () { + final validator = (StringValidator()..exactLength(length: 4)).build(); + + final result = validator.validate('asdasd'); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringExactLength)); + }); + }); + + group('maxLength()', () { + test('maxLength() pass', () { + final validator = (StringValidator()..maxLength(length: 4)).build(); + + final result = validator.validate('134'); + + expect(result.isValid, isTrue); + }); + + test('maxLength(), null value fails', () { + final validator = (StringValidator()..maxLength(length: 4)).build(); + + final result = validator.validate(null); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringMaxLength)); + }); + + test('maxLength(), empty value pass', () { + final validator = (StringValidator()..maxLength(length: 4)).build(); + + final result = validator.validate(''); + + expect(result.isValid, isTrue); + }); + + test('maxLength() fails', () { + final validator = (StringValidator()..maxLength(length: 4)).build(); + + final result = validator.validate('asdasd'); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringMaxLength)); + }); + }); + + group('minLength()', () { + test('minLength() pass', () { + final validator = (StringValidator()..minLength(length: 4)).build(); + + final result = validator.validate('abcd'); + + expect(result.isValid, isTrue); + }); + + test('minLength(), null value fails', () { + final validator = (StringValidator()..minLength(length: 4)).build(); + + final result = validator.validate(null); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringMinLength)); + }); + + test('minLength(), empty value fails', () { + final validator = (StringValidator()..minLength(length: 4)).build(); + + final result = validator.validate(''); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringMinLength)); + }); + + test('minLength() fails', () { + final validator = (StringValidator()..minLength(length: 4)).build(); + + final result = validator.validate('a'); + + expect(result.isValid, isFalse); + expect(result.errors.firstOrNull?.key, equals(GladeErrorKeys.stringMinLength)); + }); + }); +} diff --git a/melos.yaml b/melos.yaml index a487194..d7a7423 100644 --- a/melos.yaml +++ b/melos.yaml @@ -2,6 +2,7 @@ name: glade_forms_workspace packages: - glade_forms + - glade_forms/** - examples/** command: @@ -11,7 +12,8 @@ command: scripts: setup: description: Completely setups project - run: melos exec -- fvm flutter pub get | + run: | + melos exec -- fvm flutter pub get melos run gallery_setup # ANALYZING @@ -38,8 +40,8 @@ scripts: # Gallery gallery_setup: - run: - melos exec -- flutter pub run easy_localization:generate -S "assets/translations" -o locale_loader.g.dart | + run: | + melos exec -- flutter pub run easy_localization:generate -S "assets/translations" -o locale_loader.g.dart melos exec -- fvm flutter pub run easy_localization:generate -S "assets/translations" --skip-unnecessary-keys -f keys -o locale_keys.g.dart packageFilters: dependsOn: easy_localization