This commit is contained in:
string 2025-09-17 10:36:16 +05:30
parent 0805cf44e4
commit 70c992ddb9
3 changed files with 447 additions and 4 deletions

View File

@ -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',
// /// ),
// /// ];
// /// }
// /// }
// /// ```

View File

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

View File

@ -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),
),
)),