diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..07c439d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "dart.flutterSdkPath": ".fvm/flutter_sdk", + "dart.lineLength": 120, +} \ No newline at end of file diff --git a/README.md b/README.md index 8b55e73..471187d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,80 @@ - +[![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] -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +--- -## Features +A universal way to define form validators with support of translations. -TODO: List what your package can do. Maybe include images, gifs, or videos. +- [👀 What is this?](#-what-is-this) + - [Why should I use it?](#why-should-i-use-it) +- [🚀 Getting started](#-getting-started) + - [Mutable or immutable model](#mutable-or-immutable-model) + - [Quickstart example](#quickstart-example) + - [Existing validators](#existing-validators) + - [Creating own reusable ValidatorPart](#creating-own-reusable-validatorpart) + - [Adding translation support](#adding-translation-support) + - [Debugging validators](#debugging-validators) +- [👏 Contributing](#-contributing) -## Getting started +## 👀 What is this? -TODO: List prerequisites and provide or point to information on how to -start using the package. +Glade forms offer unified way to define reusable +and fluent API to define Form fields input's validators with support of translation on top of that. -## Usage +### Why should I use it? -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. + +## 🚀 Getting started + +Quicstart example: ```dart -const like = 'sample'; + + ``` -## Additional information +### Mutable or immutable model + + +### Quickstart example + +TBD + +### Existing validators + + +### Creating own reusable ValidatorPart + +### Adding translation support + + +### Debugging validators + + + + + +## 👏 Contributing + +Your contributions are always welcome! Feel free to open pull request. -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. +[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/assets/translations/cs-CZ.json b/examples/gallery/assets/translations/cs-CZ.json new file mode 100644 index 0000000..5f69e2a --- /dev/null +++ b/examples/gallery/assets/translations/cs-CZ.json @@ -0,0 +1,7 @@ +{ + "ageRestriction": { + "under18": "Musí vám být alespoň 18 let pro vstup", + "ageFormat": "Věk musí být číslo" + }, + "empty": "Povinná hodnota" +} \ No newline at end of file diff --git a/examples/gallery/assets/translations/en-US.json b/examples/gallery/assets/translations/en-US.json new file mode 100644 index 0000000..c65edc3 --- /dev/null +++ b/examples/gallery/assets/translations/en-US.json @@ -0,0 +1,7 @@ +{ + "ageRestriction": { + "under18": "You must be at least 18 years old for entry", + "ageFormat": "Age has to be number" + }, + "empty": "You must fill in value" +} \ No newline at end of file diff --git a/examples/gallery/ios/Flutter/Debug.xcconfig b/examples/gallery/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/examples/gallery/ios/Flutter/Debug.xcconfig +++ b/examples/gallery/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/examples/gallery/ios/Flutter/Release.xcconfig b/examples/gallery/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/examples/gallery/ios/Flutter/Release.xcconfig +++ b/examples/gallery/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/examples/gallery/ios/Podfile b/examples/gallery/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/examples/gallery/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/examples/gallery/ios/Runner.xcodeproj/project.pbxproj b/examples/gallery/ios/Runner.xcodeproj/project.pbxproj index a5a8491..6bd5285 100644 --- a/examples/gallery/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/gallery/ios/Runner.xcodeproj/project.pbxproj @@ -168,7 +168,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/examples/gallery/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/gallery/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e42adcb..87131a0 100644 --- a/examples/gallery/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/examples/gallery/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Example - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + CFBundleLocalizations + + en + cs + + diff --git a/examples/gallery/lib/core/widget_example.dart b/examples/gallery/lib/core/widget_example.dart deleted file mode 100644 index 6c087cc..0000000 --- a/examples/gallery/lib/core/widget_example.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -class WidgetExample extends StatelessWidget { - final Widget child; - final bool supportsWebOrDesktop; - - const WidgetExample({ - required this.child, - Key? key, - this.supportsWebOrDesktop = true, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final isMobile = Platform.isAndroid || Platform.isIOS; - if (supportsWebOrDesktop || isMobile) { - return Padding(padding: const EdgeInsets.all(25), child: child); - } - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.warning, color: Colors.yellow, size: 30), - const Text('This widget does not fully support web or desktop environment.'), - const SizedBox(height: 30), - Padding(padding: const EdgeInsets.all(25), child: child), - ], - ); - } -} diff --git a/examples/gallery/lib/core/widget_option_controls.dart b/examples/gallery/lib/core/widget_option_controls.dart deleted file mode 100644 index e69de29..0000000 diff --git a/examples/gallery/lib/generated/locale_keys.g.dart b/examples/gallery/lib/generated/locale_keys.g.dart new file mode 100644 index 0000000..0cadbe1 --- /dev/null +++ b/examples/gallery/lib/generated/locale_keys.g.dart @@ -0,0 +1,8 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +abstract class LocaleKeys { + static const ageRestriction_under18 = 'ageRestriction.under18'; + static const ageRestriction_ageFormat = 'ageRestriction.ageFormat'; + static const empty = 'empty'; + +} diff --git a/examples/gallery/lib/generated/locale_loader.g.dart b/examples/gallery/lib/generated/locale_loader.g.dart new file mode 100644 index 0000000..fa8ec45 --- /dev/null +++ b/examples/gallery/lib/generated/locale_loader.g.dart @@ -0,0 +1,32 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: prefer_single_quotes + +import 'dart:ui'; + +import 'package:easy_localization/easy_localization.dart' show AssetLoader; + +class CodegenLoader extends AssetLoader{ + const CodegenLoader(); + + @override + Future?> load(String path, Locale locale) { + return Future.value(mapLocales[locale.toString()]); + } + + static const Map cs_CZ = { + "ageRestriction": { + "under18": "Musí vám být alespoň 18 let pro vstup", + "ageFormat": "Věk musí být číslo" + }, + "empty": "Povinná hodnota" +}; +static const Map en_US = { + "ageRestriction": { + "under18": "You must be at least 18 years old for entry", + "ageFormat": "Age has to be number" + }, + "empty": "You must fill in value" +}; +static const Map> mapLocales = {"cs_CZ": cs_CZ, "en_US": en_US}; +} diff --git a/examples/gallery/lib/localization_addon_custom.dart b/examples/gallery/lib/localization_addon_custom.dart new file mode 100644 index 0000000..9ef4751 --- /dev/null +++ b/examples/gallery/lib/localization_addon_custom.dart @@ -0,0 +1,56 @@ +// ignore_for_file: prefer-match-file-name + +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; + +/// A [WidgetbookAddon] for changing the active [Locale] via [Localizations]. +class LocalizationAddonCustom extends WidgetbookAddon { + final List locales; + final List> localizationsDelegates; + final ValueChanged onChange; + + @override + List get fields { + return [ + ListField( + name: 'name', + values: locales, + initialValue: initialSetting, + labelBuilder: (locale) => locale.toLanguageTag(), + onChanged: (context, locale) => locale != null ? onChange(locale) : null, + ), + ]; + } + + LocalizationAddonCustom({ + required this.locales, + required this.localizationsDelegates, + required this.onChange, + Locale? initialLocale, + }) : assert( + locales.isNotEmpty, + 'locales cannot be empty', + ), + assert( + initialLocale == null || locales.contains(initialLocale), + 'initialLocale must be in locales', + ), + super( + name: 'Locale', + initialSetting: initialLocale ?? locales.first, + ); + + @override + Locale valueFromQueryGroup(Map group) { + return valueOf('name', group)!; + } + + @override + Widget buildUseCase(BuildContext context, Widget child, Locale setting) { + return Localizations( + locale: setting, + delegates: localizationsDelegates, + child: child, + ); + } +} diff --git a/examples/gallery/lib/main.dart b/examples/gallery/lib/main.dart index c43d1a5..178e85e 100644 --- a/examples/gallery/lib/main.dart +++ b/examples/gallery/lib/main.dart @@ -1,67 +1,119 @@ // ignore_for_file: prefer-match-file-name -import 'dart:io'; - +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.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:storybook_flutter/storybook_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; // ignore: prefer-static-class, ok for now final GlobalKey storyNavigatorKey = GlobalKey(debugLabel: 'storyNavigatorKey'); -void main() { - runApp(const App()); +void main() async { + final _ = WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); + + runApp( + EasyLocalization( + supportedLocales: const [Locale('en', 'US'), Locale('cs', 'CZ')], + path: 'assets/translations', + fallbackLocale: const Locale('en', 'US'), + assetLoader: const CodegenLoader(), + child: const App(), + ), + ); +} + +class EasyLocalePlugin extends Plugin { + EasyLocalePlugin() + : super( + icon: (context) => const Icon(Icons.language), + panelBuilder: (context) => ListView( + children: context.supportedLocales.map((e) { + final isCurrent = e == context.locale; + + return ListTile( + title: Text( + e.languageCode, + style: + Theme.of(context).textTheme.labelMedium?.copyWith(color: isCurrent ? Colors.blue : Colors.black), + ), + onTap: () => context.setLocale(e), + ); + }).toList(), + ), + ); } -class App extends StatelessWidget { - const App({Key? key}) : super(key: key); +class App extends HookWidget { + const App({super.key}); @override Widget build(BuildContext context) { - final isWebOrDesktop = !Platform.isAndroid && !Platform.isIOS; + final lightTheme = WidgetbookTheme(name: 'Light', data: ThemeData.light()); - return Storybook( - plugins: isWebOrDesktop - ? [ - const KnobsPlugin(), - const ContentsPlugin(), - DeviceFramePlugin(), - ] - : null, - wrapperBuilder: (context, child) => MaterialApp( - navigatorKey: storyNavigatorKey, - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - debugShowCheckedModeBanner: false, - home: Scaffold(body: Center(child: child)), - ), - stories: const [ - // Story( - // name: 'Forms/TextInputvalidator', - // description: 'Usage of GenericValidator', - // builder: (context) => const WidgetExample(child: ValidatorInputsExample()), - // ), + return Widgetbook.material( + // appBuilder: (context, child) => MaterialApp( + // locale: context.locale, + // localizationsDelegates: context.localizationDelegates, + // theme: ThemeData.light(), + // home: Material(child: child), + // ), + addons: [ + DeviceFrameAddon( + devices: [Devices.ios.iPhone13], + // initialDevice: Devices.ios.iPhone13, + ), + MaterialThemeAddon( + themes: [ + lightTheme, + // WidgetbookTheme(name: 'Dark', data: ThemeData.dark()), + ], + initialTheme: lightTheme, + // themeBuilder: (context, theme, child) => Theme(data: theme, child: child), + ), + AlignmentAddon(), + LocalizationAddonCustom( + locales: context.supportedLocales, + localizationsDelegates: context.localizationDelegates, + initialLocale: context.locale, + onChange: (locale) => locale == null ? {} : context.setLocale(locale), + ), + ], + directories: [ + WidgetbookUseCase(name: 'test', builder: (context) => const AgeRestrictedExample()), ], ); - } + // final themeMode = useState(ThemeMode.system); - // List _pickerStories() { - // return [ - // Story( - // name: 'Pickers/ListPicker', - // description: 'Modal list picker. Allows to choose one value from predefined set of options.', - // builder: (context) => WidgetExample( - // supportsWebOrDesktop: false, - // child: ListPickerExample( - // noValueText: context.knobs.text(label: 'No value text', initial: 'Select value'), - // confirmButtonText: context.knobs.text(label: 'Confirm button text', initial: 'Ok'), - // backgroundColor: context.knobs.options(label: 'Background color', initial: Colors.white, options: [ - // const Option(label: 'White', value: Colors.white), - // const Option(label: 'GreenBlue', value: Color(0xFF37C999)), - // const Option(label: 'Salmon', value: Color(0xFFE96E63)), - // ]), - // ), - // ), - // ), - // ]; - // } + // return MaterialApp( + // theme: ThemeData.light(), + // darkTheme: ThemeData.dark(), + // themeMode: themeMode.value, + // debugShowCheckedModeBanner: false, + // localizationsDelegates: context.localizationDelegates, + // supportedLocales: context.supportedLocales, + // locale: context.locale, + // home: Storybook( + // initialStory: 'AgeRestrictedExample', + // plugins: [ + // ThemeModePlugin(initialTheme: themeMode.value, onThemeChanged: (v) => themeMode.value = v), + // EasyLocalePlugin(), + // ], + // wrapperBuilder: (context, story) => Scaffold(body: story), + // stories: [ + // Story( + // name: 'AgeRestrictedExample', + // description: 'Simple one field dependency', + // builder: (context) => AgeRestrictedExample( + // key: ValueKey(context.locale), + // ), + // ), + // ], + // ), + // ); + } } diff --git a/examples/gallery/lib/shared/model_debug_info.dart b/examples/gallery/lib/shared/model_debug_info.dart new file mode 100644 index 0000000..2e7b25c --- /dev/null +++ b/examples/gallery/lib/shared/model_debug_info.dart @@ -0,0 +1,18 @@ +import 'package:flutter/widgets.dart'; +import 'package:glade_forms/glade_forms.dart'; + +class ModelDebugInfo extends StatelessWidget { + final MutableGenericModel model; + + const ModelDebugInfo({required this.model, super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(model.formattedValidationErrors), + Text('Model VALID? ${model.isValid}'), + ], + ); + } +} diff --git a/examples/gallery/lib/shared/usecase_container.dart b/examples/gallery/lib/shared/usecase_container.dart new file mode 100644 index 0000000..d612550 --- /dev/null +++ b/examples/gallery/lib/shared/usecase_container.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +class UsecaseContainer extends HookWidget { + final Widget child; + final String shortDescription; + final String? description; + + const UsecaseContainer({ + required this.child, + required this.shortDescription, + super.key, + this.description, + }); + + @override + Widget build(BuildContext context) { + final expanded = useState(false); + + return LayoutBuilder( + builder: (context, constraints) => Scaffold( + body: SizedBox( + height: constraints.maxHeight, + child: Stack( + children: [ + child, + // SizedBox( + // height: 180, + // child: ExpansionTile( + // initiallyExpanded: expanded.value, + // title: Markdown(data: shortDescription), + // onExpansionChanged: (v) => expanded.value = v, + // children: [if (description case final x?) Markdown(data: x)], + // ), + // ), + Align( + alignment: Alignment.bottomCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 100, maxHeight: 200), + child: SizedBox( + width: double.infinity, + child: switch (description) { + final x? => Markdown(data: x), + _ => null, + }, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/examples/gallery/lib/usecases/age_restricted_example.dart b/examples/gallery/lib/usecases/age_restricted_example.dart new file mode 100644 index 0000000..3ff8daf --- /dev/null +++ b/examples/gallery/lib/usecases/age_restricted_example.dart @@ -0,0 +1,121 @@ +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/model_debug_info.dart'; +import 'package:glade_forms_example/shared/usecase_container.dart'; + +class _ErrorKeys { + static const String ageRestriction = 'age-restriction'; +} + +class _Model extends MutableGenericModel { + late StringInput nameInput; + late GladeInput ageInput; + late GladeInput vipInput; + + @override + List> get inputs => [nameInput, ageInput, vipInput]; + + _Model() { + nameInput = StringInput.required( + inputKey: 'name-input', + defaultTranslations: DefaultTranslations( + defaultValueIsNullOrEmptyMessage: LocaleKeys.empty.tr(), + ), + ); + ageInput = GladeInput.create( + (v) => (v + ..notNull() + ..satisfy( + (value, extra, dependencies) { + final vipContentInput = dependencies.byKey('vip-input'); + + if (vipContentInput == null || !vipContentInput.value) { + return true; + } + + return value >= 18; + }, + devError: (_, __) => 'When VIP enabled you must be at least 18 years old.', + key: _ErrorKeys.ageRestriction, + )) + .build(), + value: 0, + valueConverter: StringToTypeConverter( + converter: (rawInput, cantConvert) { + if (rawInput == null) return cantConvert(error: 'Input can not be null', rawValue: rawInput); + + return int.tryParse(rawInput) ?? cantConvert(error: 'Can not convert', rawValue: rawInput); + }, + ), + inputKey: 'age-input', + 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; + }, + dependencies: () => [vipInput], + ); + vipInput = GladeInput.create( + (v) => (v..notNull()).build(), + value: false, + inputKey: 'vip-input', + ); + } +} + +class AgeRestrictedExample extends StatelessWidget { + const AgeRestrictedExample({super.key}); + + @override + Widget build(BuildContext context) { + const markdownData = ''' +If *VIP content* is checked, **age** must be over 18. + '''; + + return UsecaseContainer( + shortDescription: 'If *VIP content* is checked, **age** must be over 18.', + description: markdownData, + child: GladeFormBuilder( + create: (context) => _Model(), + builder: (context, formModel) => Padding( + padding: const EdgeInsets.all(8), + child: Form( + autovalidateMode: AutovalidateMode.always, + child: ListView( + children: [ + TextFormField( + initialValue: formModel.nameInput.value, + onChanged: (value) => formModel.stringFieldUpdateInput(formModel.nameInput, value), + validator: formModel.nameInput.formFieldInputValidator, + ), + TextFormField( + initialValue: formModel.ageInput.stringValue, + onChanged: (value) => formModel.stringFieldUpdateInput(formModel.ageInput, value), + validator: (v) => formModel.ageInput.formFieldInputValidator(v), + ), + CheckboxListTile( + value: formModel.vipInput.value, + title: const Text('VIP Content'), + onChanged: (value) => formModel.updateInput(formModel.vipInput, value), + ), + ElevatedButton( + onPressed: formModel.isValid + ? () { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Saved'))); + } + : null, + child: const Text('Save'), + ), + ModelDebugInfo(model: formModel), + ], + ), + ), + ), + ), + ); + } +} diff --git a/examples/gallery/macos/Flutter/Flutter-Debug.xcconfig b/examples/gallery/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/examples/gallery/macos/Flutter/Flutter-Debug.xcconfig +++ b/examples/gallery/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/gallery/macos/Flutter/Flutter-Release.xcconfig b/examples/gallery/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/examples/gallery/macos/Flutter/Flutter-Release.xcconfig +++ b/examples/gallery/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/gallery/macos/Podfile b/examples/gallery/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/examples/gallery/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/examples/gallery/macos/Podfile.lock b/examples/gallery/macos/Podfile.lock new file mode 100644 index 0000000..5f05f0b --- /dev/null +++ b/examples/gallery/macos/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - FlutterMacOS (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.12.1 diff --git a/examples/gallery/macos/Runner.xcodeproj/project.pbxproj b/examples/gallery/macos/Runner.xcodeproj/project.pbxproj index 7c9099d..dec09b9 100644 --- a/examples/gallery/macos/Runner.xcodeproj/project.pbxproj +++ b/examples/gallery/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4F6C8CCC23D10F13D01AD37A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B41762C53CE75F95583F8146 /* Pods_Runner.framework */; }; + 9DF300D3F5F8D65CA9871204 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CD8B857F2CE6B2ED7EAD45F /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2341B8BB5900513F5DA55765 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 2CD8B857F2CE6B2ED7EAD45F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +80,14 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4A3806580748292B3D8D3D46 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 84B2F2B6C45C77F33E06AC5F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 94B19D5FE5817CAC372502FC /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A89FDE99CC7A16E1B9DC610A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + B41762C53CE75F95583F8146 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F279D4821EB6465CF048B596 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9DF300D3F5F8D65CA9871204 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,12 +103,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4F6C8CCC23D10F13D01AD37A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 31F42CFBC45F8E64F88FD97C /* Pods */ = { + isa = PBXGroup; + children = ( + 4A3806580748292B3D8D3D46 /* Pods-Runner.debug.xcconfig */, + 2341B8BB5900513F5DA55765 /* Pods-Runner.release.xcconfig */, + F279D4821EB6465CF048B596 /* Pods-Runner.profile.xcconfig */, + 84B2F2B6C45C77F33E06AC5F /* Pods-RunnerTests.debug.xcconfig */, + 94B19D5FE5817CAC372502FC /* Pods-RunnerTests.release.xcconfig */, + A89FDE99CC7A16E1B9DC610A /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -125,6 +151,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 31F42CFBC45F8E64F88FD97C /* Pods */, ); sourceTree = ""; }; @@ -175,6 +202,8 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + B41762C53CE75F95583F8146 /* Pods_Runner.framework */, + 2CD8B857F2CE6B2ED7EAD45F /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 983806ACAB0FE61C53B30482 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + D43078054BCC334DDA485315 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + F517382A7BC3ACC15B4C7FC3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -227,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -328,6 +360,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 983806ACAB0FE61C53B30482 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D43078054BCC334DDA485315 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F517382A7BC3ACC15B4C7FC3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +472,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 84B2F2B6C45C77F33E06AC5F /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -393,6 +487,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 94B19D5FE5817CAC372502FC /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -407,6 +502,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A89FDE99CC7A16E1B9DC610A /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/examples/gallery/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/gallery/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8fedab6..397f3d3 100644 --- a/examples/gallery/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/examples/gallery/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/examples/gallery/pubspec.yaml b/examples/gallery/pubspec.yaml index da6230d..80043aa 100644 --- a/examples/gallery/pubspec.yaml +++ b/examples/gallery/pubspec.yaml @@ -4,22 +4,28 @@ version: 0.0.1 publish_to: none environment: - sdk: ">=2.16.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ^3.0.0 dependencies: + easy_localization: ^3.0.3 flutter: sdk: flutter flutter_hooks: ^0.20.1 + flutter_markdown: ^0.6.17+3 glade_forms: path: ../../glade_forms + go_router: ^10.2.0 provider: ^6.0.2 storybook_flutter: ^0.14.0 + widgetbook: ^3.3.0 dev_dependencies: + build_runner: ^2.4.6 flutter_test: sdk: flutter netglade_analysis: ^4.2.0 flutter: uses-material-design: true + assets: + - assets/translations/ diff --git a/glade_forms/lib/glade_forms.dart b/glade_forms/lib/glade_forms.dart index 348dce2..c62c850 100644 --- a/glade_forms/lib/glade_forms.dart +++ b/glade_forms/lib/glade_forms.dart @@ -1,5 +1 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. -/// TODO -library; +export 'src/src.dart'; diff --git a/glade_forms/lib/src/core/convert_error.dart b/glade_forms/lib/src/core/convert_error.dart index e62c444..25fdea5 100644 --- a/glade_forms/lib/src/core/convert_error.dart +++ b/glade_forms/lib/src/core/convert_error.dart @@ -1,27 +1,42 @@ import 'package:equatable/equatable.dart'; +import 'package:glade_forms/src/core/glade_input_error.dart'; -typedef OnConvertError = String Function(String? rawInput, Object? extra); +/// Before validation when converer from string to prpoer type failed. +typedef OnConvertError = String Function(String? rawInput, {Object? extra, Object? key}); -class ConvertError extends Equatable implements Exception { +class ConvertError extends GladeInputError with EquatableMixin implements Exception { final OnConvertError devError; - // ignore: avoid-dynamic, allow dynamic for now - final dynamic error; + final String? input; - final String? rawValue; + // /// Can be used to identify concrete error when translating. + // @override + // // ignore: no-object-declaration, key can be any object. + // final Object? key; + + // ignore: no-object-declaration, error can be anything (but typically it is string) + final Object? _convertError; @override - List get props => [rawValue, devError, error]; + List get props => [input, devError, error, key, _convertError, isConversionError]; String get targetType => T.runtimeType.toString(); + String get devErrorMessage => devError(input, extra: error, key: key); + + @override + // ignore: no-object-declaration, error can be any object. + Object? get error => _convertError; + ConvertError({ - required this.error, - required this.rawValue, - OnConvertError? onError, - }) : devError = - onError ?? ((rawValue, extra) => 'Value "${rawValue ?? 'NULL'}" does not have valid format. Error: $error'); + required Object error, + required this.input, + super.key, + OnConvertError? formatError, + }) : _convertError = error, + devError = formatError ?? + ((rawValue, {extra, key}) => 'Value "${rawValue ?? 'NULL'}" does not have valid format. Error: $error'); @override - String toString() => devError(rawValue, error); + String toString() => devError(input, key: key); } diff --git a/glade_forms/lib/src/core/core.dart b/glade_forms/lib/src/core/core.dart new file mode 100644 index 0000000..b1f231c --- /dev/null +++ b/glade_forms/lib/src/core/core.dart @@ -0,0 +1,9 @@ +export 'convert_error.dart'; +export 'error_translator.dart'; +export 'glade_form_mixin.dart'; +export 'glade_input.dart'; +export 'glade_input_error.dart'; +export 'input_dependencies.dart'; +export 'mutable_generic_model.dart'; +export 'string_to_type_converter.dart'; +export 'type_helper.dart'; diff --git a/glade_forms/lib/src/core/error_translator.dart b/glade_forms/lib/src/core/error_translator.dart new file mode 100644 index 0000000..81599b5 --- /dev/null +++ b/glade_forms/lib/src/core/error_translator.dart @@ -0,0 +1,27 @@ +// ignore_for_file: prefer-match-file-name + +import 'package:glade_forms/src/core/glade_input_error.dart'; +import 'package:glade_forms/src/core/input_dependencies.dart'; + +typedef ErrorTranslator = String Function( + /// Error to translate. + GladeInputError error, + + /// An error identification. + Object? key, + + /// Default dev message. + String devMessage, { + ///Input's dependencies + required InputDependenciesFactory dependencies, +}); + +class DefaultTranslations { + final String? defaultValueIsNullOrEmptyMessage; + final String? defaultConversionMessage; + + const DefaultTranslations({ + this.defaultValueIsNullOrEmptyMessage, + this.defaultConversionMessage, + }); +} diff --git a/glade_forms/lib/src/core/generic_input.dart b/glade_forms/lib/src/core/generic_input.dart deleted file mode 100644 index 38bada8..0000000 --- a/glade_forms/lib/src/core/generic_input.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:glade_forms/src/core/glade_input_base.dart'; -import 'package:glade_forms/src/validator/validator.dart'; -import 'package:meta/meta.dart'; - -typedef InputDependencies = List>; - -extension InputDependenciesFunctions on InputDependencies { - GenericInput? byKey(String key) => firstWhereOrNull((x) => x.inputKey == key); -} - -@immutable -class GenericInput extends GladeInputBase { - @protected - final GenericValidatorInstance validatorInstance; - - final InputDependencies dependencies; - - GenericInput._({ - required super.value, - required this.validatorInstance, - super.isPure, - super.initialValue, - super.valueComparator, - super.translateError, - super.inputKey, - this.dependencies = const [], - }) { - validatorInstance.bindInput(this); - } - - GenericInput.dirty({ - required T value, - T? initialValue, - String? inputKey, - TranslateError? translateError, - ValueComparator? valueComparator, - GenericValidatorInstance? validatorInstance, - InputDependencies dependencies = const [], - }) : this._( - value: value, - validatorInstance: validatorInstance ?? GenericValidator().build(), - inputKey: inputKey, - initialValue: initialValue, - translateError: translateError, - valueComparator: valueComparator, - dependencies: dependencies, - isPure: false, - ); - - GenericInput.pure({ - required T value, - T? initialValue, - String? inputKey, - TranslateError? translateError, - ValueComparator? valueComparator, - GenericValidatorInstance? validatorInstance, - InputDependencies dependencies = const [], - }) : this._( - value: value, - validatorInstance: validatorInstance ?? GenericValidator().build(), - inputKey: inputKey, - initialValue: initialValue ?? value, - translateError: translateError, - valueComparator: valueComparator, - dependencies: dependencies, - isPure: true, - ); - - factory GenericInput.create( - GenericValidatorInstance Function(GenericValidator validator) validatorFactory, { - /// Sets current value of input. - required T value, - - /// Initial value when GenericInput is created. - /// - /// This value can potentially differ from value. Used for computing `isUnchanged`. - T? initialValue, - bool pure = true, - TranslateError? translateError, - ValueComparator? comparator, - String? inputKey, - InputDependencies dependencies = const [], - }) { - final validator = validatorFactory(GenericValidator()); - - return pure - ? GenericInput.pure( - validatorInstance: validator, - value: value, - translateError: translateError, - valueComparator: comparator, - inputKey: inputKey, - dependencies: dependencies, - ) - : GenericInput.dirty( - validatorInstance: validator, - value: value, - initialValue: initialValue, - translateError: translateError, - valueComparator: comparator, - inputKey: inputKey, - dependencies: dependencies, - ); - } - - @override - GenericInput asDirty(T value) => GenericInput.dirty( - validatorInstance: validatorInstance, - value: value, - translateError: translateError, - initialValue: initialValue, - inputKey: inputKey, - valueComparator: valueComparator, - dependencies: dependencies, - ); - - @override - GenericInput asPure(T value) => GenericInput.pure( - validatorInstance: validatorInstance, - value: value, - translateError: translateError, - initialValue: initialValue, - inputKey: inputKey, - valueComparator: valueComparator, - dependencies: dependencies, - ); - - @override - String errorFormatted({String delimiter = '|'}) => error?.errors.map((e) => e.toString()).join(delimiter) ?? ''; - - @protected - // ignore: avoid-dynamic, ok for now - bool errorIsGenericInputError(dynamic error) => error is ValidatorErrors; - - @override - String? translate({String delimiter = '.', Object? customError}) { - final err = customError ?? error; - - if (err == null) return null; - - if (errorIsGenericInputError(err)) { - return translateGenericErrors(err as ValidatorErrors, delimiter); - } - - //ignore: avoid-dynamic, ok for now - if (err is List) { - return err.map((x) => x.toString()).join('.'); - } - - return err.toString(); - } - - @protected - String translateGenericErrors(ValidatorErrors errors, String delimiter) { - final translateErrorTmp = translateError; - if (translateErrorTmp != null) { - return errors.errors - .map((e) => translateErrorTmp(e, e.key, e.onErrorMessage, dependencies: dependencies)) - .join(delimiter); - } - - return errors.errors.map((e) => e.toString()).join(delimiter); - } - - ValidatorErrors? validate() => validator(value); - - @override - ValidatorErrors? validator(T value) { - final result = validatorInstance.validate(value); - - if (result.isValid) return null; - - return result; - } - - String? formFieldValidator(T value, {String delimiter = '.'}) { - final convertedError = validator(value); - - return convertedError != null ? translate(delimiter: delimiter, customError: convertedError) : null; - } -} diff --git a/glade_forms/lib/src/core/glade_form_mixin.dart b/glade_forms/lib/src/core/glade_form_mixin.dart new file mode 100644 index 0000000..5407520 --- /dev/null +++ b/glade_forms/lib/src/core/glade_form_mixin.dart @@ -0,0 +1,26 @@ +import 'package:glade_forms/src/core/glade_input.dart'; + +mixin GladeFormMixin { + bool get isValid => inputs.every((input) => input.isValid); + + bool get isNotValid => !isValid; + + bool get isPure => inputs.every((input) => input.isPure); + + bool get isDirty => !isPure; + + List> get inputs; + + /// Formats errors from `inputs`. + String get formattedValidationErrors => inputs.map((e) { + if (e.hasConversionError) return '${e.inputKey ?? e.runtimeType} - CONVERSION ERROR'; + + if (e.error?.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(); +} diff --git a/glade_forms/lib/src/core/glade_input.dart b/glade_forms/lib/src/core/glade_input.dart new file mode 100644 index 0000000..8888aab --- /dev/null +++ b/glade_forms/lib/src/core/glade_input.dart @@ -0,0 +1,383 @@ +import 'package:flutter/foundation.dart'; +import 'package:glade_forms/src/core/convert_error.dart'; +import 'package:glade_forms/src/core/error_translator.dart'; +import 'package:glade_forms/src/core/input_dependencies.dart'; +import 'package:glade_forms/src/core/mutable_generic_model.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'; + +typedef ValueComparator = bool Function(T? initial, T? value); + +class GladeInput extends ChangeNotifier { + final MutableGenericModel? bindContext; + + @protected + final ValueComparator? valueComparator; + + @protected + final GenericValidatorInstance validatorInstance; + + @protected + final StringToTypeConverter? stringTovalueConverter; + + final InputDependenciesFactory dependencies; + + /// An input's identification. + final String? inputKey; + + /// Initial value - does not change after creating. + final T? initialValue; + + final ErrorTranslator? translateError; + + /// Validation message for conversion error. + final DefaultTranslations? defaultTranslations; + + final StringToTypeConverter _defaultConverter = StringToTypeConverter(converter: (x, _) => x as T); + + /// Current input's value. + T _value; + + /// Input did not updated its value from initialValue. + bool _isPure; + + /// Input is in invalid state when there was conversion error. + bool _conversionError = false; + + T get value => _value; + + bool get isPure => _isPure; + + // @override + // int get hashCode => Object.hashAll([value, _isPure]); + + ValidatorErrors? get error => _validator(value); + + /// [value] is equal to [initialValue]. + /// + /// Can be dirty or pure. + bool get isUnchanged => (valueComparator?.call(initialValue, value) ?? value) == initialValue; + + bool get isValid => !_conversionError && _validator(value) == null; + + bool get isNotValid => !isValid; + + bool get hasConversionError => _conversionError; + + String get stringValue => stringTovalueConverter?.convertBack(value) ?? value.toString(); + + // ignore: no_runtimetype_tostring, in this case it is ok - only for dev purposes + String get inputName => inputKey ?? '$runtimeType($value)'; + + set value(T value) { + _value = value; + + _isPure = false; + _conversionError = false; + + notifyListeners(); + } + + GladeInput({ + required T value, + required this.validatorInstance, + required bool isPure, + required this.initialValue, + required this.valueComparator, + required this.inputKey, + required this.translateError, + required this.bindContext, + required this.stringTovalueConverter, + required this.dependencies, + required this.defaultTranslations, + }) : _isPure = isPure, + _value = value { + validatorInstance.bindInput(this); + } + + GladeInput.pure( + T value, { + T? initialValue, + String? inputKey, + ValueComparator? valueComparator, + MutableGenericModel? bindContext, + StringToTypeConverter? valueConverter, + GenericValidatorInstance? validatorInstance, + InputDependenciesFactory? dependencies, + ErrorTranslator? translateError, + DefaultTranslations? defaultTranslations, + }) : this( + value: value, + isPure: true, + inputKey: inputKey, + initialValue: initialValue ?? value, + valueComparator: valueComparator, + bindContext: bindContext, + stringTovalueConverter: valueConverter, + dependencies: dependencies ?? () => [], + validatorInstance: validatorInstance ?? GenericValidator().build(), + translateError: translateError, + defaultTranslations: defaultTranslations, + ); + + GladeInput.dirty( + T value, { + T? initialValue, + String? inputKey, + ValueComparator? valueComparator, + MutableGenericModel? bindContext, + StringToTypeConverter? valueConverter, + GenericValidatorInstance? validatorInstance, + InputDependenciesFactory? dependencies, + ErrorTranslator? translateError, + DefaultTranslations? defaultTranslations, + }) : this( + value: value, + isPure: false, + inputKey: inputKey, + initialValue: initialValue, + valueComparator: valueComparator, + stringTovalueConverter: valueConverter, + bindContext: bindContext, + dependencies: dependencies ?? () => [], + validatorInstance: validatorInstance ?? GenericValidator().build(), + translateError: translateError, + defaultTranslations: defaultTranslations, + ); + + factory GladeInput.create( + GenericValidatorInstance Function(GenericValidator v) validatorFactory, { + /// Sets current value of input. + required T value, + + /// Initial value when GenericInput is created. + /// + /// This value can potentially differ from value. Used for computing `isUnchanged`. + T? initialValue, + bool pure = true, + ErrorTranslator? translateError, + ValueComparator? comparator, + String? inputKey, + StringToTypeConverter? valueConverter, + InputDependenciesFactory? dependencies, + }) { + final validator = validatorFactory(GenericValidator()); + + return pure + ? GladeInput.pure( + value, + validatorInstance: validator, + initialValue: initialValue ?? value, + translateError: translateError, + valueComparator: comparator, + inputKey: inputKey, + valueConverter: valueConverter, + dependencies: dependencies, + ) + : GladeInput.dirty( + value, + validatorInstance: validator, + initialValue: initialValue, + translateError: translateError, + valueComparator: comparator, + inputKey: inputKey, + valueConverter: valueConverter, + dependencies: dependencies, + ); + } + + // Predefined GenericInput without any validations. + /// + /// Useful for input which allows null value without additional validations. + /// + /// In case of need of any validation use [GladeInput.create] directly. + factory GladeInput.optional({ + required T value, + T? initialValue, + bool pure = true, + ErrorTranslator? translateError, + ValueComparator? comparator, + String? inputKey, + StringToTypeConverter? valueConverter, + InputDependenciesFactory? dependencies, + }) => + GladeInput.create( + (v) => v.build(), + value: value, + initialValue: initialValue, + translateError: translateError, + comparator: comparator, + valueConverter: valueConverter, + inputKey: inputKey, + pure: pure, + dependencies: dependencies, + ); + + // Predefined GenericInput with predefined `notNull` validation. + /// + /// In case of need of any aditional validation use [GladeInput.create] directly. + factory GladeInput.required({ + required T value, + T? initialValue, + bool pure = true, + ErrorTranslator? translateError, + ValueComparator? comparator, + String? inputKey, + StringToTypeConverter? valueConverter, + InputDependenciesFactory? dependencies, + }) => + GladeInput.create( + (v) => (v..notNull()).build(), + value: value, + initialValue: initialValue, + translateError: translateError, + comparator: comparator, + valueConverter: valueConverter, + inputKey: inputKey, + pure: pure, + dependencies: dependencies, + ); + + GladeInput asDirty(T value) => copyWith(isPure: false, value: value); + + GladeInput asPure(T value) => copyWith(isPure: true, value: value); + + ValidatorErrors? validate() => _validator(value); + + // TODO(petr): Consider to pass BuildContext. This would allow use context based translation. + String? translate({String delimiter = '.', Object? customError}) { + final err = customError ?? error; + + if (err == null) return null; + + if (err is ValidatorErrors) { + return _translateGenericErrors(err, delimiter); + } + + if (err is ConvertError) { + final defaultTranslationsTmp = this.defaultTranslations; + final translateErrorTmp = translateError; + if (translateErrorTmp != null) { + return translateErrorTmp(err, err.key, err.devErrorMessage, dependencies: dependencies); + } else if (defaultTranslationsTmp != null && defaultTranslationsTmp.defaultConversionMessage != null) { + return defaultTranslationsTmp.defaultConversionMessage; + } + } + + //ignore: avoid-dynamic, ok for now + if (err is List) { + return err.map((x) => x.toString()).join('.'); + } + + return err.toString(); + } + + String errorFormatted({String delimiter = '|'}) => error?.errors.map((e) => e.toString()).join(delimiter) ?? ''; + + /// Shorthand validator for TextFieldForm inputs. + /// + /// Returns translated validation message. Translated message is returned through [translate]. + /// If there are multiple errors they are concenated into one string with [delimiter]. + String? formFieldInputValidatorCustom(String? value, {String delimiter = '.'}) { + assert( + TypeHelper.typesEqual() || TypeHelper.typesEqual() || stringTovalueConverter != null, + 'For non-string values [converter] must be provided. TInput type: ${T.runtimeType}', + ); + final converter = stringTovalueConverter ?? _defaultConverter; + + try { + final convertedValue = converter.convert(value); + final convertedError = _validator(convertedValue); + + return convertedError != null ? translate(delimiter: delimiter, customError: convertedError) : null; + } on ConvertError catch (formatError) { + return formatError.error != null + ? translate(delimiter: delimiter, customError: formatError) + : formatError.devError(value, extra: error); + } + } + + /// Shorthand validator for TextFieldForm inputs. + /// + /// Returns translated validation message. Translated message is returned through [translate]. + String? formFieldInputValidator(String? value) => formFieldInputValidatorCustom(value); + + void updateValueWithString(String? strValue) { + assert( + TypeHelper.typesEqual() || TypeHelper.typesEqual() || stringTovalueConverter != null, + 'For non-string values [converter] must be provided. TInput type: ${T.runtimeType}', + ); + + final converter = stringTovalueConverter ?? _defaultConverter; + + try { + this.value = converter.convert(strValue); + } on ConvertError { + _conversionError = true; + } + } + + // @override + // bool operator ==(Object other) { + // if (other.runtimeType != runtimeType) return false; + + // return other is GladeInputBase && other.value == value && other._isPure == _isPure; + // } + + @protected + GladeInput copyWith({ + MutableGenericModel? bindContext, + ValueComparator? valueComparator, + GenericValidatorInstance? validatorInstance, + StringToTypeConverter? stringTovalueConverter, + InputDependenciesFactory? dependencies, + String? inputKey, + T? initialValue, + ErrorTranslator? translateError, + T? value, + bool? isPure, + DefaultTranslations? defaultTranslations, + }) { + return GladeInput( + value: value ?? this.value, + bindContext: bindContext ?? this.bindContext, + valueComparator: valueComparator ?? this.valueComparator, + validatorInstance: validatorInstance ?? this.validatorInstance, + stringTovalueConverter: stringTovalueConverter ?? this.stringTovalueConverter, + dependencies: dependencies ?? this.dependencies, + inputKey: inputKey ?? this.inputKey, + initialValue: initialValue ?? this.initialValue, + translateError: translateError ?? this.translateError, + isPure: isPure ?? this.isPure, + defaultTranslations: defaultTranslations ?? this.defaultTranslations, + ); + } + + ValidatorErrors? _validator(T value) { + final result = validatorInstance.validate(value); + + if (result.isValid) return null; + + return result; + } + + String _translateGenericErrors(ValidatorErrors inputErrors, String delimiter) { + final translateErrorTmp = translateError; + + final defaultTranslationsTmp = this.defaultTranslations; + if (translateErrorTmp != null) { + return inputErrors.errors + .map((e) => translateErrorTmp(e, e.key, e.devErrorMessage, dependencies: dependencies)) + .join(delimiter); + } + + return inputErrors.errors.map((e) { + if (defaultTranslationsTmp != null && (e.isNullError || e.hasStringEmptyOrNullErrorKey)) { + return defaultTranslationsTmp.defaultValueIsNullOrEmptyMessage ?? e.toString(); + } + + return e.toString(); + }).join(delimiter); + } +} diff --git a/glade_forms/lib/src/core/glade_input_base.dart b/glade_forms/lib/src/core/glade_input_base.dart deleted file mode 100644 index 2b2d85d..0000000 --- a/glade_forms/lib/src/core/glade_input_base.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/validator/validator.dart'; - -// /// Base input with value of type [TInput] and validation error of type [TError]. -// /// Provides [translate] method for translating its error. -// abstract class BaseInput extends FormzInput { -// const BaseInput.asDirty(super.value) : super.dirty(); - -// const BaseInput.dirty(super.value) : super.dirty(); - -// const BaseInput.pure(super.value) : super.pure(); - -// // ignore: avoid-dynamic, error can be anything -// String? translate({String delimiter = '.', dynamic customError}) => error?.toString(); -// } - -typedef ValueComparator = bool Function(T? initial, T? value); - -typedef TranslateError = String Function( - GenericValidatorError error, - Object? key, - String defaultMessage, { - required InputDependencies dependencies, -}); - -@immutable -abstract class GladeInputBase { - @protected - final ValueComparator? valueComparator; - - final String? inputKey; - - final T value; - - /// Initial value - does not change after creating. - final T? initialValue; - - final bool isPure; - - final TranslateError? translateError; - - @override - int get hashCode => Object.hashAll([value, isPure]); - - ValidatorErrors? get error => validator(value); - - bool get isUnchanged => (valueComparator?.call(initialValue, value) ?? value) == initialValue; - - bool get isValid => validator(value) == null; - - bool get isNotValid => !isValid; - - const GladeInputBase({ - required this.value, - this.isPure = true, - this.initialValue, - this.valueComparator, - this.inputKey, - this.translateError, - }); - - const GladeInputBase.pure( - T value, { - T? initialValue, - ValueComparator? comparator, - }) : this( - value: value, - initialValue: initialValue ?? value, - valueComparator: comparator, - ); - - const GladeInputBase.dirty( - T value, { - T? initialValue, - ValueComparator? comparator, - }) : this( - value: value, - isPure: false, - initialValue: initialValue, - valueComparator: comparator, - ); - - GladeInputBase asDirty(T value); - - GladeInputBase asPure(T value); - - ValidatorErrors? validator(T value); - - String? translate({String delimiter = '.', Object? customError}) => error?.toString(); - - String errorFormatted({String delimiter = '|'}) => error?.errors.map((e) => e.toString()).join(delimiter) ?? ''; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - - return other is GladeInputBase && other.value == value && other.isPure == isPure; - } -} - -class _GladeForms { - // ignore: avoid-dynamic, we allow mix of inputs. - static bool validate(List> inputs) { - return inputs.every((input) => input.isValid); - } - - // ignore: avoid-dynamic, we allow mix of inputs. - static bool isPure(List> inputs) { - return inputs.every((input) => input.isPure); - } -} - -mixin GladeFormMixin { - bool get isValid => _GladeForms.validate(inputs); - - bool get isNotValid => !isValid; - - bool get isPure => _GladeForms.isPure(inputs); - - bool get isDirty => !isPure; - - // ignore: avoid-dynamic, we allow mix of inputs. - List> get inputs; - - /// Formats errors from `inputs`. - String get formattedValidationErrors => inputs.map((e) { - if (e.error?.errors.isNotEmpty ?? false) { - return '${e.inputKey ?? e.runtimeType} - ${e.errorFormatted()}'; - } - - return '${e.inputKey ?? e.runtimeType} - VALID'; - }).join('\n'); - - // ignore: avoid-dynamic, error from Formz is dynamic - List get errors => inputs.map((e) => e.error).toList(); -} - -abstract class GenericModel with GladeFormMixin {} diff --git a/glade_forms/lib/src/core/glade_input_error.dart b/glade_forms/lib/src/core/glade_input_error.dart new file mode 100644 index 0000000..b735919 --- /dev/null +++ b/glade_forms/lib/src/core/glade_input_error.dart @@ -0,0 +1,19 @@ +import 'package:glade_forms/src/core/convert_error.dart'; +import 'package:glade_forms/src/validator/validator.dart'; +import 'package:glade_forms/src/validator/validator_error/validator_keys.dart'; + +abstract class GladeInputError { + /// Can be used to identify concrete error when translating. + // ignore: no-object-declaration, key can be any object + final Object? key; + + // ignore: no-object-declaration, error can be anything (but typically it is string) + Object? get error; + + bool get isConversionError => this is ConvertError; + bool get isNullError => this is ValueNullError; + + bool get hasStringEmptyOrNullErrorKey => key == GladeErrorKeys.stringEmpty; + + const GladeInputError({this.key}); +} diff --git a/glade_forms/lib/src/core/input_dependencies.dart b/glade_forms/lib/src/core/input_dependencies.dart new file mode 100644 index 0000000..40d2109 --- /dev/null +++ b/glade_forms/lib/src/core/input_dependencies.dart @@ -0,0 +1,15 @@ +// ignore_for_file: avoid-dynamic, prefer-match-file-name + +import 'package:collection/collection.dart'; +import 'package:glade_forms/src/core/core.dart'; + +typedef InputDependencies = List>; +typedef InputDependenciesFactory = InputDependencies Function(); + +extension InputDependenciesFunctions on InputDependencies { + GladeInput? byKey(String key) => firstWhereOrNull((x) => x.inputKey == key).castOrNull>(); +} + +extension ObjectEx on Object? { + T? castOrNull() => this is T ? this as T : null; +} diff --git a/glade_forms/lib/src/core/mutable_generic_model.dart b/glade_forms/lib/src/core/mutable_generic_model.dart new file mode 100644 index 0000000..020dec9 --- /dev/null +++ b/glade_forms/lib/src/core/mutable_generic_model.dart @@ -0,0 +1,26 @@ +import 'package:flutter/foundation.dart'; +import 'package:glade_forms/src/core/core.dart'; + +abstract class MutableGenericModel extends ChangeNotifier with GladeFormMixin { + // @protected + // void updateInput, T>(INPUT input, T value, void Function(INPUT v) assign) { + // if (input.value == value) return; + + // assign(input.asDirty(value) as INPUT); + // notifyListeners(); + // } + + void stringFieldUpdateInput>(INPUT input, String? value) { + if (input.value == value) return; + + input.updateValueWithString(value); + notifyListeners(); + } + + void updateInput, T>(INPUT input, T value) { + if (input.value == value) return; + + input.value = value; + notifyListeners(); + } +} 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 b788cdb..0f62b3e 100644 --- a/glade_forms/lib/src/core/string_to_type_converter.dart +++ b/glade_forms/lib/src/core/string_to_type_converter.dart @@ -1,27 +1,34 @@ import 'package:glade_forms/src/core/convert_error.dart'; -import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; -// ignore: avoid-dynamic, ok for now -typedef OnErrorCallback = ConvertError Function(String? rawValue, dynamic error); +typedef OnErrorCallback = ConvertError Function(String? rawValue, Object error); -typedef Converter = T Function( +typedef ConverterToType = T Function( String? rawInput, T Function({ - // ignore: avoid-dynamic, ok for now - required dynamic error, + required Object error, required String? rawValue, - OnError? onError, - }) convert, + OnConvertError? onError, + }) cantConvert, ); +typedef TypeConverterToString = String? Function(T rawInput); + +/// Used to convert string input into `T` value. class StringToTypeConverter { - final Converter converter; + final ConverterToType converter; final OnErrorCallback onError; + final TypeConverterToString _converterBack; + StringToTypeConverter({ + /// Converts string value to [T] type. required this.converter, - OnErrorCallback? onError, - }) : onError = onError ?? ((rawValue, error) => ConvertError(rawValue: rawValue, error: error)); + + /// Converts [T] back to string. + TypeConverterToString? converterBack, + //OnErrorCallback? onError, + }) : _converterBack = converterBack ?? ((rawInput) => rawInput.toString()), + onError = ((rawValue, error) => ConvertError(input: rawValue, error: error)); T convert(String? input) { try { @@ -29,18 +36,19 @@ 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 it catches everything + // 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); } } + String? convertBack(T input) => _converterBack(input); + T _cantConvert({ required String? rawValue, - // ignore: avoid-dynamic, ok for now - required dynamic error, - OnError? onError, + required Object error, + OnConvertError? onError, }) => - throw ConvertError(rawValue: rawValue, onError: onError, error: error); + throw ConvertError(input: rawValue, formatError: onError, error: error); } diff --git a/glade_forms/lib/src/core/type_helper.dart b/glade_forms/lib/src/core/type_helper.dart new file mode 100644 index 0000000..4e77415 --- /dev/null +++ b/glade_forms/lib/src/core/type_helper.dart @@ -0,0 +1,3 @@ +class TypeHelper { + static bool typesEqual() => T1 == T2; +} diff --git a/glade_forms/lib/src/forms/text_form_field_generic_input.dart b/glade_forms/lib/src/forms/text_form_field_generic_input.dart deleted file mode 100644 index 8eb7f33..0000000 --- a/glade_forms/lib/src/forms/text_form_field_generic_input.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/core/glade_input_base.dart'; -import 'package:glade_forms/src/core/string_to_type_converter.dart'; -import 'package:glade_forms/src/forms/text_form_field_input_validator_mixin.dart'; -import 'package:glade_forms/src/validator/validator.dart'; - -bool _typesEqual() => T1 == T2; - -/// Same as generic input but with mixed-in [TextFormFieldInputValidatorMixin]. -class TextFormFieldGenericInput extends GenericInput with TextFormFieldInputValidatorMixin { - final StringToTypeConverter _stringToTypeConverter; - - @override - StringToTypeConverter get converter => _stringToTypeConverter; - - factory TextFormFieldGenericInput.create( - GenericValidatorInstance Function(GenericValidator validator) validatorFactory, { - T? defaultValue, - bool pure = true, - StringToTypeConverter? converter, - TranslateError? translateError, - String? inputKey, - }) { - final instance = validatorFactory(GenericValidator()); - - return pure - ? TextFormFieldGenericInput.pure( - validatorInstance: instance, - value: defaultValue, - stringToTypeConverter: converter, - translateError: translateError, - inputKey: inputKey, - ) - : TextFormFieldGenericInput.dirty( - validatorInstance: instance, - value: defaultValue, - stringToTypeConverter: converter, - translateError: translateError, - inputKey: inputKey, - ); - } - - TextFormFieldGenericInput.dirty({ - super.value, - super.validatorInstance, - StringToTypeConverter? stringToTypeConverter, - super.translateError, - super.inputKey, - }) : assert( - _typesEqual() || _typesEqual() || stringToTypeConverter != null, - 'For non-string values [converter] must be provided. TInput type: ${T.runtimeType}', - ), - _stringToTypeConverter = stringToTypeConverter ?? StringToTypeConverter(converter: (x, _) => x as T), - super.dirty(); - - TextFormFieldGenericInput.pure({ - super.value, - super.validatorInstance, - StringToTypeConverter? stringToTypeConverter, - super.translateError, - super.inputKey, - }) : assert( - _typesEqual() || _typesEqual() || stringToTypeConverter != null, - 'For non-string values [converter] must be provided. TInput type: ${T.runtimeType}', - ), - _stringToTypeConverter = stringToTypeConverter ?? StringToTypeConverter(converter: (x, _) => x as T), - super.pure(); - - @override - TextFormFieldGenericInput asDirty(T? value) => TextFormFieldGenericInput.dirty( - validatorInstance: validatorInstance, - value: value, - stringToTypeConverter: _stringToTypeConverter, - translateError: translateError, - inputKey: inputKey, - ); - - @override - TextFormFieldGenericInput asPure(T? value) => TextFormFieldGenericInput.pure( - validatorInstance: validatorInstance, - value: value, - stringToTypeConverter: _stringToTypeConverter, - translateError: translateError, - inputKey: inputKey, - ); -} diff --git a/glade_forms/lib/src/forms/text_form_field_input_validator_mixin.dart b/glade_forms/lib/src/forms/text_form_field_input_validator_mixin.dart deleted file mode 100644 index 61f0255..0000000 --- a/glade_forms/lib/src/forms/text_form_field_input_validator_mixin.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:glade_forms/src/core/convert_error.dart'; -import 'package:glade_forms/src/core/glade_input_base.dart'; -import 'package:glade_forms/src/core/string_to_type_converter.dart'; -import 'package:meta/meta.dart'; - -/// Provides short-hand validator function for TextFormField's [validator] function -/// -/// That is function prototype `String? Function(String?)` - if there is any error it should return non-null string value. -/// -/// This mixin could be mixed-in into GenericInput's with its validator for easy validation. -/// If [T] is not a String instance given class should override [converter] to provide concrete type converter ino concrete type. -mixin TextFormFieldInputValidatorMixin on GladeInputBase { - @protected - StringToTypeConverter get converter => StringToTypeConverter(converter: (x, _) => x as T); - - /// Shorthand validator for TextFieldForm inputs. - /// - /// Returns translated validation message. Translated message is returned through [translate]. - String? formFieldInputValidator(String? value, {String delimiter = '.'}) { - try { - final convertedValue = converter.convert(value); - final convertedError = validator(convertedValue); - - return convertedError != null ? translate(delimiter: delimiter, customError: convertedError) : null; - } on ConvertError catch (formatError) { - return formatError.error != null - ? translate(delimiter: delimiter, customError: formatError.error) - : formatError.devError(value, error); - } - } -} diff --git a/glade_forms/lib/src/forms/text_form_field_string_input.dart b/glade_forms/lib/src/forms/text_form_field_string_input.dart deleted file mode 100644 index abc8c0e..0000000 --- a/glade_forms/lib/src/forms/text_form_field_string_input.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/core/glade_input_base.dart'; -import 'package:glade_forms/src/core/string_to_type_converter.dart'; -import 'package:glade_forms/src/forms/text_form_field_input_validator_mixin.dart'; -import 'package:glade_forms/src/validator/generic_validator_instance.dart'; -import 'package:glade_forms/src/validator/string_validator.dart'; - -/// Same as string input but with mixed-in [TextFormFieldInputValidatorMixin]. -class TextFormFieldStringInput extends GenericInput with TextFormFieldInputValidatorMixin { - final StringToTypeConverter _stringToTypeConverter; - - @override - StringToTypeConverter get converter => _stringToTypeConverter; - - factory TextFormFieldStringInput.create( - GenericValidatorInstance Function(StringValidator validator) validatorFactory, { - String? defaultValue, - bool pure = true, - TranslateError? translateError, - String? inputKey, - }) { - final instance = validatorFactory(StringValidator()); - - return pure - ? TextFormFieldStringInput.pure( - validatorInstance: instance, - value: defaultValue, - translateError: translateError, - inputKey: inputKey, - ) - : TextFormFieldStringInput.dirty( - validatorInstance: instance, - value: defaultValue, - translateError: translateError, - inputKey: inputKey, - ); - } - - TextFormFieldStringInput.dirty({ - super.value, - super.validatorInstance, - super.translateError, - super.inputKey, - super.valueComparator, - }) : _stringToTypeConverter = StringToTypeConverter(converter: (x, _) => x ?? ''), - super.dirty(); - - TextFormFieldStringInput.pure({ - super.value, - super.validatorInstance, - super.translateError, - super.inputKey, - super.valueComparator, - }) : _stringToTypeConverter = StringToTypeConverter(converter: (x, _) => x ?? ''), - super.pure(); - - @override - TextFormFieldStringInput asDirty(String? value) => TextFormFieldStringInput.dirty( - value: value, - validatorInstance: validatorInstance, - translateError: translateError, - inputKey: inputKey, - valueComparator: valueComparator, - ); - - @override - TextFormFieldStringInput asPure(String? value) => TextFormFieldStringInput.pure( - value: value, - validatorInstance: validatorInstance, - translateError: translateError, - inputKey: inputKey, - valueComparator: valueComparator, - ); -} diff --git a/glade_forms/lib/src/model/generic_form_model.dart b/glade_forms/lib/src/model/generic_form_model.dart deleted file mode 100644 index 0c45149..0000000 --- a/glade_forms/lib/src/model/generic_form_model.dart +++ /dev/null @@ -1,10 +0,0 @@ - -// abstract class GenericFormModel extends ChangeNotifier with FormzMixin, GenericInputsFormzMixin { -// @protected -// void updateInput, T>(INPUT input, T value, void Function(INPUT v) assign) { -// if (input.value == value) return; - -// assign(input.asDirty(value) as INPUT); -// notifyListeners(); -// } -// } diff --git a/glade_forms/lib/src/src.dart b/glade_forms/lib/src/src.dart new file mode 100644 index 0000000..3060aa2 --- /dev/null +++ b/glade_forms/lib/src/src.dart @@ -0,0 +1,4 @@ +export 'core/core.dart'; +export 'validator/validator.dart'; +export 'variants/variants.dart'; +export 'widgets/widgets.dart'; diff --git a/glade_forms/lib/src/validator/generic_validator.dart b/glade_forms/lib/src/validator/generic_validator.dart index 6b91926..c86ca53 100644 --- a/glade_forms/lib/src/validator/generic_validator.dart +++ b/glade_forms/lib/src/validator/generic_validator.dart @@ -1,12 +1,13 @@ -import 'package:glade_forms/src/core/generic_input.dart'; +import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/validator/generic_validator_instance.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_error/validator_keys.dart'; typedef ValidateFunction = GenericValidatorError? Function( - T? value, { + T value, { required InputDependencies dependencies, Object? extra, }); @@ -15,7 +16,7 @@ class GenericValidator { List> parts = []; GenericValidatorInstance build({ - /// Return validation result on first error or continue validation. + /// Returns validation result on first error or continues validation. /// /// Beware that some validators assume non-null value. bool stopOnFirstError = true, @@ -39,13 +40,13 @@ class GenericValidator { void customPart(InputValidatorPart part) => parts.add(part); /// Checks that value is not null. Returns [ValueNullError] error. - void notNull({OnError? devError, Object? key}) => custom( + void notNull({OnValidateError? devError, Object? key}) => custom( (value, {required dependencies, extra}) => value == null ? ValueNullError( value: value, devError: devError, extra: extra, - key: key, + key: key ?? GladeErrorKeys.valueIsNull, ) : null, ); @@ -53,7 +54,7 @@ class GenericValidator { /// Value must satisfy given [predicate]. Returns [ValueSatisfyPredicateError]. void satisfy( SatisfyPredicate predicate, { - OnError? devError, + OnValidateError? devError, InputDependencies dependencies = const [], Object? extra, Object? key, diff --git a/glade_forms/lib/src/validator/generic_validator_instance.dart b/glade_forms/lib/src/validator/generic_validator_instance.dart index fcad9f1..8f4e250 100644 --- a/glade_forms/lib/src/validator/generic_validator_instance.dart +++ b/glade_forms/lib/src/validator/generic_validator_instance.dart @@ -1,11 +1,10 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/validator/validator.dart'; +import 'package:glade_forms/glade_forms.dart'; class GenericValidatorInstance { /// Stops validation on first error. final bool stopOnFirstError; - late GenericInput _input; + late GladeInput _input; final List> _parts; GenericValidatorInstance({ @@ -14,14 +13,14 @@ class GenericValidatorInstance { }) : _parts = parts; // ignore: use_setters_to_change_properties, this is ok - void bindInput(GenericInput input) => _input = input; + void bindInput(GladeInput input) => _input = input; /// Performs validation on given [value]. ValidatorErrors validate(T value, {Object? extra}) { final errors = >[]; for (final part in _parts) { - final error = part.validate(value, extra: extra, dependencies: _input.dependencies); + final error = part.validate(value, extra: extra, dependencies: _input.dependencies()); if (error != null) { errors.add(error); 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 037eb0a..b079f53 100644 --- a/glade_forms/lib/src/validator/part/custom_validation_part.dart +++ b/glade_forms/lib/src/validator/part/custom_validation_part.dart @@ -1,10 +1,10 @@ -import 'package:glade_forms/src/core/generic_input.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/validator_error.dart'; class CustomValidationPart extends InputValidatorPart { final GenericValidatorError? Function( - T? value, { + T value, { required InputDependencies dependencies, Object? extra, }) customValidator; @@ -17,7 +17,7 @@ class CustomValidationPart extends InputValidatorPart { @override GenericValidatorError? validate( - T? value, { + 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 692067c..f54d788 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,4 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -// ignore: one_member_abstracts, abstract class needed +import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; abstract class InputValidatorPart { 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 01f67bf..8c22b81 100644 --- a/glade_forms/lib/src/validator/part/satisfy_predicate_part.dart +++ b/glade_forms/lib/src/validator/part/satisfy_predicate_part.dart @@ -1,11 +1,11 @@ -import 'package:glade_forms/src/core/generic_input.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/validator_error.dart'; typedef SatisfyPredicate = bool Function(T value, Object? extra, InputDependencies dependencies); class SatisfyPredicatePart extends InputValidatorPart { - final OnError devError; + final OnValidateError devError; // ignore: no-object-declaration, extra can be any object final Object? extra; diff --git a/glade_forms/lib/src/validator/string_validator.dart b/glade_forms/lib/src/validator/string_validator.dart index 0aacb4d..f983915 100644 --- a/glade_forms/lib/src/validator/string_validator.dart +++ b/glade_forms/lib/src/validator/string_validator.dart @@ -1,13 +1,14 @@ import 'package:glade_forms/src/validator/generic_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/validator_keys.dart'; class StringValidator extends GenericValidator { /// Checks that value is valid email address. /// /// Used Regex expression `^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`. void isEmail({ - OnError? devError, + OnValidateError? devError, Object? extra, bool allowEmpty = false, Object? key, @@ -24,7 +25,7 @@ class StringValidator extends GenericValidator { }, devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" is not in e-mail format', extra: extra, - key: key, + key: key ?? GladeErrorKeys.stringNotEmail, ); /// Checks that value is valid URL address. @@ -32,7 +33,7 @@ class StringValidator extends GenericValidator { /// [requireHttpScheme] - if true HTTP(S) is mandatory. void isUrl({ bool requireHttpScheme = false, - OnError? devError, + OnValidateError? devError, Object? extra, bool allowEmpty = false, Object? key, @@ -49,14 +50,14 @@ class StringValidator extends GenericValidator { }, devError: devError ?? (value, _) => 'Value "${value ?? 'NULL'}" is not valid URL address', extra: extra, - key: key, + key: key ?? GladeErrorKeys.stringNotUrl, ); /// Given value can't be empty string (or null). - void notEmpty({OnError? devError, Object? extra, Object? key}) => satisfy( + void notEmpty({OnValidateError? devError, Object? extra, Object? key}) => satisfy( (input, extra, __) => input.isNotEmpty, devError: devError ?? (_, __) => "Value can't be empty", extra: extra, - key: key, + key: key ?? GladeErrorKeys.stringEmpty, ); } diff --git a/glade_forms/lib/src/validator/validator_error/generic_validator_error.dart b/glade_forms/lib/src/validator/validator_error/generic_validator_error.dart index 0aac9ac..6796cee 100644 --- a/glade_forms/lib/src/validator/validator_error/generic_validator_error.dart +++ b/glade_forms/lib/src/validator/validator_error/generic_validator_error.dart @@ -1,33 +1,43 @@ import 'package:equatable/equatable.dart'; +import 'package:glade_forms/src/core/glade_input_error.dart'; import 'package:glade_forms/src/validator/validator_error/value_null_error.dart'; -typedef OnError = String Function(T? value, Object? extra); +/// When validation failed but we already have propert [T] value. +typedef OnValidateError = String Function(T? value, Object? extra); -abstract class GenericValidatorError extends Equatable { +// TODO(petr): Rename to Glade?. +abstract class GenericValidatorError extends GladeInputError with EquatableMixin { /// Error message when translation is not used. Useful for development. - final OnError devError; + final OnValidateError devError; // ignore: no-object-declaration, extra can be any object final Object? extra; - /// Can be used to identify concrete error when translating. - // ignore: no-object-declaration, locate key can be any object - final Object? key; + // /// Can be used to identify concrete error when translating. + // @override + // // ignore: no-object-declaration, key can be any object + // final Object? key; /// Value which triggered validation and returned this error. final T? value; - String get onErrorMessage => devError(value, extra); + @override + // ignore: no-object-declaration, error can be anything (but typically it is string) + Object get error => devError(value, extra); + + String get devErrorMessage => devError(value, extra); @override - List get props => [value, devError, extra, key]; + List get props => [value, devError, extra, key, error, isConversionError]; - const GenericValidatorError({ + GenericValidatorError({ required this.value, - required this.devError, + OnValidateError? devError, this.extra, - this.key, - }); + super.key, + }) : devError = devError ?? + ((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}) => ValueNullError(value: value, key: key, extra: extra); 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 7ab1954..1024e99 100644 --- a/glade_forms/lib/src/validator/validator_error/validator_error.dart +++ b/glade_forms/lib/src/validator/validator_error/validator_error.dart @@ -1,5 +1,4 @@ export 'generic_validator_error.dart'; export 'value_error.dart'; -export 'value_incorrect_format_error.dart'; export 'value_null_error.dart'; export 'value_satisfy_predicate_error.dart'; diff --git a/glade_forms/lib/src/validator/validator_error/validator_keys.dart b/glade_forms/lib/src/validator/validator_error/validator_keys.dart new file mode 100644 index 0000000..8388757 --- /dev/null +++ b/glade_forms/lib/src/validator/validator_error/validator_keys.dart @@ -0,0 +1,6 @@ +class GladeErrorKeys { + static const String stringEmpty = 'string-empty-error'; + static const String stringNotUrl = 'string-not-url'; + static const String stringNotEmail = 'string-not-email'; + static const String valueIsNull = 'value-is-null'; +} 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 eb4d930..02f82a5 100644 --- a/glade_forms/lib/src/validator/validator_error/value_error.dart +++ b/glade_forms/lib/src/validator/validator_error/value_error.dart @@ -1,7 +1,7 @@ import 'package:glade_forms/src/validator/validator_error/generic_validator_error.dart'; class ValueError extends GenericValidatorError { - const ValueError({ + ValueError({ required super.value, required super.devError, super.extra, diff --git a/glade_forms/lib/src/validator/validator_error/value_incorrect_format_error.dart b/glade_forms/lib/src/validator/validator_error/value_incorrect_format_error.dart deleted file mode 100644 index 4403ae6..0000000 --- a/glade_forms/lib/src/validator/validator_error/value_incorrect_format_error.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:glade_forms/src/validator/validator_error/generic_validator_error.dart'; - -class ValueIncorrectFormatError extends GenericValidatorError { - ValueIncorrectFormatError({ - required super.value, - OnError? devError, - super.extra, - super.key, - }) : super(devError: devError ?? (value, __) => 'Given value "${value ?? 'NULL'}" does not have valid format'); -} 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 f973f1a..78a7145 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 @@ -3,7 +3,7 @@ import 'package:glade_forms/src/validator/validator_error/generic_validator_erro class ValueNullError extends GenericValidatorError { ValueNullError({ required super.value, - OnError? devError, + OnValidateError? devError, super.extra, super.key, }) : super(devError: devError ?? (_, __) => "Value can't be null"); 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 4d92fb4..c06132d 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,7 +1,7 @@ import 'package:glade_forms/src/validator/validator_error/generic_validator_error.dart'; class ValueSatisfyPredicateError extends GenericValidatorError { - const ValueSatisfyPredicateError({ + ValueSatisfyPredicateError({ required super.value, required super.devError, super.extra, diff --git a/glade_forms/lib/src/validator/validator_errors.dart b/glade_forms/lib/src/validator/validator_errors.dart index ebec9f1..4038de9 100644 --- a/glade_forms/lib/src/validator/validator_errors.dart +++ b/glade_forms/lib/src/validator/validator_errors.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; -import 'package:glade_forms/src/core/generic_input.dart'; +import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/validator/validator_error/validator_error.dart'; class ValidatorErrors extends Equatable { - final GenericInput associatedInput; + final GladeInput associatedInput; final List> errors; bool get isValid => errors.isEmpty; diff --git a/glade_forms/lib/src/variants/optional_input.dart b/glade_forms/lib/src/variants/optional_input.dart deleted file mode 100644 index 3d4e4cb..0000000 --- a/glade_forms/lib/src/variants/optional_input.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/validator/generic_validator.dart'; - -/// Predefined GenericInput without any validations. -/// -/// Useful for input which allows null value without additional validations. -/// -/// In case of need of any validation use `GenericInput` directly. -class OptionalInput extends GenericInput { - OptionalInput.dirty({ - super.value, - super.translateError, - super.valueComparator, - super.inputKey, - super.initialValue, - }) : super.dirty(validatorInstance: GenericValidator().build()); - - OptionalInput.pure({ - super.value, - super.translateError, - super.valueComparator, - super.inputKey, - }) : super.pure(validatorInstance: GenericValidator().build()); - - @override - OptionalInput asDirty(T? value) => OptionalInput.dirty( - value: value, - translateError: translateError, - inputKey: inputKey, - valueComparator: valueComparator, - initialValue: initialValue, - ); - - @override - OptionalInput asPure(T? value) => OptionalInput.pure( - value: value, - translateError: translateError, - inputKey: inputKey, - valueComparator: valueComparator, - ); -} diff --git a/glade_forms/lib/src/variants/required_input.dart b/glade_forms/lib/src/variants/required_input.dart deleted file mode 100644 index c0edbe4..0000000 --- a/glade_forms/lib/src/variants/required_input.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/validator/generic_validator.dart'; - -/// Predefined GenericInput with predefined `notNull` validation. -/// -/// In case of need of any aditional validation use `GenericInput` directly. -class RequiredInput extends GenericInput { - RequiredInput.dirty({ - required super.value, - super.translateError, - super.valueComparator, - super.inputKey, - super.initialValue, - }) : super.dirty(validatorInstance: (GenericValidator()..notNull()).build()); - - RequiredInput.pure({ - required super.value, - super.translateError, - super.valueComparator, - super.inputKey, - }) : super.pure(validatorInstance: (GenericValidator()..notNull()).build()); - - @override - RequiredInput asDirty(T value) => RequiredInput.dirty( - value: value, - translateError: translateError, - inputKey: inputKey, - valueComparator: valueComparator, - initialValue: initialValue, - ); - - @override - RequiredInput asPure(T value) => RequiredInput.pure( - value: value, - translateError: translateError, - inputKey: inputKey, - valueComparator: valueComparator, - ); -} diff --git a/glade_forms/lib/src/variants/string_input.dart b/glade_forms/lib/src/variants/string_input.dart index c0f3e64..89805bf 100644 --- a/glade_forms/lib/src/variants/string_input.dart +++ b/glade_forms/lib/src/variants/string_input.dart @@ -1,15 +1,13 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/core/glade_input_base.dart'; -import 'package:glade_forms/src/forms/text_form_field_input_validator_mixin.dart'; +import 'package:glade_forms/src/core/core.dart'; import 'package:glade_forms/src/validator/generic_validator_instance.dart'; import 'package:glade_forms/src/validator/string_validator.dart'; -class StringInput extends GenericInput with TextFormFieldInputValidatorMixin { +class StringInput extends GladeInput { factory StringInput.create( GenericValidatorInstance Function(StringValidator validator) validatorFactory, { String? defaultValue, bool pure = true, - TranslateError? translateError, + ErrorTranslator? translateError, String? inputKey, }) { final instance = validatorFactory(StringValidator()); @@ -36,14 +34,18 @@ class StringInput extends GenericInput with TextFormFieldInputValidatorM super.initialValue, super.inputKey, super.valueComparator, - }) : super.dirty(value: value ?? ''); + super.dependencies, + super.defaultTranslations, + }) : super.dirty(value ?? ''); /// String input which allows empty (and null) values without any additional validations. factory StringInput.optional({ String? defaultValue, bool pure = true, - TranslateError? translateError, + ErrorTranslator? translateError, String? inputKey, + InputDependenciesFactory? dependencies, + DefaultTranslations? defaultTranslations, }) { final instance = StringValidator().build(); @@ -53,12 +55,16 @@ class StringInput extends GenericInput with TextFormFieldInputValidatorM value: defaultValue ?? '', translateError: translateError, inputKey: inputKey, + dependencies: dependencies, + defaultTranslations: defaultTranslations, ) : StringInput.dirty( validatorInstance: instance, value: defaultValue ?? '', translateError: translateError, inputKey: inputKey, + dependencies: dependencies, + defaultTranslations: defaultTranslations, ); } @@ -68,15 +74,20 @@ class StringInput extends GenericInput with TextFormFieldInputValidatorM super.translateError, super.inputKey, super.valueComparator, - }) : super.pure(value: value ?? ''); + super.dependencies, + super.initialValue, + super.defaultTranslations, + }) : super.pure(value ?? ''); /// String input with predefined `notEmpty` validation rule. factory StringInput.required({ GenericValidatorInstance Function(StringValidator validator)? validatorFactory, String? defaultValue, bool pure = true, - TranslateError? translateError, + ErrorTranslator? translateError, String? inputKey, + InputDependenciesFactory? dependencies, + DefaultTranslations? defaultTranslations, }) { final instance = validatorFactory?.call(StringValidator()..notEmpty()) ?? (StringValidator()..notEmpty()).build(); @@ -86,12 +97,16 @@ class StringInput extends GenericInput with TextFormFieldInputValidatorM value: defaultValue ?? '', translateError: translateError, inputKey: inputKey, + dependencies: dependencies, + defaultTranslations: defaultTranslations, ) : StringInput.dirty( validatorInstance: instance, value: defaultValue ?? '', translateError: translateError, inputKey: inputKey, + dependencies: dependencies, + defaultTranslations: defaultTranslations, ); } @@ -103,6 +118,8 @@ class StringInput extends GenericInput with TextFormFieldInputValidatorM initialValue: initialValue, inputKey: inputKey, valueComparator: valueComparator, + dependencies: dependencies, + defaultTranslations: defaultTranslations, ); @override @@ -112,5 +129,7 @@ class StringInput extends GenericInput with TextFormFieldInputValidatorM translateError: translateError, inputKey: inputKey, valueComparator: valueComparator, + dependencies: dependencies, + defaultTranslations: defaultTranslations, ); } diff --git a/glade_forms/lib/src/variants/variants.dart b/glade_forms/lib/src/variants/variants.dart new file mode 100644 index 0000000..55756d4 --- /dev/null +++ b/glade_forms/lib/src/variants/variants.dart @@ -0,0 +1 @@ +export 'string_input.dart'; diff --git a/glade_forms/lib/src/widgets/glade_form_builder.dart b/glade_forms/lib/src/widgets/glade_form_builder.dart new file mode 100644 index 0000000..05a40c1 --- /dev/null +++ b/glade_forms/lib/src/widgets/glade_form_builder.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:glade_forms/src/core/core.dart'; +import 'package:glade_forms/src/widgets/glade_form_provider.dart'; +import 'package:provider/provider.dart'; + +typedef GladeFormWidgetBuilder = Widget Function(BuildContext context, M model); + +class GladeFormBuilder extends StatelessWidget { + final CreateModelFunction? create; + final M? value; + final GladeFormWidgetBuilder builder; + + factory GladeFormBuilder({ + required CreateModelFunction create, + required GladeFormWidgetBuilder builder, + Key? key, + }) => + GladeFormBuilder._(builder: builder, create: create, key: key); + + const GladeFormBuilder._({ + required this.builder, + super.key, + this.create, + this.value, + }); + + factory GladeFormBuilder.value({ + required GladeFormWidgetBuilder builder, + required M value, + Key? key, + }) => + GladeFormBuilder._(builder: builder, value: value, key: key); + + @override + Widget build(BuildContext context) { + if (create case final createFn?) { + return GladeFormProvider( + create: createFn, + child: Consumer( + builder: (context, model, _) => builder(context, model), + ), + ); + } else if (value case final modelValue?) { + return GladeFormProvider.value( + value: modelValue, + child: Consumer( + builder: (context, model, _) => builder(context, model), + ), + ); + } + + return Consumer( + builder: (context, model, _) => builder(context, model), + ); + } +} diff --git a/glade_forms/lib/src/widgets/glade_form_provider.dart b/glade_forms/lib/src/widgets/glade_form_provider.dart new file mode 100644 index 0000000..5706771 --- /dev/null +++ b/glade_forms/lib/src/widgets/glade_form_provider.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; +import 'package:glade_forms/src/core/mutable_generic_model.dart'; +import 'package:provider/provider.dart'; + +typedef CreateModelFunction = M Function(BuildContext context); + +class GladeFormProvider extends StatelessWidget { + final CreateModelFunction? create; + final M? value; + final Widget child; + + factory GladeFormProvider({required CreateModelFunction create, required Widget child}) => + GladeFormProvider._(create: create, child: child); + + const GladeFormProvider._({ + required this.child, + this.create, + this.value, + super.key, + }); + + factory GladeFormProvider.value({required M value, required Widget child}) => + GladeFormProvider._(value: value, child: child); + + @override + Widget build(BuildContext context) { + if (value case final x?) { + return ChangeNotifierProvider.value(value: x, child: child); + } + + return ChangeNotifierProvider(create: create!, child: child); + } +} diff --git a/glade_forms/lib/src/widgets/widgets.dart b/glade_forms/lib/src/widgets/widgets.dart new file mode 100644 index 0000000..5289e8c --- /dev/null +++ b/glade_forms/lib/src/widgets/widgets.dart @@ -0,0 +1,2 @@ +export 'glade_form_builder.dart'; +export 'glade_form_provider.dart'; diff --git a/glade_forms/pubspec.yaml b/glade_forms/pubspec.yaml index 1b2cae7..59005c7 100644 --- a/glade_forms/pubspec.yaml +++ b/glade_forms/pubspec.yaml @@ -4,8 +4,8 @@ version: 1.0.0 # repository: https://github.com/my_org/my_repo environment: - sdk: ^3.0.6 - flutter: ">=1.17.0" + sdk: ^3.0.0 + flutter: ">=3.0.0" # Add regular dependencies here. dependencies: @@ -14,6 +14,7 @@ dependencies: flutter: sdk: flutter meta: ^1.9.1 + provider: ^6.0.5 dev_dependencies: netglade_analysis: ^4.1.0 diff --git a/glade_forms/test/deps_test.dart b/glade_forms/test/deps_test.dart index 71e1363..dd762d2 100644 --- a/glade_forms/test/deps_test.dart +++ b/glade_forms/test/deps_test.dart @@ -1,64 +1,55 @@ -import 'package:glade_forms/src/core/generic_input.dart'; -import 'package:glade_forms/src/variants/required_input.dart'; +import 'package:glade_forms/src/core/core.dart'; import 'package:test/test.dart'; -class Model { - late final RequiredInput vipContent; +class _Model { + late final GladeInput vipContent; - late final GenericInput age; + late final GladeInput age; - Model({bool enableVip = false}) { - vipContent = RequiredInput.pure(value: enableVip, inputKey: 'vip'); - age = GenericInput.create( + _Model({bool enableVip = false}) { + vipContent = GladeInput.required(value: enableVip, inputKey: 'vip'); + age = GladeInput.create( (validator) => (validator ..satisfy((value, extra, dependencies) { final vipContentInput = dependencies.byKey('vip'); if (vipContentInput == null) { - print('dep null'); - return true; } - print(vipContent.value); - if (vipContent.value) return value >= 18; return value < 18; })) .build(), value: 0, - dependencies: [vipContent], + dependencies: () => [vipContent], ); } - Model update(bool vip) => Model(enableVip: vip); + _Model update(bool vip) => _Model(enableVip: vip); } void main() { test('Deps test - local mutable properties', () { - var vipContent = RequiredInput.pure(value: false, inputKey: 'vip'); + var vipContent = GladeInput.required(value: false, inputKey: 'vip'); - final a = GenericInput.create( + final a = GladeInput.create( (validator) => (validator ..satisfy((value, extra, dependencies) { final vipContentInput = dependencies.byKey('vip'); if (vipContentInput == null) { - print('dep null'); - return true; } - print(vipContent.value); - if (vipContent.value) return value >= 18; return value < 18; })) .build(), value: 0, - dependencies: [vipContent], + dependencies: () => [vipContent], ); expect(a.isValid, isTrue); @@ -68,8 +59,36 @@ void main() { expect(a.isValid, isFalse); }); + test('Deps test - local mutable inputs', () { + final vipContent = GladeInput.required(value: false, inputKey: 'vip'); + + final a = GladeInput.create( + (validator) => (validator + ..satisfy((value, extra, dependencies) { + final vipContentInput = dependencies.byKey('vip'); + + if (vipContentInput == null) { + return true; + } + + if (vipContent.value) return value >= 18; + + return value < 18; + })) + .build(), + value: 0, + dependencies: () => [vipContent], + ); + + expect(a.isValid, isTrue); + + vipContent.value = true; + + expect(a.isValid, isFalse); + }); + test('Deps test - immutable model', () { - var model = Model(); + var model = _Model(); expect(model.age.isValid, isTrue); diff --git a/melos.yaml b/melos.yaml index 68526a3..f341767 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,7 +1,7 @@ name: glade_forms_workspace packages: - - glade_forms/** + - glade_forms - examples/** command: @@ -9,6 +9,9 @@ command: usePubspecOverrides: true scripts: + setup: + run: melos exec -- fvm flutter pub get + # ANALYZING lint:all: @@ -30,3 +33,11 @@ scripts: packageFilters: dirExists: test description: Run all Dart tests. + + # Gallery + gallery_setup: + 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 diff --git a/pubspec.yaml b/pubspec.yaml index fbe7d3b..c2e08a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: glade_forms_workspace publish_to: "none" environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=2.18.0 <4.0.0" dev_dependencies: melos: ^3.0.0