[0.1.0]
This commit is contained in:
3
lib/src/form/form_helper.astromic.dart
Normal file
3
lib/src/form/form_helper.astromic.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
export 'package:form_controller/form_controller.dart';
|
||||
export 'src/controller.dart';
|
||||
export 'src/form_field.dart';
|
||||
146
lib/src/form/src/controller.dart
Normal file
146
lib/src/form/src/controller.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
//s1 Imports
|
||||
//s2 Core Package Imports
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
//s2 1st-party Package Imports
|
||||
import 'package:form_controller/form_controller.dart';
|
||||
//s2 3rd-party Package Imports
|
||||
//s2 Dependancies Imports
|
||||
//s3 Routes
|
||||
//s3 Services
|
||||
//s3 Models & Widgets
|
||||
import 'enums/enums.exports.dart';
|
||||
//s1 Exports
|
||||
|
||||
/// A specialized form controller to handle form states,
|
||||
class AstromicFormController extends FormController {
|
||||
// State Variables
|
||||
final Map<String, AstromicFieldState> fieldStates = <String, AstromicFieldState>{};
|
||||
final Map<String, String?> fieldMessages = <String, String?>{};
|
||||
final Map<String, dynamic> _hostedValues = <String, dynamic>{};
|
||||
final Map<String, Map<String, String>>? streamedErrorMaps; // fieldId: {errorCode: errorMessage}
|
||||
final Stream<List<String>>? errorStream;
|
||||
// State Stream Variables
|
||||
static final StreamController<(String, AstromicFieldState)> _stateStreamController = StreamController<(String id, AstromicFieldState)>.broadcast();
|
||||
final Stream<(String id, AstromicFieldState)> stateStream = _stateStreamController.stream;
|
||||
|
||||
AstromicFormController({
|
||||
Map<String, (String initialText, bool initialObscurity)>? extraControllers,
|
||||
this.streamedErrorMaps,
|
||||
this.errorStream,
|
||||
}) : super(controllers: extraControllers) {
|
||||
// Add states and messages based on initial controller.
|
||||
_addInitialControllers();
|
||||
|
||||
// Listen on the error stream for values and push to the corresponding field state.
|
||||
if (errorStream != null) {
|
||||
_handleErrorStream();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the field state and message of a specific field using it's ID.
|
||||
(AstromicFieldState, String? message)? getState(String fieldId) {
|
||||
if (fieldStates.containsKey(fieldId)) {
|
||||
return (fieldStates[fieldId]!, fieldMessages[fieldId]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the field state and message of a specific field using it's ID.
|
||||
void setState(String fieldId, AstromicFieldState state, {String? message}) {
|
||||
if (!fieldStates.containsKey(fieldId)) {
|
||||
throw FlutterError('Field ID $fieldId does not exist.');
|
||||
}
|
||||
fieldStates[fieldId] = state;
|
||||
fieldMessages[fieldId] = message;
|
||||
_stateStreamController.add((fieldId, state));
|
||||
}
|
||||
|
||||
/// Reset the state of a specific field using it's ID.
|
||||
resetState(String fieldId) {
|
||||
setState(fieldId, AstromicFieldState.idle);
|
||||
}
|
||||
|
||||
/// Set the value of a hosted state variable using it's ID.
|
||||
void setValue<T>(String id, T data) {
|
||||
if (_hostedValues.keys.toList().contains(id)) {
|
||||
_hostedValues[id] = data;
|
||||
} else {
|
||||
_hostedValues.addEntries(<MapEntry<String, dynamic>>[MapEntry<String, dynamic>(id, data)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value of a hosted state variable using it's ID.
|
||||
T? getValue<T>(String id) {
|
||||
if (_hostedValues.keys.toList().contains(id)) {
|
||||
if (_hostedValues[id] is T) {
|
||||
return _hostedValues[id];
|
||||
} else {
|
||||
throw FlutterError('Value found but is not of the type $T');
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TextEditingController controller(String id, {String? initialText, bool isObscure = false}) {
|
||||
if (super.controllers == null || !super.controllers!.keys.toList().contains(id)) {
|
||||
fieldStates.addEntries(<MapEntry<String, AstromicFieldState>>[MapEntry<String, AstromicFieldState>(id, AstromicFieldState.idle)]);
|
||||
fieldMessages.addEntries(<MapEntry<String, String?>>[MapEntry<String, String?>(id, null)]);
|
||||
}
|
||||
|
||||
TextEditingController ret = super.controller(id, initialText: initialText, isObscure: isObscure);
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
if (ret.text.isEmpty) {
|
||||
ret.text = initialText ?? '';
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_stateStreamController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
//SECTION - Helper Methods
|
||||
_addInitialControllers() {
|
||||
// Add in the initial field states...
|
||||
fieldStates.addEntries(super.controllers?.entries.toList().map((MapEntry<String, (String, bool)> e) => MapEntry<String, AstromicFieldState>(
|
||||
e.key, // controller ID
|
||||
AstromicFieldState.idle, // Initial state of any new controller is Idle
|
||||
)) ??
|
||||
<MapEntry<String, AstromicFieldState>>{});
|
||||
|
||||
// Add in the initial field messages...
|
||||
fieldMessages.addEntries(super.controllers?.entries.toList().map((MapEntry<String, (String, bool)> e) => MapEntry<String, String?>(
|
||||
e.key, // Controller ID
|
||||
null, // The initial message it has which is Null
|
||||
)) ??
|
||||
<MapEntry<String, String?>>{});
|
||||
}
|
||||
|
||||
_handleErrorStream() {
|
||||
errorStream!.distinct().listen((List<String> errorCodes) {
|
||||
if (streamedErrorMaps != null && streamedErrorMaps!.isNotEmpty) {
|
||||
for (String errorMapId in streamedErrorMaps!.keys.toList()) {
|
||||
if (super.controllers != null && super.controllers!.containsKey(errorMapId)) {
|
||||
if (streamedErrorMaps![errorMapId] != null &&
|
||||
streamedErrorMaps![errorMapId]!.isNotEmpty &&
|
||||
streamedErrorMaps![errorMapId]!.keys.toList().where((String k) => errorCodes.contains(k)).toList().isNotEmpty) {
|
||||
for (String eC in streamedErrorMaps![errorMapId]!.keys.toList().where((String k) => errorCodes.contains(k)).toList()) {
|
||||
String? m = streamedErrorMaps![errorMapId]![eC];
|
||||
setState(errorMapId, AstromicFieldState.withError, message: m ?? 'Error Message was not set!');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
//!SECTION
|
||||
}
|
||||
1
lib/src/form/src/enums/enums.exports.dart
Normal file
1
lib/src/form/src/enums/enums.exports.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'state.enum.dart';
|
||||
30
lib/src/form/src/enums/state.enum.dart
Normal file
30
lib/src/form/src/enums/state.enum.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
enum AstromicFieldState {
|
||||
idle,
|
||||
withInfo,
|
||||
withWarning,
|
||||
withError,
|
||||
withSuccess,
|
||||
}
|
||||
|
||||
extension FieldStateExtension on AstromicFieldState {
|
||||
T valuefyer<T>(
|
||||
T idle, {
|
||||
T? withInfo,
|
||||
T? withWarning,
|
||||
T? withError,
|
||||
T? withSuccess,
|
||||
}) {
|
||||
switch (this) {
|
||||
case AstromicFieldState.idle:
|
||||
return idle;
|
||||
case AstromicFieldState.withInfo:
|
||||
return withInfo ?? idle;
|
||||
case AstromicFieldState.withWarning:
|
||||
return withWarning ?? idle;
|
||||
case AstromicFieldState.withError:
|
||||
return withError ?? idle;
|
||||
case AstromicFieldState.withSuccess:
|
||||
return withSuccess ?? idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
315
lib/src/form/src/form_field.dart
Normal file
315
lib/src/form/src/form_field.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
//s1 Imports
|
||||
//s2 Core Package Imports
|
||||
import 'package:flutter/widgets.dart';
|
||||
//s2 1st-party Package Imports
|
||||
import 'package:astromic_elements/astromic_elements.dart';
|
||||
import 'package:form_controller/form_controller.dart';
|
||||
//s2 3rd-party Package Imports
|
||||
//s2 Dependancies Imports
|
||||
//s3 Routes
|
||||
//s3 Services
|
||||
//s3 Models & Widgets
|
||||
import 'controller.dart';
|
||||
import 'enums/enums.exports.dart';
|
||||
//s1 Exports
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class AstromicFormField<T> extends StatefulWidget {
|
||||
//SECTION - Widget Arguments
|
||||
//S1 -- Shared
|
||||
final AstromicFormController formController;
|
||||
final String formID;
|
||||
final bool? initialObscurity;
|
||||
//
|
||||
final AstromicFieldConfiguration? configuration;
|
||||
final List<FormControllerValidator>? validators;
|
||||
final bool? resetMessageOnChange;
|
||||
//
|
||||
final AstromicFieldStyle Function(bool isEnabled, bool isFocused, AstromicFieldState state)? style;
|
||||
//
|
||||
final String? hint;
|
||||
final Widget? Function(bool isEnabled, bool isFocused, VoidCallback stateSetter, AstromicFieldState state)? prefixWidget;
|
||||
final Widget? Function(bool isEnabled, bool isFocused, VoidCallback stateSetter, AstromicFieldState state)? suffixWidget;
|
||||
final Widget? Function(bool isEnabled, bool isFocused, AstromicFieldState state, String? message)? messageBuilder;
|
||||
//S1 -- Text Specific
|
||||
String? initialText;
|
||||
void Function(String v, AstromicFieldState state)? onChanged;
|
||||
void Function(String v, AstromicFieldState state)? onSubmited;
|
||||
Iterable<ContextMenuButtonItem>? contextButtons;
|
||||
//S1 -- Action Specific
|
||||
(T item, String label)? initialValue;
|
||||
Future<(T item, String label)?> Function((T item, String label)? currentValue)? onTap;
|
||||
Future<(T item, String label)?> Function((T item, String label)? currentValue)? onHold;
|
||||
String Function(String? oldValue, String newValue)? onValueChangedMapper;
|
||||
//!SECTION
|
||||
|
||||
AstromicFormField.text({
|
||||
required this.formController,
|
||||
required this.formID,
|
||||
this.initialObscurity,
|
||||
this.configuration,
|
||||
this.validators,
|
||||
this.resetMessageOnChange,
|
||||
this.style,
|
||||
this.hint,
|
||||
this.prefixWidget,
|
||||
this.suffixWidget,
|
||||
this.messageBuilder,
|
||||
//
|
||||
this.initialText,
|
||||
this.onChanged,
|
||||
this.onSubmited,
|
||||
this.contextButtons,
|
||||
});
|
||||
|
||||
AstromicFormField.action({
|
||||
required this.formController,
|
||||
required this.formID,
|
||||
this.initialObscurity,
|
||||
this.configuration,
|
||||
this.validators,
|
||||
this.resetMessageOnChange,
|
||||
this.style,
|
||||
this.hint,
|
||||
this.prefixWidget,
|
||||
this.suffixWidget,
|
||||
this.messageBuilder,
|
||||
//
|
||||
this.initialValue,
|
||||
this.onTap,
|
||||
this.onHold,
|
||||
this.onValueChangedMapper,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AstromicFormField<T>> createState() => _AstromicFormFieldState<T>();
|
||||
}
|
||||
|
||||
class _AstromicFormFieldState<T> extends State<AstromicFormField<T>> {
|
||||
//
|
||||
//SECTION - State Variables
|
||||
//s1 --State
|
||||
late AstromicFieldState _currentState;
|
||||
//s1 --State
|
||||
//
|
||||
//s1 --Controllers
|
||||
late TextEditingController _controller;
|
||||
//s1 --Controllers
|
||||
//
|
||||
//s1 --Constants
|
||||
//s1 --Constants
|
||||
//!SECTION
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//
|
||||
//SECTION - State Variables initializations & Listeners
|
||||
//s1 --State
|
||||
_currentState = AstromicFieldState.idle;
|
||||
//s1 --State
|
||||
//
|
||||
//s1 --Controllers & Listeners
|
||||
// Set the textEditingController for this field
|
||||
_controller = widget.formController.controller(widget.formID, initialText: widget.initialText, isObscure: widget.initialObscurity ?? false);
|
||||
|
||||
// Listen to the state stream for updated form states...
|
||||
widget.formController.stateStream.listen(((String, AstromicFieldState) newState) {
|
||||
if (mounted && widget.formID == newState.$1) {
|
||||
setState(() {
|
||||
_currentState = newState.$2;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to the error stream for updated errors and set the field's state accordingly...
|
||||
if (widget.formController.errorStream != null) {
|
||||
widget.formController.errorStream!.listen((List<String> errorCodes) {
|
||||
if (widget.formController.streamedErrorMaps != null) {
|
||||
// fieldId: {errorCode: errorMessage}
|
||||
for (String cID in widget.formController.streamedErrorMaps!.keys.toList()) {
|
||||
if (cID == widget.formID && widget.formController.streamedErrorMaps![cID] != null) {
|
||||
for (MapEntry<String, String> errMapItem in widget.formController.streamedErrorMaps![cID]!.entries) {
|
||||
if (errorCodes.map((String x) => x.toString()).contains(errMapItem.key.toString())) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_setFieldErrorState(widget.formID, errMapItem.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
});
|
||||
}
|
||||
//s1 --Controllers & Listeners
|
||||
//
|
||||
//s1 --Late & Async Initializers
|
||||
//s1 --Late & Async Initializers
|
||||
//!SECTION
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
//
|
||||
//SECTION - State Variables initializations & Listeners
|
||||
//s1 --State
|
||||
//s1 --State
|
||||
//
|
||||
//s1 --Controllers & Listeners
|
||||
//s1 --Controllers & Listeners
|
||||
//
|
||||
//!SECTION
|
||||
}
|
||||
|
||||
//SECTION - Dumb Widgets
|
||||
//!SECTION
|
||||
|
||||
//SECTION - Stateless functions
|
||||
_setFieldErrorState(String id, String? message) {
|
||||
widget.formController.setState(id, AstromicFieldState.withError, message: message);
|
||||
}
|
||||
//!SECTION
|
||||
|
||||
//SECTION - Action Callbacks
|
||||
//!SECTION
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//SECTION - Build Setup
|
||||
//s1 --Values
|
||||
//double w = MediaQuery.of(context).size.width;
|
||||
//double h = MediaQuery.of(context).size.height;
|
||||
//s1 --Values
|
||||
//
|
||||
//s1 --Contexted Widgets
|
||||
Widget buildText() {
|
||||
return AstromicFields.text(
|
||||
stateKey: widget.formController.fieldStateKey(widget.formID),
|
||||
controller: _controller,
|
||||
onChanged: (String s) {
|
||||
setState(() {
|
||||
if (widget.resetMessageOnChange ?? false) {
|
||||
widget.formController.resetState(widget.formID);
|
||||
}
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(s, _currentState);
|
||||
}
|
||||
});
|
||||
},
|
||||
onSubmited: (String s) {
|
||||
if (widget.onSubmited != null) {
|
||||
widget.onSubmited!(s, _currentState);
|
||||
}
|
||||
},
|
||||
configuration: widget.configuration?.copyWith(
|
||||
isTextObscured: widget.formController.isObscure(widget.formID),
|
||||
validator: (widget.validators != null && widget.validators!.isNotEmpty)
|
||||
? (bool enabled, bool focused, String? s) {
|
||||
widget.formController.resetState(widget.formID);
|
||||
//
|
||||
List<FormControllerValidator> validators = <FormControllerValidator>[];
|
||||
//
|
||||
validators.addAll(widget.validators!);
|
||||
//
|
||||
Map<FormControllerValidator, bool> checks = <FormControllerValidator, bool>{};
|
||||
//
|
||||
for (FormControllerValidator validator in validators) {
|
||||
bool res = validator.checker(s);
|
||||
checks.addEntries(<MapEntry<FormControllerValidator, bool>>[MapEntry<FormControllerValidator, bool>(validator, res)]);
|
||||
}
|
||||
//
|
||||
if (checks.containsValue(false)) {
|
||||
// It has an Error!
|
||||
_setFieldErrorState(widget.formID, checks.entries.where((MapEntry<FormControllerValidator, bool> e) => e.value == false).toList().first.key.message);
|
||||
return '';
|
||||
} else {
|
||||
// It has no Errors!
|
||||
return null;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
style: (bool enabled, bool focused) => widget.style != null ? widget.style!(enabled, focused, _currentState) : const AstromicFieldStyle(),
|
||||
//
|
||||
contextButtons: widget.contextButtons,
|
||||
//
|
||||
hint: widget.hint,
|
||||
prefixWidget: widget.prefixWidget != null ? (bool enabled, bool focused, void Function() setter) => widget.prefixWidget!(enabled, focused, setter, _currentState) : null,
|
||||
suffixWidget: widget.suffixWidget != null ? (bool enabled, bool focused, void Function() setter) => widget.suffixWidget!(enabled, focused, setter, _currentState) : null,
|
||||
messageBuilder:
|
||||
widget.messageBuilder != null ? (bool enabled, bool focused) => widget.messageBuilder!(enabled, focused, _currentState, widget.formController.getState(widget.formID)?.$2) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAction() {
|
||||
return AstromicFields.action(
|
||||
stateKey: widget.formController.fieldStateKey(widget.formID),
|
||||
controller: _controller,
|
||||
initialValue: widget.initialValue,
|
||||
onTap: widget.onTap,
|
||||
onHold: widget.onHold,
|
||||
onValueChangedMapper: widget.onValueChangedMapper!,
|
||||
//
|
||||
style: (bool enabled) => widget.style != null ? widget.style!(enabled, false, _currentState) : const AstromicFieldStyle(),
|
||||
configuration: widget.configuration?.copyWith(
|
||||
isTextObscured: widget.formController.isObscure(widget.formID),
|
||||
validator: (widget.validators != null && widget.validators!.isNotEmpty)
|
||||
? (bool enabled, bool focused, String? s) {
|
||||
widget.formController.resetState(widget.formID);
|
||||
//
|
||||
List<FormControllerValidator> validators = <FormControllerValidator>[];
|
||||
//
|
||||
validators.addAll(widget.validators!);
|
||||
//
|
||||
Map<FormControllerValidator, bool> checks = <FormControllerValidator, bool>{};
|
||||
//
|
||||
for (FormControllerValidator validator in validators) {
|
||||
bool res = validator.checker(s);
|
||||
checks.addEntries(<MapEntry<FormControllerValidator, bool>>[MapEntry<FormControllerValidator, bool>(validator, res)]);
|
||||
}
|
||||
//
|
||||
if (checks.containsValue(false)) {
|
||||
// It has an Error!
|
||||
_setFieldErrorState(widget.formID, checks.entries.where((MapEntry<FormControllerValidator, bool> e) => e.value == false).toList().first.key.message);
|
||||
return '';
|
||||
} else {
|
||||
// It has no Errors!
|
||||
return null;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
//
|
||||
hint: widget.hint,
|
||||
prefixWidget: widget.prefixWidget != null ? (bool enabled, void Function() setter) => widget.prefixWidget!(enabled, false, setter, _currentState) : null,
|
||||
suffixWidget: widget.suffixWidget != null ? (bool enabled, void Function() setter) => widget.suffixWidget!(enabled, false, setter, _currentState) : null,
|
||||
messageBuilder: widget.messageBuilder != null ? (bool enabled) => widget.messageBuilder!(enabled, false, _currentState, widget.formController.getState(widget.formID)?.$2) : null,
|
||||
//
|
||||
);
|
||||
}
|
||||
//s1 --Contexted Widgets
|
||||
//!SECTION
|
||||
|
||||
//SECTION - Build Return
|
||||
return widget.onValueChangedMapper != null
|
||||
?
|
||||
// Is Action Field
|
||||
buildAction()
|
||||
:
|
||||
// Is Text Field
|
||||
buildText();
|
||||
//!SECTION
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
//SECTION - Disposable variables
|
||||
_controller.dispose();
|
||||
//!SECTION
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user