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