dattype
This commit is contained in:
@@ -1,6 +1,394 @@
|
||||
import 'package:base_project/core/providers/dynamic_theme_provider.dart';
|
||||
// import 'package:base_project/core/providers/dynamic_theme_provider.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:provider/provider.dart';
|
||||
// import '../fields/base_field.dart';
|
||||
// import '../../../shared/widgets/buttons/modern_button.dart';
|
||||
// import '../../../core/constants/ui_constants.dart';
|
||||
|
||||
// /// Reusable form component that dynamically renders fields based on field definitions
|
||||
// /// This allows UI to be independent of field types and enables reusability
|
||||
// class EntityForm extends StatefulWidget {
|
||||
// final List<BaseField> fields;
|
||||
// final Map<String, dynamic>? initialData;
|
||||
// final Function(Map<String, dynamic>) onSubmit;
|
||||
// final String submitButtonText;
|
||||
// final bool isLoading;
|
||||
|
||||
// const EntityForm({
|
||||
// super.key,
|
||||
// required this.fields,
|
||||
// this.initialData,
|
||||
// required this.onSubmit,
|
||||
// this.submitButtonText = 'Submit',
|
||||
// this.isLoading = false,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// State<EntityForm> createState() => _EntityFormState();
|
||||
// }
|
||||
|
||||
// class _EntityFormState extends State<EntityForm> {
|
||||
// final _formKey = GlobalKey<FormState>();
|
||||
// final Map<String, TextEditingController> _controllers = {};
|
||||
// final Map<String, BaseField> _fieldByKey = {};
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _initializeControllers();
|
||||
// }
|
||||
|
||||
// void _initializeControllers() {
|
||||
// for (final field in widget.fields) {
|
||||
// _controllers[field.fieldKey] = TextEditingController(
|
||||
// text: widget.initialData?[field.fieldKey]?.toString() ?? '',
|
||||
// );
|
||||
// _fieldByKey[field.fieldKey] = field;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// for (final controller in _controllers.values) {
|
||||
// controller.dispose();
|
||||
// }
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Consumer<DynamicThemeProvider>(
|
||||
// builder: (context, dynamicThemeProvider, child) {
|
||||
// final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
// Theme.of(context).brightness == Brightness.dark,
|
||||
// );
|
||||
|
||||
// return Form(
|
||||
// key: _formKey,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// // Dynamic field rendering
|
||||
// ...widget.fields.map((field) => Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
// child: field.buildField(
|
||||
// controller: _controllers[field.fieldKey]!,
|
||||
// colorScheme: colorScheme,
|
||||
// onChanged: () => setState(() {}),
|
||||
// ),
|
||||
// )),
|
||||
|
||||
// const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// // Submit button
|
||||
// ModernButton(
|
||||
// text: widget.submitButtonText,
|
||||
// type: ModernButtonType.primary,
|
||||
// size: ModernButtonSize.large,
|
||||
// isLoading: widget.isLoading,
|
||||
// onPressed: widget.isLoading ? null : _handleSubmit,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _handleSubmit() {
|
||||
// if (_formKey.currentState!.validate()) {
|
||||
// // Dynamic cross-field match for any password-confirm group
|
||||
// final Map<String, String> passwordByGroup = {};
|
||||
// final Map<String, String> confirmByGroup = {};
|
||||
// for (final entry in _controllers.entries) {
|
||||
// final key = entry.key;
|
||||
// final field = _fieldByKey[key];
|
||||
// final props = field?.customProperties ?? const {};
|
||||
// final isPassword = props['isPassword'] == true;
|
||||
// if (!isPassword) continue;
|
||||
// final String? groupId = props['groupId'];
|
||||
// if (groupId == null) continue;
|
||||
// final bool isConfirm = props['isConfirm'] == true;
|
||||
// if (isConfirm) {
|
||||
// confirmByGroup[groupId] = entry.value.text;
|
||||
// } else {
|
||||
// passwordByGroup[groupId] = entry.value.text;
|
||||
// }
|
||||
// }
|
||||
// for (final gid in confirmByGroup.keys) {
|
||||
// final confirm = confirmByGroup[gid] ?? '';
|
||||
// final pass = passwordByGroup[gid] ?? '';
|
||||
// if (confirm.isNotEmpty && confirm != pass) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(content: Text('Passwords do not match')),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// final formData = <String, dynamic>{};
|
||||
// for (final entry in _controllers.entries) {
|
||||
// final key = entry.key;
|
||||
// final field = _fieldByKey[key];
|
||||
// final value = entry.value.text.trim();
|
||||
|
||||
// // Skip confirm entries for any password group
|
||||
// final props = field?.customProperties ?? const {};
|
||||
// final bool isPassword = props['isPassword'] == true;
|
||||
// final bool isConfirm = props['isConfirm'] == true;
|
||||
// if (isPassword && isConfirm) continue;
|
||||
|
||||
// formData[key] = value;
|
||||
// }
|
||||
// widget.onSubmit(formData);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// SECOND WORKING CODE
|
||||
// import 'dart:convert';
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:provider/provider.dart';
|
||||
|
||||
// import '../../../core/constants/ui_constants.dart';
|
||||
// import '../../../core/providers/dynamic_theme_provider.dart';
|
||||
// import '../../../shared/widgets/buttons/modern_button.dart';
|
||||
// import '../fields/base_field.dart';
|
||||
|
||||
// /// Reusable form component that dynamically renders fields based on field definitions
|
||||
// /// This allows UI to be independent of field types and enables reusability
|
||||
// class EntityForm extends StatefulWidget {
|
||||
// final List<BaseField> fields;
|
||||
// final Map<String, dynamic>? initialData;
|
||||
// final Function(Map<String, dynamic>) onSubmit;
|
||||
// final String submitButtonText;
|
||||
// final bool isLoading;
|
||||
|
||||
// const EntityForm({
|
||||
// super.key,
|
||||
// required this.fields,
|
||||
// this.initialData,
|
||||
// required this.onSubmit,
|
||||
// this.submitButtonText = 'Submit',
|
||||
// this.isLoading = false,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// State<EntityForm> createState() => _EntityFormState();
|
||||
// }
|
||||
|
||||
// class _EntityFormState extends State<EntityForm> {
|
||||
// final _formKey = GlobalKey<FormState>();
|
||||
// final Map<String, TextEditingController> _controllers = {};
|
||||
// final Map<String, BaseField> _fieldByKey = {};
|
||||
// late final Map<String, dynamic> _initialData;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _initializeControllers();
|
||||
// }
|
||||
|
||||
// void _initializeControllers() {
|
||||
// _initialData = widget.initialData ?? const {};
|
||||
// for (final field in widget.fields) {
|
||||
// final props = field.customProperties ?? const {};
|
||||
// final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
|
||||
// final List<dynamic>? paths = props['paths'] as List<dynamic>?;
|
||||
// String initialText =
|
||||
// widget.initialData?[field.fieldKey]?.toString() ?? '';
|
||||
// if (assignByJsonPaths && paths != null) {
|
||||
// final Map<String, dynamic> values = {};
|
||||
// for (final p in paths) {
|
||||
// if (p is String) {
|
||||
// final v = _readValueByPath(_initialData, p);
|
||||
// if (v != null) values[p] = v;
|
||||
// }
|
||||
// }
|
||||
// if (values.isNotEmpty) {
|
||||
// initialText = _encodeMap(values);
|
||||
// }
|
||||
// }
|
||||
// _controllers[field.fieldKey] = TextEditingController(text: initialText);
|
||||
// _fieldByKey[field.fieldKey] = field;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// for (final controller in _controllers.values) {
|
||||
// controller.dispose();
|
||||
// }
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Consumer<DynamicThemeProvider>(
|
||||
// builder: (context, dynamicThemeProvider, child) {
|
||||
// final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
// Theme.of(context).brightness == Brightness.dark,
|
||||
// );
|
||||
|
||||
// return Form(
|
||||
// key: _formKey,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// // Dynamic field rendering
|
||||
// ...widget.fields.map((field) => Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
// child: field.buildField(
|
||||
// controller: _controllers[field.fieldKey]!,
|
||||
// colorScheme: colorScheme,
|
||||
// onChanged: () => setState(() {}),
|
||||
// ),
|
||||
// )),
|
||||
|
||||
// const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// // Submit button
|
||||
// ModernButton(
|
||||
// text: widget.submitButtonText,
|
||||
// type: ModernButtonType.primary,
|
||||
// size: ModernButtonSize.large,
|
||||
// isLoading: widget.isLoading,
|
||||
// onPressed: widget.isLoading ? null : _handleSubmit,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _handleSubmit() {
|
||||
// if (_formKey.currentState!.validate()) {
|
||||
// // Dynamic cross-field match for any password-confirm group
|
||||
// final Map<String, String> passwordByGroup = {};
|
||||
// final Map<String, String> confirmByGroup = {};
|
||||
// for (final entry in _controllers.entries) {
|
||||
// final key = entry.key;
|
||||
// final field = _fieldByKey[key];
|
||||
// final props = field?.customProperties ?? const {};
|
||||
// final isPassword = props['isPassword'] == true;
|
||||
// if (!isPassword) continue;
|
||||
// final String? groupId = props['groupId'];
|
||||
// if (groupId == null) continue;
|
||||
// final bool isConfirm = props['isConfirm'] == true;
|
||||
// if (isConfirm) {
|
||||
// confirmByGroup[groupId] = entry.value.text;
|
||||
// } else {
|
||||
// passwordByGroup[groupId] = entry.value.text;
|
||||
// }
|
||||
// }
|
||||
// for (final gid in confirmByGroup.keys) {
|
||||
// final confirm = confirmByGroup[gid] ?? '';
|
||||
// final pass = passwordByGroup[gid] ?? '';
|
||||
// if (confirm.isNotEmpty && confirm != pass) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(content: Text('Passwords do not match')),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// final formData = <String, dynamic>{};
|
||||
// for (final entry in _controllers.entries) {
|
||||
// final key = entry.key;
|
||||
// final field = _fieldByKey[key];
|
||||
// final value = entry.value.text.trim();
|
||||
|
||||
// // Skip fields that are marked as non-submittable (e.g., DataGrid)
|
||||
// final props = field?.customProperties ?? const {};
|
||||
// final bool excludeFromSubmit = props['excludeFromSubmit'] == true;
|
||||
// if (excludeFromSubmit) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // Skip confirm entries for any password group
|
||||
// final bool isPassword = props['isPassword'] == true;
|
||||
// final bool isConfirm = props['isConfirm'] == true;
|
||||
// if (isPassword && isConfirm) continue;
|
||||
|
||||
// final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
|
||||
// if (assignByJsonPaths) {
|
||||
// // If composite and empty -> skip adding base key
|
||||
// if (value.isEmpty) {
|
||||
// continue;
|
||||
// }
|
||||
// final Map<String, dynamic>? map = _tryDecodeMap(value);
|
||||
// if (map != null) {
|
||||
// map.forEach((path, v) {
|
||||
// if (path is String) {
|
||||
// _assignValueByPath(formData, path, v);
|
||||
// }
|
||||
// });
|
||||
// continue;
|
||||
// }
|
||||
// // If not decodable, also skip to avoid sending invalid base key
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// formData[key] = value;
|
||||
// }
|
||||
// widget.onSubmit(formData);
|
||||
// }
|
||||
// }
|
||||
|
||||
// dynamic _readValueByPath(Map<String, dynamic>? source, String path) {
|
||||
// if (source == null) return null;
|
||||
// final segments = path.split('.');
|
||||
// dynamic current = source;
|
||||
// for (final segment in segments) {
|
||||
// if (current is Map<String, dynamic> && current.containsKey(segment)) {
|
||||
// current = current[segment];
|
||||
// } else {
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
// return current;
|
||||
// }
|
||||
|
||||
// void _assignValueByPath(
|
||||
// Map<String, dynamic> target, String path, dynamic value) {
|
||||
// final segments = path.split('.');
|
||||
// Map<String, dynamic> current = target;
|
||||
// for (int i = 0; i < segments.length; i++) {
|
||||
// final seg = segments[i];
|
||||
// final bool isLast = i == segments.length - 1;
|
||||
// if (isLast) {
|
||||
// current[seg] = value;
|
||||
// } else {
|
||||
// if (current[seg] is! Map<String, dynamic>) {
|
||||
// current[seg] = <String, dynamic>{};
|
||||
// }
|
||||
// current = current[seg] as Map<String, dynamic>;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// String _encodeMap(Map<String, dynamic> map) {
|
||||
// return const JsonEncoder().convert(map);
|
||||
// }
|
||||
|
||||
// Map<String, dynamic>? _tryDecodeMap(String value) {
|
||||
// try {
|
||||
// final decoded = const JsonDecoder().convert(value);
|
||||
// if (decoded is Map<String, dynamic>) return decoded;
|
||||
// return null;
|
||||
// } catch (_) {
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/providers/dynamic_theme_provider.dart';
|
||||
import '../fields/base_field.dart';
|
||||
import '../../../shared/widgets/buttons/modern_button.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
@@ -31,6 +419,7 @@ class _EntityFormState extends State<EntityForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final Map<String, TextEditingController> _controllers = {};
|
||||
final Map<String, BaseField> _fieldByKey = {};
|
||||
late final Map<String, dynamic> _initialData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -39,10 +428,26 @@ class _EntityFormState extends State<EntityForm> {
|
||||
}
|
||||
|
||||
void _initializeControllers() {
|
||||
_initialData = widget.initialData ?? const {};
|
||||
for (final field in widget.fields) {
|
||||
_controllers[field.fieldKey] = TextEditingController(
|
||||
text: widget.initialData?[field.fieldKey]?.toString() ?? '',
|
||||
);
|
||||
final props = field.customProperties ?? const {};
|
||||
final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
|
||||
final List<dynamic>? paths = props['paths'] as List<dynamic>?;
|
||||
String initialText =
|
||||
widget.initialData?[field.fieldKey]?.toString() ?? '';
|
||||
if (assignByJsonPaths && paths != null) {
|
||||
final Map<String, dynamic> values = {};
|
||||
for (final p in paths) {
|
||||
if (p is String) {
|
||||
final v = _readValueByPath(_initialData, p);
|
||||
if (v != null) values[p] = v;
|
||||
}
|
||||
}
|
||||
if (values.isNotEmpty) {
|
||||
initialText = _encodeMap(values);
|
||||
}
|
||||
}
|
||||
_controllers[field.fieldKey] = TextEditingController(text: initialText);
|
||||
_fieldByKey[field.fieldKey] = field;
|
||||
}
|
||||
}
|
||||
@@ -63,32 +468,36 @@ class _EntityFormState extends State<EntityForm> {
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Dynamic field rendering
|
||||
...widget.fields.map((field) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
child: field.buildField(
|
||||
controller: _controllers[field.fieldKey]!,
|
||||
colorScheme: colorScheme,
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
)),
|
||||
return EntityFormScope(
|
||||
controllers: _controllers,
|
||||
notifyParent: () => setState(() {}),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Dynamic field rendering
|
||||
...widget.fields.map((field) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
child: field.buildField(
|
||||
controller: _controllers[field.fieldKey]!,
|
||||
colorScheme: colorScheme,
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
)),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// Submit button
|
||||
ModernButton(
|
||||
text: widget.submitButtonText,
|
||||
type: ModernButtonType.primary,
|
||||
size: ModernButtonSize.large,
|
||||
isLoading: widget.isLoading,
|
||||
onPressed: widget.isLoading ? null : _handleSubmit,
|
||||
),
|
||||
],
|
||||
// Submit button
|
||||
ModernButton(
|
||||
text: widget.submitButtonText,
|
||||
type: ModernButtonType.primary,
|
||||
size: ModernButtonSize.large,
|
||||
isLoading: widget.isLoading,
|
||||
onPressed: widget.isLoading ? null : _handleSubmit,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -132,15 +541,108 @@ class _EntityFormState extends State<EntityForm> {
|
||||
final field = _fieldByKey[key];
|
||||
final value = entry.value.text.trim();
|
||||
|
||||
// Skip confirm entries for any password group
|
||||
// Skip fields that are marked as non-submittable (e.g., DataGrid)
|
||||
final props = field?.customProperties ?? const {};
|
||||
final bool excludeFromSubmit = props['excludeFromSubmit'] == true;
|
||||
if (excludeFromSubmit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip confirm entries for any password group
|
||||
final bool isPassword = props['isPassword'] == true;
|
||||
final bool isConfirm = props['isConfirm'] == true;
|
||||
if (isPassword && isConfirm) continue;
|
||||
|
||||
final bool assignByJsonPaths = props['assignByJsonPaths'] == true;
|
||||
if (assignByJsonPaths) {
|
||||
// If composite and empty -> skip adding base key
|
||||
if (value.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final Map<String, dynamic>? map = _tryDecodeMap(value);
|
||||
if (map != null) {
|
||||
map.forEach((path, v) {
|
||||
if (path is String) {
|
||||
_assignValueByPath(formData, path, v);
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// If not decodable, also skip to avoid sending invalid base key
|
||||
continue;
|
||||
}
|
||||
|
||||
formData[key] = value;
|
||||
}
|
||||
widget.onSubmit(formData);
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _readValueByPath(Map<String, dynamic>? source, String path) {
|
||||
if (source == null) return null;
|
||||
final segments = path.split('.');
|
||||
dynamic current = source;
|
||||
for (final segment in segments) {
|
||||
if (current is Map<String, dynamic> && current.containsKey(segment)) {
|
||||
current = current[segment];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
void _assignValueByPath(
|
||||
Map<String, dynamic> target, String path, dynamic value) {
|
||||
final segments = path.split('.');
|
||||
Map<String, dynamic> current = target;
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
final seg = segments[i];
|
||||
final bool isLast = i == segments.length - 1;
|
||||
if (isLast) {
|
||||
current[seg] = value;
|
||||
} else {
|
||||
if (current[seg] is! Map<String, dynamic>) {
|
||||
current[seg] = <String, dynamic>{};
|
||||
}
|
||||
current = current[seg] as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _encodeMap(Map<String, dynamic> map) {
|
||||
return const JsonEncoder().convert(map);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _tryDecodeMap(String value) {
|
||||
try {
|
||||
final decoded = const JsonDecoder().convert(value);
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inherited scope to provide access to the form's controllers for advanced shared fields
|
||||
class EntityFormScope extends InheritedWidget {
|
||||
final Map<String, TextEditingController> controllers;
|
||||
final VoidCallback notifyParent;
|
||||
|
||||
const EntityFormScope({
|
||||
super.key,
|
||||
required this.controllers,
|
||||
required this.notifyParent,
|
||||
required Widget child,
|
||||
}) : super(child: child);
|
||||
|
||||
static EntityFormScope? of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<EntityFormScope>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant EntityFormScope oldWidget) {
|
||||
return oldWidget.controllers != controllers;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user