dep
This commit is contained in:
		
							parent
							
								
									0805cf44e4
								
							
						
					
					
						commit
						70c992ddb9
					
				| @ -0,0 +1,159 @@ | ||||
| // import 'package:flutter/material.dart'; | ||||
| // import 'base_field.dart'; | ||||
| // import 'dependent_dropdown_field.dart'; | ||||
| // import 'dropdown_field.dart'; | ||||
| 
 | ||||
| // /// Example usage of DependentDropdownField | ||||
| // /// This demonstrates how to create cascading dropdowns with API calls | ||||
| // class DependentDropdownExample { | ||||
| //   /// Example field definitions for a location form with country -> state -> district | ||||
| //   static List<BaseField> getLocationFields() { | ||||
| //     return [ | ||||
| //       // Parent dropdown - Country selection | ||||
| //       DropdownField( | ||||
| //         fieldKey: 'country', | ||||
| //         label: 'Country', | ||||
| //         hint: 'Select country', | ||||
| //         options: const [ | ||||
| //           {'id': 'india', 'name': 'India'}, | ||||
| //           {'id': 'usa', 'name': 'United States'}, | ||||
| //           {'id': 'canada', 'name': 'Canada'}, | ||||
| //         ], | ||||
| //         valueKey: 'id', | ||||
| //         displayKey: 'name', | ||||
| //       ), | ||||
| 
 | ||||
| //       // Dependent dropdown - State selection (depends on country) | ||||
| //       DependentDropdownField( | ||||
| //         fieldKey: 'state', | ||||
| //         label: 'State', | ||||
| //         hint: 'Select state', | ||||
| //         dependentFieldKey: 'country', | ||||
| //         apiEndpoint: '/State_ListFilter1/State_ListFilter11', | ||||
| //         valueKey: 'state_name', | ||||
| //         displayKey: 'state_name', | ||||
| //         dependentValueKey: | ||||
| //             'name', // This is the field from country that gets passed to API | ||||
| //       ), | ||||
| 
 | ||||
| //       // Dependent dropdown - District selection (depends on state) | ||||
| //       DependentDropdownField( | ||||
| //         fieldKey: 'district', | ||||
| //         label: 'District', | ||||
| //         hint: 'Select district', | ||||
| //         dependentFieldKey: 'state', | ||||
| //         apiEndpoint: '/District_ListFilter1/District_ListFilter11', | ||||
| //         valueKey: 'district_name', | ||||
| //         displayKey: 'district_name', | ||||
| //         dependentValueKey: | ||||
| //             'state_name', // This is the field from state that gets passed to API | ||||
| //       ), | ||||
| //     ]; | ||||
| //   } | ||||
| 
 | ||||
| //   /// Example field definitions for a product form with category -> subcategory -> product | ||||
| //   static List<BaseField> getProductFields() { | ||||
| //     return [ | ||||
| //       // Parent dropdown - Category selection | ||||
| //       DropdownField( | ||||
| //         fieldKey: 'category', | ||||
| //         label: 'Category', | ||||
| //         hint: 'Select category', | ||||
| //         options: const [ | ||||
| //           {'id': 'electronics', 'name': 'Electronics'}, | ||||
| //           {'id': 'clothing', 'name': 'Clothing'}, | ||||
| //           {'id': 'books', 'name': 'Books'}, | ||||
| //         ], | ||||
| //         valueKey: 'id', | ||||
| //         displayKey: 'name', | ||||
| //       ), | ||||
| 
 | ||||
| //       // Dependent dropdown - Subcategory selection (depends on category) | ||||
| //       DependentDropdownField( | ||||
| //         fieldKey: 'subcategory', | ||||
| //         label: 'Subcategory', | ||||
| //         hint: 'Select subcategory', | ||||
| //         dependentFieldKey: 'category', | ||||
| //         apiEndpoint: '/Subcategory_ListFilter1/Subcategory_ListFilter11', | ||||
| //         valueKey: 'subcategory_id', | ||||
| //         displayKey: 'subcategory_name', | ||||
| //         dependentValueKey: 'name', | ||||
| //       ), | ||||
| 
 | ||||
| //       // Dependent dropdown - Product selection (depends on subcategory) | ||||
| //       DependentDropdownField( | ||||
| //         fieldKey: 'product', | ||||
| //         label: 'Product', | ||||
| //         hint: 'Select product', | ||||
| //         dependentFieldKey: 'subcategory', | ||||
| //         apiEndpoint: '/Product_ListFilter1/Product_ListFilter11', | ||||
| //         valueKey: 'product_id', | ||||
| //         displayKey: 'product_name', | ||||
| //         dependentValueKey: 'subcategory_name', | ||||
| //       ), | ||||
| //     ]; | ||||
| //   } | ||||
| // } | ||||
| 
 | ||||
| // /// How the DependentDropdownField works: | ||||
| // ///  | ||||
| // /// 1. **Parent Field Selection**: When user selects a value in the parent field (e.g., country) | ||||
| // /// 2. **API Call Triggered**: The dependent field automatically detects the change | ||||
| // /// 3. **API Call Made**: Calls the specified API endpoint with the parent field value | ||||
| // /// 4. **Options Loaded**: The API response is parsed and options are populated | ||||
| // /// 5. **UI Updated**: The dropdown shows the new options | ||||
| // /// 6. **Cascading Effect**: If there are more dependent fields, they get cleared and wait for new selection | ||||
| // ///  | ||||
| // /// **API Endpoint Format**: | ||||
| // /// - Base URL: `ApiConstants.baseUrl` | ||||
| // /// - Endpoint: `/State_ListFilter1/State_ListFilter11/{dependentValue}` | ||||
| // /// - Example: `https://api.example.com/State_ListFilter1/State_ListFilter11/india` | ||||
| // ///  | ||||
| // /// **API Response Format**: | ||||
| // /// ```json | ||||
| // /// [ | ||||
| // ///   { | ||||
| // ///     "state_name": "Maharashtra", | ||||
| // ///     "state_id": "1" | ||||
| // ///   }, | ||||
| // ///   { | ||||
| // ///     "state_name": "Karnataka",  | ||||
| // ///     "state_id": "2" | ||||
| // ///   } | ||||
| // /// ] | ||||
| // /// ``` | ||||
| // ///  | ||||
| // /// **Key Features**: | ||||
| // /// - â
 **Automatic API Calls**: No manual API handling needed | ||||
| // /// - â
 **Loading States**: Shows loading indicator while fetching data | ||||
| // /// - â
 **Error Handling**: Displays error messages if API fails | ||||
| // /// - â
 **Cascading Clear**: Dependent fields clear when parent changes | ||||
| // /// - â
 **Form Integration**: Works seamlessly with EntityForm | ||||
| // /// - â
 **Theme Support**: Uses DynamicThemeProvider for consistent styling | ||||
| // /// - â
 **Validation**: Built-in required field validation | ||||
| // /// - â
 **Responsive**: Works on all screen sizes | ||||
| // ///  | ||||
| // /// **Usage in Entity**: | ||||
| // /// ```dart | ||||
| // /// // In your entity fields file | ||||
| // /// class MyEntityFields { | ||||
| // ///   static List<BaseField> getFields() { | ||||
| // ///     return [ | ||||
| // ///       DropdownField( | ||||
| // ///         fieldKey: 'country', | ||||
| // ///         label: 'Country', | ||||
| // ///         options: countryOptions, | ||||
| // ///       ), | ||||
| // ///       DependentDropdownField( | ||||
| // ///         fieldKey: 'state', | ||||
| // ///         label: 'State', | ||||
| // ///         dependentFieldKey: 'country', | ||||
| // ///         apiEndpoint: '/State_ListFilter1/State_ListFilter11', | ||||
| // ///         valueKey: 'state_name', | ||||
| // ///         displayKey: 'state_name', | ||||
| // ///         dependentValueKey: 'name', | ||||
| // ///       ), | ||||
| // ///     ]; | ||||
| // ///   } | ||||
| // /// } | ||||
| // /// ``` | ||||
| @ -0,0 +1,252 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'base_field.dart'; | ||||
| import 'dynamic_dropdown_field.dart'; | ||||
| import '../ui/entity_form.dart'; | ||||
| 
 | ||||
| /// Dependent dropdown field that loads options based on another field's value | ||||
| /// This field automatically makes API calls when the dependent field changes | ||||
| class DependentDropdownField extends BaseField { | ||||
|   final String fieldKey; | ||||
|   final String label; | ||||
|   final String hint; | ||||
|   final bool isRequired; | ||||
|   final String dependentFieldKey; // The field this dropdown depends on | ||||
|   final Future<List<Map<String, dynamic>>> Function(String parentValue) | ||||
|       optionsLoader; // Loader using parent value | ||||
|   final String valueKey; // Field name for value in API response | ||||
|   final String displayKey; // Field name for display text in API response | ||||
|   final Map<String, dynamic>? customProperties; | ||||
| 
 | ||||
|   DependentDropdownField({ | ||||
|     required this.fieldKey, | ||||
|     required this.label, | ||||
|     required this.hint, | ||||
|     this.isRequired = false, | ||||
|     required this.dependentFieldKey, | ||||
|     required this.optionsLoader, | ||||
|     this.valueKey = 'id', | ||||
|     this.displayKey = 'name', | ||||
|     this.customProperties, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   String? Function(String?)? get validator => (value) { | ||||
|         if (isRequired && (value == null || value.isEmpty)) { | ||||
|           return '$label is required'; | ||||
|         } | ||||
|         return null; | ||||
|       }; | ||||
| 
 | ||||
|   @override | ||||
|   Widget buildField({ | ||||
|     required TextEditingController controller, | ||||
|     required ColorScheme colorScheme, | ||||
|     VoidCallback? onChanged, | ||||
|   }) { | ||||
|     return _DependentDropdownWidget( | ||||
|       fieldKey: fieldKey, | ||||
|       label: label, | ||||
|       hint: hint, | ||||
|       isRequired: isRequired, | ||||
|       dependentFieldKey: dependentFieldKey, | ||||
|       optionsLoader: optionsLoader, | ||||
|       valueKey: valueKey, | ||||
|       displayKey: displayKey, | ||||
|       controller: controller, | ||||
|       colorScheme: colorScheme, | ||||
|       onChanged: onChanged, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class _DependentDropdownWidget extends StatefulWidget { | ||||
|   final String fieldKey; | ||||
|   final String label; | ||||
|   final String hint; | ||||
|   final bool isRequired; | ||||
|   final String dependentFieldKey; | ||||
|   final Future<List<Map<String, dynamic>>> Function(String parentValue) | ||||
|       optionsLoader; | ||||
|   final String valueKey; | ||||
|   final String displayKey; | ||||
|   final TextEditingController controller; | ||||
|   final ColorScheme colorScheme; | ||||
|   final VoidCallback? onChanged; | ||||
| 
 | ||||
|   const _DependentDropdownWidget({ | ||||
|     required this.fieldKey, | ||||
|     required this.label, | ||||
|     required this.hint, | ||||
|     required this.isRequired, | ||||
|     required this.dependentFieldKey, | ||||
|     required this.optionsLoader, | ||||
|     required this.valueKey, | ||||
|     required this.displayKey, | ||||
|     required this.controller, | ||||
|     required this.colorScheme, | ||||
|     this.onChanged, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   State<_DependentDropdownWidget> createState() => | ||||
|       _DependentDropdownWidgetState(); | ||||
| } | ||||
| 
 | ||||
| class _DependentDropdownWidgetState extends State<_DependentDropdownWidget> { | ||||
|   String? _lastDependentValue; | ||||
|   Key _reloadKey = const ValueKey('init'); | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     // Defer inherited widget access until after first frame | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       if (mounted) { | ||||
|         _checkDependentFieldChange(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   void _loadInitialOptions() async {} | ||||
| 
 | ||||
|   @override | ||||
|   void didChangeDependencies() { | ||||
|     super.didChangeDependencies(); | ||||
|     _checkDependentFieldChange(); | ||||
|   } | ||||
| 
 | ||||
|   void _checkDependentFieldChange() { | ||||
|     final formScope = EntityFormScope.of(context); | ||||
|     if (formScope != null) { | ||||
|       final dependentController = | ||||
|           formScope.controllers[widget.dependentFieldKey]; | ||||
|       if (dependentController != null) { | ||||
|         final currentValue = dependentController.text; | ||||
|         if (currentValue != _lastDependentValue) { | ||||
|           _lastDependentValue = currentValue; | ||||
|           // Defer mutations to next frame to avoid setState during build | ||||
|           WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|             if (!mounted) return; | ||||
|             // Clear current selection when parent changes | ||||
|             widget.controller.clear(); | ||||
|             // Force re-init of inner DynamicDropdown by changing key | ||||
|             setState(() { | ||||
|               _reloadKey = ValueKey( | ||||
|                   'dep-${widget.fieldKey}-${_lastDependentValue ?? 'empty'}-${DateTime.now().millisecondsSinceEpoch}'); | ||||
|             }); | ||||
|             // Notify parent about change | ||||
|             widget.onChanged?.call(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Future<List<Map<String, dynamic>>> loadOptions() async { | ||||
|       // If no dependent value selected, return empty | ||||
|       final formScope = EntityFormScope.of(context); | ||||
|       final depVal = | ||||
|           formScope?.controllers[widget.dependentFieldKey]?.text ?? ''; | ||||
|       if (depVal.isEmpty) return const []; | ||||
| 
 | ||||
|       try { | ||||
|         final response = await widget.optionsLoader(depVal); | ||||
|         return response | ||||
|             .map((item) => { | ||||
|                   widget.valueKey: item[widget.valueKey]?.toString() ?? '', | ||||
|                   widget.displayKey: item[widget.displayKey]?.toString() ?? '', | ||||
|                 }) | ||||
|             .toList(); | ||||
|       } catch (e) { | ||||
|         // Return empty list on error to prevent showing old data | ||||
|         return const []; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final innerField = DynamicDropdownField( | ||||
|       fieldKey: widget.fieldKey, | ||||
|       label: widget.label, | ||||
|       hint: widget.hint, | ||||
|       isRequired: widget.isRequired, | ||||
|       optionsLoader: loadOptions, | ||||
|       valueKey: widget.valueKey, | ||||
|       displayKey: widget.displayKey, | ||||
|     ); | ||||
| 
 | ||||
|     final child = innerField.buildField( | ||||
|       controller: widget.controller, | ||||
|       colorScheme: widget.colorScheme, | ||||
|       onChanged: widget.onChanged, | ||||
|     ); | ||||
| 
 | ||||
|     // Read current parent value for UI guard/notice | ||||
|     final formScope = EntityFormScope.of(context); | ||||
|     final depVal = formScope?.controllers[widget.dependentFieldKey]?.text ?? ''; | ||||
| 
 | ||||
|     final keyedChild = KeyedSubtree(key: _reloadKey, child: child); | ||||
| 
 | ||||
|     final content = Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         // If parent is empty, block interactions and show subtle disabled state | ||||
|         if (depVal.isEmpty) | ||||
|           Opacity( | ||||
|             opacity: 0.6, | ||||
|             child: AbsorbPointer(child: keyedChild), | ||||
|           ) | ||||
|         else | ||||
|           keyedChild, | ||||
|         if (depVal.isEmpty) | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(top: 6.0), | ||||
|             child: Text( | ||||
|               'Please select ${widget.dependentFieldKey} first', | ||||
|               style: TextStyle( | ||||
|                 color: widget.colorScheme.onSurface.withOpacity(0.7), | ||||
|                 fontSize: 12, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
| 
 | ||||
|     return content; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Helper class to manage dependent dropdown relationships | ||||
| /// This should be used in EntityForm to handle field dependencies | ||||
| class DependentDropdownManager { | ||||
|   static final Map<String, List<DependentDropdownField>> _dependencies = {}; | ||||
| 
 | ||||
|   /// Register a dependent dropdown field | ||||
|   static void registerDependency( | ||||
|       String dependentFieldKey, DependentDropdownField field) { | ||||
|     if (!_dependencies.containsKey(dependentFieldKey)) { | ||||
|       _dependencies[dependentFieldKey] = []; | ||||
|     } | ||||
|     _dependencies[dependentFieldKey]!.add(field); | ||||
|   } | ||||
| 
 | ||||
|   /// Get all dependent fields for a given field | ||||
|   static List<DependentDropdownField> getDependentFields(String fieldKey) { | ||||
|     return _dependencies[fieldKey] ?? []; | ||||
|   } | ||||
| 
 | ||||
|   /// Clear all dependencies (useful for form reset) | ||||
|   static void clearDependencies() { | ||||
|     _dependencies.clear(); | ||||
|   } | ||||
| 
 | ||||
|   /// Notify dependent fields when a field value changes | ||||
|   static void notifyDependentFields(String fieldKey, String value) { | ||||
|     final dependentFields = getDependentFields(fieldKey); | ||||
|     for (final field in dependentFields) { | ||||
|       // This would need to be implemented with proper context | ||||
|       // The actual implementation would be in EntityForm | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -383,15 +383,14 @@ | ||||
| //     } | ||||
| //   } | ||||
| // } | ||||
| 
 | ||||
| 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 '../fields/dependent_dropdown_field.dart'; | ||||
| import '../../../shared/widgets/buttons/modern_button.dart'; | ||||
| import '../../../core/constants/ui_constants.dart'; | ||||
| import 'dart:convert'; | ||||
| 
 | ||||
| /// Reusable form component that dynamically renders fields based on field definitions | ||||
| /// This allows UI to be independent of field types and enables reusability | ||||
| @ -419,12 +418,14 @@ class _EntityFormState extends State<EntityForm> { | ||||
|   final _formKey = GlobalKey<FormState>(); | ||||
|   final Map<String, TextEditingController> _controllers = {}; | ||||
|   final Map<String, BaseField> _fieldByKey = {}; | ||||
|   final Map<String, List<DependentDropdownField>> _dependentFields = {}; | ||||
|   late final Map<String, dynamic> _initialData; | ||||
| 
 | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _initializeControllers(); | ||||
|     _setupDependentFields(); | ||||
|   } | ||||
| 
 | ||||
|   void _initializeControllers() { | ||||
| @ -452,6 +453,37 @@ class _EntityFormState extends State<EntityForm> { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _setupDependentFields() { | ||||
|     // Group dependent dropdown fields by their dependent field key | ||||
|     for (final field in widget.fields) { | ||||
|       if (field is DependentDropdownField) { | ||||
|         final dependentFieldKey = field.dependentFieldKey; | ||||
|         if (!_dependentFields.containsKey(dependentFieldKey)) { | ||||
|           _dependentFields[dependentFieldKey] = []; | ||||
|         } | ||||
|         _dependentFields[dependentFieldKey]!.add(field); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _handleFieldChange(String fieldKey) { | ||||
|     setState(() {}); | ||||
| 
 | ||||
|     // Check if this field has dependent dropdowns | ||||
|     if (_dependentFields.containsKey(fieldKey)) { | ||||
|       final dependentFields = _dependentFields[fieldKey]!; | ||||
|       final fieldValue = _controllers[fieldKey]?.text ?? ''; | ||||
| 
 | ||||
|       // Clear dependent dropdown values when parent field changes | ||||
|       for (final dependentField in dependentFields) { | ||||
|         _controllers[dependentField.fieldKey]?.clear(); | ||||
|       } | ||||
| 
 | ||||
|       // Trigger rebuild to update dependent dropdowns | ||||
|       setState(() {}); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void dispose() { | ||||
|     for (final controller in _controllers.values) { | ||||
| @ -482,7 +514,7 @@ class _EntityFormState extends State<EntityForm> { | ||||
|                       child: field.buildField( | ||||
|                         controller: _controllers[field.fieldKey]!, | ||||
|                         colorScheme: colorScheme, | ||||
|                         onChanged: () => setState(() {}), | ||||
|                         onChanged: () => _handleFieldChange(field.fieldKey), | ||||
|                       ), | ||||
|                     )), | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user