This commit is contained in:
Gaurav Kumar
2025-09-10 10:57:03 +05:30
parent 13eca99151
commit bc02a06d56
14 changed files with 2580 additions and 79 deletions

View File

@@ -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;
}
}