Merge pull request #13 from Iconica-Development/feat/buttons_and_fields

FInish flutter_registration for traffic university
This commit is contained in:
mike doornenbal 2024-02-14 11:36:48 +01:00 committed by GitHub
commit 0472ebc4c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 518 additions and 197 deletions

View file

@ -3,6 +3,21 @@ SPDX-FileCopyrightText: 2022 Iconica
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
--> -->
# 2.0.0
- feat(buttons): Added the possiblity to only have a next button by return zero on the previous button builder
- feat: exposed input decoration in AuthTextField
- feat: added title widget and login button builder
- feat(bool): Add a boolean field. Can be used for accepting terms and conditions
- feat(pass): Add dedicated password screen that manages state internally
- fix: Small refactor and brought back the normal alignment for the screens
- fix: Fixed alignment and spacing when opening keyboard
- feat: add auth drop down field
- fix: added step to button builders
- fix: exported auth_pass_field
- feat: added validation to disable next button
- feat(auth-screen): add flexible spacing between fields
- fix(keyboard-focus): add unfocus for onPrevious
# 1.2.0 # 1.2.0
- feat: Added the ability to have an async register function so you can call it asynchronous. - feat: Added the ability to have an async register function so you can call it asynchronous.

View file

@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View file

@ -2,27 +2,81 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart'; import 'package:flutter_registration/flutter_registration.dart';
import 'example_registration_repository.dart'; import 'example_registration_repository.dart';
void main() { void main() {
runApp( runApp(
const MaterialApp( MaterialApp(
home: FlutterRegistrationDemo(), theme: ThemeData(
inputDecorationTheme: const InputDecorationTheme(
errorStyle: TextStyle(color: Colors.red),
),
),
home: const FlutterRegistrationDemo(),
), ),
); );
} }
class FlutterRegistrationDemo extends StatelessWidget { class FlutterRegistrationDemo extends StatefulWidget {
const FlutterRegistrationDemo({Key? key}) : super(key: key); const FlutterRegistrationDemo({Key? key}) : super(key: key);
@override
State<FlutterRegistrationDemo> createState() =>
_FlutterRegistrationDemoState();
}
class _FlutterRegistrationDemoState extends State<FlutterRegistrationDemo> {
late List<AuthStep> steps;
@override
void initState() {
super.initState();
steps = RegistrationOptions.getDefaultSteps();
steps[1].fields.add(
AuthBoolField(
name: 'termsConditions',
widgetType: BoolWidgetType.checkbox,
validators: [
(value) {
if (value == null || !value) {
return 'Required';
}
return null;
},
],
rightWidget: Text.rich(
TextSpan(
text: 'I agree with the ',
children: <TextSpan>[
TextSpan(
text: 'terms & conditions',
style: const TextStyle(
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
debugPrint('Open terms and conditions');
},
),
],
),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RegistrationScreen( return RegistrationScreen(
registrationOptions: RegistrationOptions( registrationOptions: RegistrationOptions(
registrationRepository: ExampleRegistrationRepository(), registrationRepository: ExampleRegistrationRepository(),
registrationSteps: RegistrationOptions.getDefaultSteps(), registrationSteps: steps,
afterRegistration: () { afterRegistration: () {
debugPrint('Registered!'); debugPrint('Registered!');
}, },

View file

@ -37,18 +37,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.2" version: "1.18.0"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -62,14 +62,23 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_input_library:
dependency: transitive
description:
path: "."
ref: "3.0.0"
resolved-ref: "7d1880b8e348435fc8dc2d3a11f936b224cbd5b7"
url: "https://github.com/Iconica-Development/flutter_input_library"
source: git
version: "3.0.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.3"
flutter_localizations: flutter_localizations:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -81,7 +90,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "0.5.0" version: "1.2.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -99,10 +108,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "5cfd6509652ff5e7fe149b6df4859e687fca9048437857cb2e65c8d780f396e3" sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -123,10 +132,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -152,18 +161,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@ -184,10 +193,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -200,10 +209,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.4-beta" version: "0.3.0"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=1.17.0" flutter: ">=1.17.0"

View file

@ -10,5 +10,10 @@ export 'src/model/auth_exception.dart';
export 'src/model/auth_field.dart'; export 'src/model/auth_field.dart';
export 'src/model/auth_step.dart'; export 'src/model/auth_step.dart';
export 'src/model/auth_text_field.dart'; export 'src/model/auth_text_field.dart';
export 'src/model/auth_bool_field.dart';
export 'src/model/auth_drop_down.dart';
export 'src/model/auth_pass_field.dart';
export 'src/registration_screen.dart'; export 'src/registration_screen.dart';
export 'src/service/registration_repository.dart'; export 'src/service/registration_repository.dart';
export 'package:flutter_input_library/flutter_input_library.dart'
show BoolWidgetType;

View file

@ -9,7 +9,7 @@ import 'package:flutter_registration/flutter_registration.dart';
class AuthScreen extends StatefulWidget { class AuthScreen extends StatefulWidget {
const AuthScreen({ const AuthScreen({
required this.title, required this.appBarTitle,
required this.steps, required this.steps,
required this.submitBtnTitle, required this.submitBtnTitle,
required this.nextBtnTitle, required this.nextBtnTitle,
@ -19,12 +19,18 @@ class AuthScreen extends StatefulWidget {
this.customBackgroundColor, this.customBackgroundColor,
this.nextButtonBuilder, this.nextButtonBuilder,
this.previousButtonBuilder, this.previousButtonBuilder,
this.titleWidget,
this.loginButton,
this.titleFlex,
this.formFlex,
this.beforeTitleFlex,
this.afterTitleFlex,
super.key, super.key,
}) : assert(steps.length > 0, 'At least one step is required'); }) : assert(steps.length > 0, 'At least one step is required');
final String title; final String appBarTitle;
final Future<void> Function({ final Future<void> Function({
required HashMap<String, String> values, required HashMap<String, dynamic> values,
required void Function(int? pageToReturn) onError, required void Function(int? pageToReturn) onError,
}) onFinish; }) onFinish;
final List<AuthStep> steps; final List<AuthStep> steps;
@ -33,8 +39,16 @@ class AuthScreen extends StatefulWidget {
final String previousBtnTitle; final String previousBtnTitle;
final AppBar? customAppBar; final AppBar? customAppBar;
final Color? customBackgroundColor; final Color? customBackgroundColor;
final Widget Function(Future<void> Function(), String)? nextButtonBuilder; final Widget Function(Future<void> Function()? onPressed, String label,
final Widget Function(VoidCallback, String)? previousButtonBuilder; int step, bool enabled)? nextButtonBuilder;
final Widget? Function(VoidCallback onPressed, String label, int step)?
previousButtonBuilder;
final Widget? titleWidget;
final Widget? loginButton;
final int? titleFlex;
final int? formFlex;
final int? beforeTitleFlex;
final int? afterTitleFlex;
@override @override
State<AuthScreen> createState() => _AuthScreenState(); State<AuthScreen> createState() => _AuthScreenState();
@ -45,14 +59,17 @@ class _AuthScreenState extends State<AuthScreen> {
final _pageController = PageController(); final _pageController = PageController();
final _animationDuration = const Duration(milliseconds: 300); final _animationDuration = const Duration(milliseconds: 300);
final _animationCurve = Curves.ease; final _animationCurve = Curves.ease;
bool _formValid = false;
AppBar get _appBar => AppBar get _appBar =>
widget.customAppBar ?? widget.customAppBar ??
AppBar( AppBar(
title: Text(widget.title), title: Text(widget.appBarTitle),
); );
void onPrevious() { void onPrevious() {
FocusScope.of(context).unfocus();
_validate(_pageController.page!.toInt() - 1);
_pageController.previousPage( _pageController.previousPage(
duration: _animationDuration, duration: _animationDuration,
curve: _animationCurve, curve: _animationCurve,
@ -69,12 +86,11 @@ class _AuthScreenState extends State<AuthScreen> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (widget.steps.last == step) { if (widget.steps.last == step) {
var values = HashMap<String, String>(); var values = HashMap<String, dynamic>();
for (var step in widget.steps) { for (var step in widget.steps) {
for (var field in step.fields) { for (var field in step.fields) {
values[field.name] = values[field.name] = field.value;
(field as AuthTextField).textController.value.text;
} }
} }
@ -89,6 +105,7 @@ class _AuthScreenState extends State<AuthScreen> {
return; return;
} else { } else {
_validate(_pageController.page!.toInt() + 1);
_pageController.nextPage( _pageController.nextPage(
duration: _animationDuration, duration: _animationDuration,
curve: _animationCurve, curve: _animationCurve,
@ -96,6 +113,29 @@ class _AuthScreenState extends State<AuthScreen> {
} }
} }
void _validate(int currentPage) {
bool isStepValid = true;
// Loop through each field in the current step
for (var field in widget.steps[currentPage].fields) {
for (var validator in field.validators) {
String? validationResult = validator(field.value);
if (validationResult != null) {
// If any validator returns an error, mark step as invalid and break
isStepValid = false;
break;
}
}
if (!isStepValid) {
break; // No need to check further fields if one is already invalid
}
}
setState(() {
_formValid = isStepValid;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -107,100 +147,150 @@ class _AuthScreenState extends State<AuthScreen> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
controller: _pageController, controller: _pageController,
children: <Widget>[ children: <Widget>[
for (AuthStep step in widget.steps) for (var i = 0; i < widget.steps.length; i++)
Column( Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Flexible( if (widget.titleWidget != null) ...[
child: Center( Expanded(
child: ListView( flex: widget.titleFlex ?? 1,
physics: const ClampingScrollPhysics(), child: Column(
shrinkWrap: true, mainAxisAlignment: MainAxisAlignment.start,
padding: const EdgeInsets.symmetric( mainAxisSize: MainAxisSize.min,
vertical: 8.0,
horizontal: 30.0,
),
children: [ children: [
for (AuthField field in step.fields) Expanded(
Align( flex: widget.beforeTitleFlex ?? 3,
child: Column( child: Container(),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ widget.titleWidget!,
if (field.title != null) ...[ Expanded(
field.title!, flex: widget.afterTitleFlex ?? 2,
], child: Container(),
field.build(), ),
],
),
)
], ],
), ),
), ),
), ],
Padding( Expanded(
padding: const EdgeInsets.only( flex: widget.formFlex ?? 3,
top: 15.0, child: Column(
bottom: 30.0,
left: 30.0,
right: 30.0,
),
child: Row(
mainAxisAlignment: widget.steps.first != step
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
children: [ children: [
if (widget.steps.first != step) for (AuthField field in widget.steps[i].fields)
widget.previousButtonBuilder?.call( Align(
onPrevious, child: Column(
widget.previousBtnTitle, crossAxisAlignment: CrossAxisAlignment.start,
) ?? children: [
ElevatedButton( if (field.title != null) ...[
onPressed: onPrevious, field.title!,
child: Row(
children: [
const Icon(
Icons.arrow_back,
size: 18,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(widget.previousBtnTitle),
),
],
),
),
widget.nextButtonBuilder?.call(
() async {
await onNext(step);
},
widget.steps.last == step
? widget.submitBtnTitle
: widget.nextBtnTitle,
) ??
ElevatedButton(
onPressed: () async {
await onNext(step);
},
child: Row(
children: [
Text(
widget.steps.last == step
? widget.submitBtnTitle
: widget.nextBtnTitle,
),
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Icon(
Icons.arrow_forward,
size: 18,
),
),
], ],
), field.build(context, () {
_validate(i);
}),
],
), ),
),
], ],
), ),
) ),
Padding(
padding: EdgeInsets.only(
top: 15.0,
bottom: 30.0,
left: widget.previousButtonBuilder == null &&
widget.steps.first != widget.steps[i]
? 30.0
: 0.0,
right: widget.nextButtonBuilder == null &&
widget.previousButtonBuilder == null
? 30.0
: 0.0,
),
child: Column(
children: [
Row(
mainAxisAlignment:
(widget.previousButtonBuilder != null &&
widget.previousButtonBuilder?.call(
onPrevious,
widget.previousBtnTitle,
i,
) ==
null)
? MainAxisAlignment.spaceAround
: widget.steps.first != widget.steps[i]
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
children: [
if (widget.previousButtonBuilder == null) ...[
if (widget.steps.first != widget.steps[i])
ElevatedButton(
onPressed: onPrevious,
child: Row(
children: [
const Icon(
Icons.arrow_back,
size: 18,
),
Padding(
padding:
const EdgeInsets.only(left: 4.0),
child: Text(widget.previousBtnTitle),
),
],
),
),
] else if (widget.previousButtonBuilder?.call(
onPrevious, widget.previousBtnTitle, i) !=
null) ...[
widget.previousButtonBuilder!
.call(onPrevious, widget.previousBtnTitle, i)!
],
widget.nextButtonBuilder?.call(
!_formValid
? null
: () async {
await onNext(widget.steps[i]);
},
widget.steps.last == widget.steps[i]
? widget.submitBtnTitle
: widget.nextBtnTitle,
i,
_formValid,
) ??
ElevatedButton(
onPressed: !_formValid
? null
: () async {
await onNext(widget.steps[i]);
},
child: Row(
children: [
Text(
widget.steps.last == widget.steps[i]
? widget.submitBtnTitle
: widget.nextBtnTitle,
),
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Icon(
Icons.arrow_forward,
size: 18,
),
),
],
),
),
],
),
if (widget.loginButton != null)
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: widget.loginButton!,
),
],
),
),
], ],
), ),
], ],

View file

@ -18,6 +18,8 @@ class RegistrationOptions {
this.nextButtonBuilder, this.nextButtonBuilder,
this.previousButtonBuilder, this.previousButtonBuilder,
this.backgroundColor, this.backgroundColor,
this.titleWidget,
this.loginButton,
}); });
final RegistrationTranslations registrationTranslations; final RegistrationTranslations registrationTranslations;
@ -26,11 +28,13 @@ class RegistrationOptions {
final VoidCallback afterRegistration; final VoidCallback afterRegistration;
final RegistrationRepository registrationRepository; final RegistrationRepository registrationRepository;
final AppBar Function(String title)? customAppbarBuilder; final AppBar Function(String title)? customAppbarBuilder;
final Widget Function(Future<void> Function() onPressed, String label)? final Widget Function(Future<void> Function()? onPressed, String label,
nextButtonBuilder; int step, bool enabled)? nextButtonBuilder;
final Widget Function(VoidCallback onPressed, String label)? final Widget? Function(VoidCallback onPressed, String label, int step)?
previousButtonBuilder; previousButtonBuilder;
final Color? backgroundColor; final Color? backgroundColor;
Widget? titleWidget;
Widget? loginButton;
static List<AuthStep> getDefaultSteps({ static List<AuthStep> getDefaultSteps({
TextEditingController? emailController, TextEditingController? emailController,
@ -69,8 +73,10 @@ class RegistrationOptions {
), ),
), ),
), ),
label: labelBuilder?.call(translations.defaultEmailLabel), textFieldDecoration: InputDecoration(
hintText: translations.defaultEmailHint, label: labelBuilder?.call(translations.defaultEmailLabel),
hintText: translations.defaultEmailHint,
),
textStyle: textStyle, textStyle: textStyle,
validators: [ validators: [
(email) => (email == null || email.isEmpty) (email) => (email == null || email.isEmpty)
@ -87,7 +93,7 @@ class RegistrationOptions {
), ),
AuthStep( AuthStep(
fields: [ fields: [
AuthTextField( AuthPassField(
name: 'password1', name: 'password1',
textEditingController: pass1Controller, textEditingController: pass1Controller,
title: titleBuilder?.call( title: titleBuilder?.call(
@ -105,10 +111,11 @@ class RegistrationOptions {
), ),
), ),
), ),
label: labelBuilder?.call(translations.defaultPassword1Label), textFieldDecoration: InputDecoration(
hintText: translations.defaultPassword1Hint, label: labelBuilder?.call(translations.defaultPassword1Label),
hintText: translations.defaultPassword1Hint,
),
textStyle: textStyle, textStyle: textStyle,
obscureText: true,
validators: [ validators: [
(value) => (value == null || value.isEmpty) (value) => (value == null || value.isEmpty)
? translations.defaultPassword1ValidatorMessage ? translations.defaultPassword1ValidatorMessage
@ -117,12 +124,8 @@ class RegistrationOptions {
onChange: (value) { onChange: (value) {
password1 = value; password1 = value;
}, },
hidden: pass1Hidden,
onPassChanged: (value) {
passHideOnChange?.call(true, value);
},
), ),
AuthTextField( AuthPassField(
name: 'password2', name: 'password2',
textEditingController: pass2Controller, textEditingController: pass2Controller,
title: titleBuilder?.call( title: titleBuilder?.call(
@ -140,10 +143,11 @@ class RegistrationOptions {
), ),
), ),
), ),
label: labelBuilder?.call(translations.defaultPassword2Label), textFieldDecoration: InputDecoration(
hintText: translations.defaultPassword2Hint, label: labelBuilder?.call(translations.defaultPassword2Label),
hintText: translations.defaultPassword2Hint,
),
textStyle: textStyle, textStyle: textStyle,
obscureText: true,
validators: [ validators: [
(value) { (value) {
if (pass1Controller != null) { if (pass1Controller != null) {
@ -158,10 +162,6 @@ class RegistrationOptions {
return null; return null;
} }
], ],
hidden: pass2Hidden,
onPassChanged: (value) {
passHideOnChange?.call(false, value);
},
), ),
], ],
), ),

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/flutter_input_library.dart';
import 'package:flutter_registration/flutter_registration.dart';
class AuthBoolField extends AuthField {
AuthBoolField({
required super.name,
required this.widgetType,
super.title,
super.validators = const [],
super.value = '',
this.leftWidget,
this.rightWidget,
this.onChange,
});
final Widget? leftWidget;
final Widget? rightWidget;
final BoolWidgetType widgetType;
final Function(String value)? onChange;
@override
Widget build(BuildContext context, Function onValueChanged) {
return FlutterFormInputBool(
widgetType: widgetType,
onChanged: (v) {
value = v;
onChange?.call(value);
onValueChanged();
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
return null;
},
leftWidget: leftWidget,
rightWidget: rightWidget,
);
}
}

View file

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_registration/flutter_registration.dart';
class AuthDropdownField extends AuthField {
AuthDropdownField({
required super.name,
required this.items,
required this.onChanged,
this.dropdownDecoration,
this.padding = const EdgeInsets.all(8.0),
this.textStyle,
this.icon = const Icon(Icons.keyboard_arrow_down),
required super.value,
}) {
selectedValue = value ?? items.first;
}
final List<String> items;
final Function(String?) onChanged;
String? selectedValue;
final InputDecoration? dropdownDecoration;
final EdgeInsets padding;
final TextStyle? textStyle;
final Icon icon;
@override
Widget build(BuildContext context, Function onValueChanged) {
return Padding(
padding: padding,
child: DropdownButtonFormField<String>(
icon: icon,
style: textStyle,
value: selectedValue,
decoration: dropdownDecoration,
items: items.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (newValue) {
selectedValue = newValue;
onChanged(newValue);
onValueChanged();
},
validator: (value) {
if (validators.isNotEmpty) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
}
return null;
},
),
);
}
}

View file

@ -4,18 +4,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
abstract class AuthField { abstract class AuthField<T> {
AuthField({ AuthField({
required this.name, required this.name,
required this.value,
this.onValueChanged,
this.title, this.title,
this.validators = const [], this.validators = const [],
this.value = '',
}); });
final String name; final String name;
final Widget? title; final Widget? title;
List<String? Function(String?)> validators; List<String? Function(T?)> validators;
String value; T value;
Widget build(); final Function(T)? onValueChanged; // Callback for value changes
Widget build(BuildContext context, Function onValueChanged);
} }

View file

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2024 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_input_library/flutter_input_library.dart';
import 'package:flutter_registration/flutter_registration.dart';
class AuthPassField extends AuthField {
AuthPassField({
required super.name,
TextEditingController? textEditingController,
super.title,
super.validators = const [],
super.value = '',
this.textStyle,
this.onChange,
this.iconSize,
this.textFieldDecoration,
this.padding = const EdgeInsets.all(8.0),
});
final TextStyle? textStyle;
final double? iconSize;
final Function(String value)? onChange;
final InputDecoration? textFieldDecoration;
final EdgeInsets padding;
@override
Widget build(BuildContext context, Function onValueChanged) {
return Padding(
padding: padding,
child: FlutterFormInputPassword(
style: textStyle,
iconSize: iconSize ?? 24.0,
decoration: textFieldDecoration,
onChanged: (v) {
value = v;
onChange?.call(value);
onValueChanged();
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
}
}
return null;
},
),
);
}
}

View file

@ -12,72 +12,45 @@ class AuthTextField extends AuthField {
super.title, super.title,
super.validators = const [], super.validators = const [],
super.value = '', super.value = '',
this.obscureText = false,
this.hintText,
this.label,
this.textStyle, this.textStyle,
this.onChange, this.onChange,
this.hidden, this.textFieldDecoration,
this.onPassChanged, this.padding = const EdgeInsets.all(8.0),
}) { }) {
textController = textController =
textEditingController ?? TextEditingController(text: value); textEditingController ?? TextEditingController(text: value);
} }
late TextEditingController textController; late TextEditingController textController;
final bool obscureText;
final String? hintText;
final Widget? label;
final TextStyle? textStyle; final TextStyle? textStyle;
final Function(String value)? onChange; final Function(String value)? onChange;
final bool? hidden; final InputDecoration? textFieldDecoration;
final Function(bool value)? onPassChanged; final EdgeInsets padding;
@override @override
Widget build() { Widget build(BuildContext context, Function onValueChanged) {
Widget? suffix; return Padding(
padding: padding,
if (hidden != null) { child: TextFormField(
if (hidden!) { style: textStyle,
suffix = GestureDetector( decoration: textFieldDecoration,
onTap: () { controller: textController,
onPassChanged?.call(!hidden!); onChanged: (v) {
}, value = v;
child: const Icon(Icons.visibility), onChange?.call(value);
); onValueChanged();
} else { },
suffix = GestureDetector( validator: (value) {
onTap: () { for (var validator in validators) {
onPassChanged?.call(!hidden!); var output = validator(value);
}, if (output != null) {
child: const Icon(Icons.visibility_off), return output;
); }
}
}
return TextFormField(
style: textStyle,
decoration: InputDecoration(
label: label,
hintText: hintText,
suffix: suffix,
),
controller: textController,
obscureText: hidden ?? obscureText,
onChanged: (v) {
value = v;
onChange?.call(value);
},
validator: (value) {
for (var validator in validators) {
var output = validator(value);
if (output != null) {
return output;
} }
}
return null; return null;
}, },
),
); );
} }
} }

View file

@ -17,7 +17,7 @@ class RegistrationScreen extends StatelessWidget {
final RegistrationOptions registrationOptions; final RegistrationOptions registrationOptions;
Future<void> register({ Future<void> register({
required HashMap<String, String> values, required HashMap<String, dynamic> values,
required void Function(int? pageToReturn) onError, required void Function(int? pageToReturn) onError,
}) async { }) async {
try { try {
@ -46,13 +46,15 @@ class RegistrationScreen extends StatelessWidget {
translations.title, translations.title,
), ),
onFinish: register, onFinish: register,
title: translations.title, appBarTitle: translations.title,
submitBtnTitle: translations.registerBtn, submitBtnTitle: translations.registerBtn,
nextBtnTitle: translations.nextStepBtn, nextBtnTitle: translations.nextStepBtn,
previousBtnTitle: translations.previousStepBtn, previousBtnTitle: translations.previousStepBtn,
nextButtonBuilder: registrationOptions.nextButtonBuilder, nextButtonBuilder: registrationOptions.nextButtonBuilder,
previousButtonBuilder: registrationOptions.previousButtonBuilder, previousButtonBuilder: registrationOptions.previousButtonBuilder,
customBackgroundColor: registrationOptions.backgroundColor, customBackgroundColor: registrationOptions.backgroundColor,
titleWidget: registrationOptions.titleWidget,
loginButton: registrationOptions.loginButton,
); );
} }
} }

View file

@ -4,14 +4,21 @@
name: flutter_registration name: flutter_registration
description: A Flutter Registration package description: A Flutter Registration package
version: 1.2.0 version: 2.0.0
repository: https://github.com/Iconica-Development/flutter_registration repository: https://github.com/Iconica-Development/flutter_registration
publish_to: none
environment: environment:
sdk: ">=2.18.0 <3.0.0" sdk: ">=2.18.0 <3.0.0"
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
flutter_input_library:
git:
url: https://github.com/Iconica-Development/flutter_input_library
ref: 3.0.1
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations: