diff --git a/base_project/lib/BuilderField/shared/fields/dependent_dropdown_example.dart b/base_project/lib/BuilderField/shared/fields/dependent_dropdown_example.dart new file mode 100644 index 0000000..0005640 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/dependent_dropdown_example.dart @@ -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 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 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 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', +// /// ), +// /// ]; +// /// } +// /// } +// /// ``` diff --git a/base_project/lib/BuilderField/shared/fields/dependent_dropdown_field.dart b/base_project/lib/BuilderField/shared/fields/dependent_dropdown_field.dart new file mode 100644 index 0000000..944f680 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/dependent_dropdown_field.dart @@ -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>> 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? 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>> 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>> 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> _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 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 + } + } +} diff --git a/base_project/lib/BuilderField/shared/ui/entity_form.dart b/base_project/lib/BuilderField/shared/ui/entity_form.dart index 08c4341..3c34427 100644 --- a/base_project/lib/BuilderField/shared/ui/entity_form.dart +++ b/base_project/lib/BuilderField/shared/ui/entity_form.dart @@ -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 { final _formKey = GlobalKey(); final Map _controllers = {}; final Map _fieldByKey = {}; + final Map> _dependentFields = {}; late final Map _initialData; @override void initState() { super.initState(); _initializeControllers(); + _setupDependentFields(); } void _initializeControllers() { @@ -452,6 +453,37 @@ class _EntityFormState extends State { } } + 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 { child: field.buildField( controller: _controllers[field.fieldKey]!, colorScheme: colorScheme, - onChanged: () => setState(() {}), + onChanged: () => _handleFieldChange(field.fieldKey), ), )),