How to call a form from an 'Add New' DropdownButtonFormField - flutter

I'm a degreed 25 yr computer scientist but new to Flutter and mobile apps in general.
I am building forms now and want to append an 'Add New' item at the bottom of a list of existing members in a DropDownButtonFormField. If a user selects the 'Add New' item, I'd like to call a 'Register New Member' form for them to fill out. When submitted, I'd like to return the user to the original form, populate the new member info into the field they were working on, and continue with the rest of the form.
Sounds reasonable, right?
The only thing I've tried thus far is using the onChanged: event using a Hero as shown below.
This doesn't call the MemberForm() at all, but does populate 'Add New Contact' in the field.
I use this same Hero approach when adding new members from a Button and it works fine.
Any help would be appreciated!
DropdownButtonFormField(
decoration: InputDecoration(...),
value: mainContact,
style: const TextStyle(fontSize: 20, color: Colors.black54),
items: members
.map((member) => DropdownMenuItem<String>(
value: member,
child: Text(member),
))
.toList(),
onChanged: (member) =>
member == 'Add New Contact' ?
const Hero(tag: 'Hero-Member', child: MemberForm()) :
setState(() => mainContact = member),
validator: (value) {...},
),

It depends on UX how you like to add new item. but make sure to await and then setState to update main ui. This demo will show the flow with modalBottomSheet
class TestWidget extends StatefulWidget {
const TestWidget({super.key});
#override
State<TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
List<String> items = ['Add New Contact', "A", "B"];
String? mainContact;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
DropdownButtonFormField(
items: items
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e),
),
)
.toList(),
onChanged: (member) async {
if (member == 'Add New Contact') {
const Hero(tag: 'Hero-Member', child: Text("MemberForm"));
final TextEditingController controller =
TextEditingController();
final String newData = await showModalBottomSheet(
context: context,
builder: (context) => Column(
children: [
TextField(
controller: controller,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(controller.text);
},
child: Text("Save"),
),
],
));
//you can also get text directly from `controller`
if (newData.isNotEmpty) {
items.add(newData);
}
}
mainContact = member;
setState(() {});
},
)
],
),
);
}
}

Related

How can I make dropdownbutton using Getx in flutter?

I'm trying to make dropdownbutton using Getx in flutter
However, it doesn't work.
Even if I choose a value, the value does not been selected.
class BecomePlayerPage2 extends GetView<BecomePlayerController> {
const BecomePlayerPage2 ({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Obx(
() => Scaffold(
body: Padding(
padding: EdgeInsets.all(20),
child:
DropdownButton<RxString>(
onChanged: (newValue){
controller.selected=newValue!;
},
value: controller.selected,
items: [
for(var value in controller.tierList)
DropdownMenuItem(
child: new Text(
value,
),
value: value.obs,
),
]
),
),
),
}
class BecomePlayerController extends GetxController {
final tierList=['first', 'second', 'third'].obs;
var selected = "first".obs;
}
This is my first day of studying Getx so it is so hard to match with basic widget.
What should I do?
Try replacing the onChange statement with
onChanged: (newValue) {
controller.selected.value = newValue.toString();
},
or by changing Dropdown button type from RXString to String
return Obx(
() => Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: DropdownButton<String>( // updated
onChanged: (newValue) {
controller.selected.value = newValue!; //updated
},
value: controller.selected.value, //updated
items: [
for (var value in controller.tierList)
DropdownMenuItem(
value: value,
child: Text(
value, //updated
),
),
]),
),
),
);
Use round brackets to update .obs and change RxString to String
authController.selected(newValue.toString());
onChanged: (newValue){
// controller.selected=newValue!;
controller.selected.value =newValue!;
},

Keep dropdown open when active checkbox

I have a code that is responsible for filtering by certain categories (I shortened it for ease of reading). When opening the filter window, the user sees these category names ('Select a brand', 'Select a operation system', 'Select a color' etc).
Next, the user can open the category (initially, the dropdown list is in the closed position.), and select the parameters from the drop-down list (and click the apply button). The next time you open the filter window, the checkboxes in front of the parameters remain, but the drop-down list collapses.
Tell me how to do it: if in any category there are options marked with a checkmark, so that the drop-down list will be open the next time the window with filters is opened.
class FilterDialog extends StatefulWidget {
final void Function(Map<String, List<String>?>) onApplyFilters;
final Map<String, List<String>?> initialState;
const FilterDialog({
Key? key,
required this.onApplyFilters,
this.initialState = const {},
}) : super(key: key);
#override
State<FilterDialog> createState() => _FilterDialogState();
}
class _FilterDialogState extends State<FilterDialog> {
// Temporary storage of filters.
Map<String, List<String>?> filters = {};
bool needRefresh = false;
// Variable for the ability to hide all elements of filtering by any parameter.
bool isClickedBrand = false;
List manufacturer = [];
#override
void initState() {
super.initState();
filters = widget.initialState;
}
// A function to be able to select an element to filter.
void _handleCheckFilter(bool checked, String key, String value) {
final currentFilters = filters[key] ?? [];
if (checked) {
currentFilters.add(value);
} else {
currentFilters.remove(value);
}
setState(() {
filters[key] = currentFilters;
});
}
// Building a dialog box with filters.
#override
Widget build(BuildContext context) {
return SimpleDialog(
// Window title.
title: const Text('Filters',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w600,
)),
contentPadding: const EdgeInsets.all(16),
// Defining parameters for filtering.
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Here and in subsequent Column, there will be a definition of parameters for filtering,
// a title, the ability to hide/show the items of list
Column(children: [
InkWell(
onTap: () async {
manufacturer = await getManufacturerOptions();
setState(() {
isClickedBrand = !isClickedBrand;
});
},
child: Row(children: [
Text('Select a brand'.toString(),
style: const TextStyle(
fontSize: 18,
)),
const Spacer(),
isClickedBrand
? const Icon(Icons.arrow_circle_up)
: const Icon(Icons.arrow_circle_down)
])),
!isClickedBrand
? Container()
: Column(
children: manufacturer
.map(
(el) => CustomCheckboxTile(
value: filters['manufacturer']?.contains(el) ??
false,
label: el,
onChange: (check) =>
_handleCheckFilter(check, 'manufacturer', el),
),
)
.toList())
]),
const SizedBox(
height: 5,
),
// Building a button to apply parameters.
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
widget.onApplyFilters(filters);
needRefresh = true;
},
child:
const Text('APPLY', style: TextStyle(color: Colors.black)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.grey),
)),
// Building a button to reset parameters.
const SizedBox(
height: 5,
),
ElevatedButton(
onPressed: () async {
setState(() {
filters.clear();
});
widget.onApplyFilters(filters);
},
child: const Text('RESET FILTERS',
style: TextStyle(color: Colors.black)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.grey),
)),
],
),
],
);
}
}
For example: the user clicks on the filter box, selects the brands to search for, and clicks the apply button. My task is that the next time the user opens the filter window, the categories with active checkboxes (in this example, the brand) are in an expanded state
The concept is, you need to check filter data with while opening dialog, To simplify the process I am using ExpansionTile. You can check this demo and customize the behavior and look.
Run on dartPad, Click fab to open dialog and touch outside the dialog to close this.
class ExTExpample extends StatefulWidget {
ExTExpample({Key? key}) : super(key: key);
#override
State<ExTExpample> createState() => _ExTExpampleState();
}
class _ExTExpampleState extends State<ExTExpample> {
// you can use map or model class or both,
List<String> filter_data = [];
List<String> brands = ["Apple", "SamSung"];
List<String> os = ["iOS", "Android"];
_showFilter() async {
await showDialog(
context: context,
builder: (c) {
// you can replace [AlertDialog]
return AlertDialog(
content: StatefulBuilder(
builder: (context, setSBState) => SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ExpansionTile(
title: const Text("Brand"),
/// check any of its's item is checked or not
initiallyExpanded: () {
// you can do different aproach
for (final f in brands) {
if (filter_data.contains(f)) return true;
}
return false;
}(),
children: [
...brands.map(
(brandName) => CheckboxListTile(
value: filter_data.contains(brandName),
title: Text(brandName),
onChanged: (v) {
if (filter_data.contains(brandName)) {
filter_data.remove(brandName);
} else {
filter_data.add(brandName);
}
setSBState(() {});
//you need to reflect the main ui, also call `setState((){})`
},
),
),
],
),
ExpansionTile(
title: const Text("select OS"),
/// check any of its's item is checked or not
initiallyExpanded: () {
// you can do different aproach
for (final f in os) {
if (filter_data.contains(f)) return true;
}
return false;
}(),
children: [
...os.map(
(osName) => CheckboxListTile(
value: filter_data.contains(osName),
title: Text(osName),
onChanged: (v) {
if (filter_data.contains(osName)) {
filter_data.remove(osName);
} else {
filter_data.add(osName);
}
setSBState(() {});
//you need to reflect the main ui, also call `setState((){})`
},
),
),
],
),
],
),
),
),
);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FloatingActionButton(
onPressed: () {
_showFilter();
},
),
),
);
}
}
Whenever you open filter the isClickedBrand is False so it won't showed you a list. So the solution is :
After selecting option from list, change the state of isClickedBrand state. I mean if it's true then it will show the list otherwise show container.
Hope you get my point.

clear DropDownButton flutter

i have a DropDownButton which add category for my product. after added product DropDownButton display category which i choosed. I want to refresh or clear DropDownButton after adding product. Better to display hint text.
code:
child: StreamBuilder<List<Categories>>(
stream: categories,
builder: ((context, snapshot) {
if (!snapshot.hasData)
return CircularProgressIndicator();
return DropdownButton(
hint: Text('choose category'),
value: _currentCategory,
items: snapshot.data
.map((doc) => DropdownMenuItem(
child: Text(doc.categoryname),
value: doc,
))
.toList(),
onChanged: (selectedCategory) =>
setState(() {
_currentCategory = selectedCategory;
}),
);
})),
),
SizedBox(height: 15),
RaisedButton(
onPressed: () {
if (_formKeyProduct.currentState.validate()) {
ProductService().addProduct(
_nameproductController.text,
_priceproductController.text,
_currentCategory.categoryname.toString());
_formKeyProduct.currentState.reset();
_nameproductController.clear();
_priceproductController.clear();
}
},
child: Text('add product'),
Since the value you chose is _currentCategory using
setState(() {
_currentCategory = null;
}
)
should do the trick.
EDIT:
I see some downvotes on this. This answer was based on Flutter 1.22.6. If it doesn't work on Flutter 2 that is why.
This is a complete widget including a button to reset the dropdown value. It is based on provider package. The widget include a button to clear selection.
class MyDataDropDown extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Consumer<HomeModel>(
builder: (context, model, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(10)),
child: Row(
children: [
Expanded(
child: DropdownButton<SearchMyData>(
isExpanded: true,
value: model.currentMyData,
onChanged: (final SearchMyData? newValue) {
model.setCurrentMyData(newValue);
},
items: model.data.map<DropdownMenuItem<SearchMyData>>((SearchMyData value) {
return DropdownMenuItem<SearchMyData>(
value: value,
child: Text(MyDataDataUtils.getMyDataLabelDropDown(value), overflow: TextOverflow.ellipsis),
);
}).toList(),
// add extra sugar..
hint: const Text("Pick a data"),
icon: const Icon(Icons.arrow_drop_down),
underline: const SizedBox(),
),
),
IconButton( // clear dropdown button
onPressed: () => model.setCurrentMyData(null),
icon: Icon(Icons.clear))
],
),
);
},
);
}
}
Provider class
class HomeModel extends ChangeNotifier{
....
UnmodifiableListView<SearchTheme> get data => UnmodifiableListView([your data here]);
SearchMyData? _selectedMyData;
SearchMyData? get currentMyData => _selectedMyData;
setCurrentMyData(final SearchMyData? item) {
_selectedMyData = item;
notifyListeners();
}
}
Now you have a complete dropdown including clear selection button.

How to call specific validator instead of calling all the validators in same time by using `form.validate()`?

This is the code :
if (form.validate()) {
// Text forms was validated.
if (mounted) {
setState(() {
_isButtonEnabled = true;
});
}
form.save();
}
I want to call specific validator for specific TextFormField
Joao's answer is correct, but it's also over-engineered. You can instead declare a GlobalKey for each field that you want to validate independently:
GlobalKey<FormFieldState> fieldKey = GlobalKey();
Assign it to the TextFormField that you want to validate:
TextFormField(
key: fieldKey,
...
And then use the key to validate that field:
bool valid = fieldKey.currentState.validate();
You will need to wrap each field you want to validate individually with a Form Widget and it's own key:
class TextFormFieldValidation extends StatefulWidget {
#override
_TextFormFieldValidationState createState() => _TextFormFieldValidationState();
}
class _TextFormFieldValidationState extends State<TextFormFieldValidation> {
List<GlobalKey<FormState>> _formKeysList= [
GlobalKey<FormState>(),
GlobalKey<FormState>(),
];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Form(
key: _formKeysList[0],
child: TextFormField(
validator: (value) => 'Bad',
),
),
Form(
key: _formKeysList[1],
child: TextFormField(
validator: (value) => 'Bad',
),
),
RaisedButton(
child: Text('Validate 1'),
onPressed: () => _formKeysList[0].currentState.validate(),
),
RaisedButton(
child: Text('Validate 2'),
onPressed: () => _formKeysList[1].currentState.validate(),
),
RaisedButton(
child: Text('Reset'),
onPressed: () => _formKeysList.forEach((key) => key.currentState.reset()),
),
],
);
}
}

Flutter TextFormField not updating value on rebuild

I'm trying to make it possible to edit a list of strings. The problem is, when I delete a list item via the IconButton, the TextField rebuilds but somehow still has the old value. With debugging I see, that my List of Strings are however updated correctly, meaning the deleted String is actually being deleted in my list.
This is my code:
class EditInfoItemDialog extends StatefulWidget {
#override
State<StatefulWidget> createState() => _EditInfoitemDialogState();
}
class _EditInfoitemDialogState extends State<EditInfoItemDialog> {
final _formKey = GlobalKey<FormState>();
List<String> values = ['A', 'B', 'C'];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
child: Column(
children: <Widget>[
...values.asMap().map((int index, String point) {
return MapEntry(index, Row(
children: [
Text(index.toString()),
Expanded(
child: TextFormField(
decoration: InputDecoration(hintText: 'Info'),
initialValue: values[index],
onChanged: (value) => setState(() {values[index] = value; }),
),
),
IconButton(
icon: Icon(Icons.delete),
color: Colors.red,
onPressed: () {
setState(() {
values.removeAt(index);
print(values);
});
},
)
]
));
}).values.toList(),
FlatButton(
child: Text('Add Bulletpoint'),
onPressed: () {
setState(() {
if (values == null) values = [];
values.add('');
});
},
)
],
),
),
);
}
Any Ideas?
You need to add a key to your TextFormField like this:
key: ObjectKey(values[index])
Here is an explanation and an example with the reason why you need to add a key in this case: What are Keys in the Stateless widgets class?
Key is something used by flutter engine at the step of recognizing
which widget in a list as changed
More info:
key property