From 7c252464a698f713f86953c55197603b75da7924 Mon Sep 17 00:00:00 2001 From: Goxiaoy Date: Fri, 28 Jul 2023 00:06:11 +0800 Subject: [PATCH 1/2] multiple_text add InputDecoration --- example/ios/Podfile.lock | 41 +++++++++++++++++++++ lib/ui/elements/multiple_text.dart | 57 +++++++++++++++++++----------- pubspec.yaml | 1 + 3 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 example/ios/Podfile.lock diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 00000000..4a8e5d4d --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - "app_settings (3.0.0+1)": + - Flutter + - Flutter (1.0.0) + - image_picker_ios (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - video_player_avfoundation (0.0.1): + - Flutter + +DEPENDENCIES: + - app_settings (from `.symlinks/plugins/app_settings/ios`) + - Flutter (from `Flutter`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) + +EXTERNAL SOURCES: + app_settings: + :path: ".symlinks/plugins/app_settings/ios" + Flutter: + :path: Flutter + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" + +SPEC CHECKSUMS: + app_settings: d103828c9f5d515c4df9ee754dabd443f7cedcf3 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + video_player_avfoundation: 81e49bb3d9fb63dccf9fa0f6d877dc3ddbeac126 + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.12.1 diff --git a/lib/ui/elements/multiple_text.dart b/lib/ui/elements/multiple_text.dart index ff3bd742..10e7bf3a 100644 --- a/lib/ui/elements/multiple_text.dart +++ b/lib/ui/elements/multiple_text.dart @@ -5,33 +5,50 @@ import 'package:flutter_survey_js/ui/reactive/reactive_nested_form.dart'; import 'package:flutter_survey_js/ui/survey_configuration.dart'; import 'package:flutter_survey_js_model/flutter_survey_js_model.dart' as s; import 'package:reactive_forms/reactive_forms.dart'; - +import 'package:async/async.dart'; Widget multipleTextBuilder(BuildContext context, s.Elementbase element, {ElementConfiguration? configuration}) { final e = element as s.Multipletext; final texts = (e.items?.toList() ?? []).map(toText).toList(); + return ReactiveNestedForm( formControlName: e.name, - child: ListView.separated( - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - itemCount: texts.length, - itemBuilder: (BuildContext context, int index) { - final res = SurveyConfiguration.of(context)! - .factory - .resolve(context, texts[index]); - return index == 0 - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: res, - ) - : res; - }, - separatorBuilder: (BuildContext context, int index) { - return SurveyConfiguration.of(context)!.separatorBuilder(context); - }, - ).wrapQuestionTitle(context, element, configuration: configuration)); + child: Builder(builder: (context) { + final control = ReactiveForm.of(context) as FormGroup; + final effectiveDecoration = const InputDecoration() + .applyDefaults(Theme.of(context).inputDecorationTheme); + return StreamBuilder( + stream: + StreamGroup.merge([control.touchChanges, control.statusChanged]), + builder: (context, _) { + return InputDecorator( + decoration: effectiveDecoration.copyWith( + errorText: getErrorTextFromFormControl(context, control)), + child: ListView.separated( + physics: const ClampingScrollPhysics(), + shrinkWrap: true, + itemCount: texts.length, + itemBuilder: (BuildContext context, int index) { + final res = SurveyConfiguration.of(context)! + .factory + .resolve(context, texts[index]); + return index == 0 + ? Padding( + padding: const EdgeInsets.only(top: 8.0), + child: res, + ) + : res; + }, + separatorBuilder: (BuildContext context, int index) { + return SurveyConfiguration.of(context)! + .separatorBuilder(context); + }, + ), + ); + }, + ); + }).wrapQuestionTitle(context, element, configuration: configuration)); } AbstractControl multipleTextControlBuilder( diff --git a/pubspec.yaml b/pubspec.yaml index d79e36ab..5b7248ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: built_value: ">=8.4.0 <9.0.0" built_collection: ">=5.1.1 <6.0.0" intl: ^0.18.0 + async: ^2.11.0 dev_dependencies: flutter_test: From 953fd294ab335545a74ef5933e75615b51b6aada Mon Sep 17 00:00:00 2001 From: David Chopin Date: Wed, 26 Jul 2023 14:59:00 -0500 Subject: [PATCH 2/2] Added required validation to MultipleText - DC --- lib/ui/validators.dart | 33 ++++-- test/questions/multiple_text_test.dart | 149 +++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 8 deletions(-) diff --git a/lib/ui/validators.dart b/lib/ui/validators.dart index b8ffa062..62c9db66 100644 --- a/lib/ui/validators.dart +++ b/lib/ui/validators.dart @@ -23,17 +23,34 @@ class NonEmptyValidator extends Validator { static ValidatorFunction get get => NonEmptyValidator().validate; } -List questionToValidators(s.Question question) { - return surveyToValidators( - isRequired: question.isRequired, - validators: question.validators?.map((p) => p.realValidator).toList()); +class MultipleTextNonEmptyValidator extends Validator { + @override + Map? validate(AbstractControl control) { + final error = {ValidationMessage.required: true}; + + bool hasError = true; + + for (final value in control.value.values) { + if (value != null && value.toString().trim().isNotEmpty) { + hasError = false; + } + } + + return hasError ? error : null; + } + + static ValidatorFunction get get => MultipleTextNonEmptyValidator().validate; } -List surveyToValidators( - {bool? isRequired, List? validators}) { +List questionToValidators(s.Question question) { final res = []; - if (isRequired == true) { - res.add(NonEmptyValidator.get); + final validators = question.validators?.map((p) => p.realValidator).toList(); + if (question.isRequired == true) { + if (question is s.Multipletext) { + res.add(MultipleTextNonEmptyValidator.get); + } else { + res.add(NonEmptyValidator.get); + } } if (validators != null) { for (var value in validators) { diff --git a/test/questions/multiple_text_test.dart b/test/questions/multiple_text_test.dart index 0f15f52b..6dad2c84 100644 --- a/test/questions/multiple_text_test.dart +++ b/test/questions/multiple_text_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_survey_js/survey.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:reactive_forms/reactive_forms.dart'; void main() { // 单一的测试 @@ -72,4 +73,152 @@ void main() { expect(find.text(placeholder1), findsOneWidget); expect(find.text(placeholder2), findsOneWidget); }); + + testWidgets('is not valid when required and no item has a value', + (WidgetTester tester) async { + final SurveyController surveyController = SurveyController(); + bool onErrorsCalled = false; + final s = surveyFromJson( + { + "pages": [ + { + "name": "page1", + "elements": [ + { + "type": "multipletext", + "name": "question1", + "title": "Multiple text", + "isRequired": true, + "items": [ + {"name": "text1"}, + {"name": "text2"} + ] + } + ] + } + ] + }, + )!; + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: const [ + MultiAppLocalizationsDelegate(), + ], + home: Material( + child: SurveyWidget( + survey: s, + controller: surveyController, + onErrors: (data) { + onErrorsCalled = true; + }), + ), + ), + ); + await tester.pump(); + await tester.idle(); + + surveyController.submit(); + await tester.pumpAndSettle(); + expect(onErrorsCalled, isTrue); + }); + + testWidgets('is valid when required and any item has a value', + (WidgetTester tester) async { + final SurveyController surveyController = SurveyController(); + bool onErrorsCalled = false; + final s = surveyFromJson( + { + "pages": [ + { + "name": "page1", + "elements": [ + { + "type": "multipletext", + "name": "question1", + "title": "Multiple text", + "isRequired": true, + "items": [ + {"name": "text1"}, + {"name": "text2"} + ] + } + ] + } + ] + }, + )!; + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: const [ + MultiAppLocalizationsDelegate(), + ], + home: Material( + child: SurveyWidget( + survey: s, + controller: surveyController, + onErrors: (data) { + onErrorsCalled = true; + }), + ), + ), + ); + await tester.pump(); + await tester.idle(); + + await tester.enterText(find.byType(ReactiveTextField).first, 'some answer'); + + surveyController.submit(); + await tester.pumpAndSettle(); + expect(onErrorsCalled, isFalse); + }); + + testWidgets('is not valid when required and all items are null or empty', + (WidgetTester tester) async { + final SurveyController surveyController = SurveyController(); + bool onErrorsCalled = false; + final s = surveyFromJson( + { + "pages": [ + { + "name": "page1", + "elements": [ + { + "type": "multipletext", + "name": "question1", + "title": "Multiple text", + "isRequired": true, + "items": [ + {"name": "text1"}, + {"name": "text2"} + ] + } + ] + } + ] + }, + )!; + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: const [ + MultiAppLocalizationsDelegate(), + ], + home: Material( + child: SurveyWidget( + survey: s, + controller: surveyController, + onErrors: (data) { + onErrorsCalled = true; + }), + ), + ), + ); + await tester.pump(); + await tester.idle(); + + await tester.enterText(find.byType(ReactiveTextField).first, ' '); + + surveyController.submit(); + await tester.pumpAndSettle(); + expect(onErrorsCalled, isTrue); + }); }