I'm creating a Flutter app to store progress for board game characters, I want to allow a user to check up to 18 checkboxes, where the checkboxes are grouped in threes:
My first approach was to code this widget such that I have as little code reused as possible.
CheckboxGroup is a Row widget with a checkmark icon, ":", and three checkboxes as children.
CheckboxGridRow is a Row widget with two CheckboxGroups as children.
CheckboxGrid is a Column widget with three CheckboxGridRows as children.
-> CheckboxGrid
-> CheckboxGridRow
-> CheckboxGroup
-> CheckboxGroup
-> CheckboxGridRow
-> CheckboxGroup
-> CheckboxGroup
-> CheckboxGridRow
-> CheckboxGroup
-> CheckboxGroup
This works fine for UI purposes, but I'm struggling to wrap my head around how to manage/store state for it. I expect that I will use a List<bool> to store true/false for state, but where should the state change and database logic be for this setup?
In Flutter, the state only goes down the tree. So there aren't many solutions:
The state must be stored by a common ancestor of all of your checkboxes.
From there, it's fairly easy to deduce what "checkbox" and "checkbox group" should do: UI
The checkbox widget
Nothing special. Any checkbox will do, as long as it has:
a "checked" boolean
an "onChanged" callback.
The default Checkbox or Switch should do just fine.
The checkbox group
This widget will map a list of options into a list of checkboxes and determine if they are checked or not.
It'll have usually three parameters:
The list of all options
The list of selected options
an "onChanged" callback
Here's a typical implementation:
#immutable
class Option<T> {
Option({#required this.value, #required this.label});
final T value;
final Widget label;
}
class CheckboxGroup<T> extends StatelessWidget {
CheckboxGroup({
Key key,
#required this.options,
#required this.selectedValues,
#required this.onChange,
}) : super(key: key);
final List<Option<T>> options;
final List<T> selectedValues;
final ValueChanged<List<T>> onChange;
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
for (final option in options) ...[
Checkbox(
key: ObjectKey(option.value),
value: selectedValues.contains(option.value),
onChanged: (res) {
if (res) {
onChange([...selectedValues, option.value]);
} else {
onChange([...selectedValues]..remove(option.value));
}
},
),
option.label,
]
],
);
}
}
The state
Compared to the previous snippet, it's dead simple:
It simply declared a bunch of fields, sends them to CheckboxGroup, and call setState when needed.
enum Foo { a, b, c }
enum Bar { a, b, c }
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
List<Foo> foo = const [];
List<Bar> bar = const [];
#override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
CheckboxGroup<Foo>(
selectedValues: foo,
options: [
for (final value in Foo.values)
Option(value: value, label: Text(value.toString()))
],
onChange: (value) => setState(() => foo = value),
),
CheckboxGroup<Bar>(
selectedValues: bar,
options: [
for (final value in Bar.values)
Option(value: value, label: Text(value.toString()))
],
onChange: (value) => setState(() => bar = value),
),
],
);
}
}
Related
I'm making an app that contains ViewList that have some Widget Inside it,
the Widget has functions And three Inputs (text title, text SubTitle, button with action 'OnPressed' it will change every Widget)
I need to duplicate this Widget 42 Times and every Widget has a different (title, subtitle, button)
so how I can make a loop that duplicates the Widgets and Array to enter a specific (title, subtitle, button) to each Widget?
(more Explanation) array list Contains(title, subtitle, button), every time the loop creates a new Widget in the ViewList, the Widget Get's (title, subtitle, button) from the Arraylist.
I had done that before but not in flutter not even using dart so I'm a little bit confused
this picture explains what I need
press here
This uses a for loop to loop through a the lists of lists containing the information for your widgets, and add each element of the list to a text widget, you just have to make sure the elements of each list are the correct type that you have to pass to the widget.
#override
Widget build(BuildContext context) {
List lists = [
['title', 'subtitle', 'button'],
['title', 'subtitle', 'button'],
['title', 'subtitle', 'button'],
];
return MaterialApp(
title: MyApp._title,
home: Scaffold(
appBar: AppBar(title: const Text(MyApp._title)),
body: SingleChildScrollView(
child: Column(
children: [
for (var i in lists)
Card(
child: Column(
children: [
Text(i[0]),
Text(i[1]),
Text(i[2]),
],
),
)
],
),
)),
);
}
}
I am working to recreate the nested array for Angular Reactive Forms.
Existing Nested Array.
nestedArray = [{
id:'abc',
required:true,
children:[{
id:"bcd",
parentId:"abc",
required:true,
children:[{
id:"cde",
parentId:"bcd",
required:false,
children:[{
id:"def",
parentId:"cde",
required:true,
children:[{
id:"efg",
parentId:"def",
required:false,
}]
},
{
id: "xyz",
parentId: "cde",
required: true,
}]
}]
}]
}]
Recreate this array for Angular Reactive Forms
nestedArray= this.fb.group({
abc: [''],
bcd: this.fb.group({
cde: [''],
efg: [''],
}),
});
Above array is incorrect, looking for the right way to create the children in Angular Reactive Form.
Thanks
You need make a recursive function that return a FormControl or a FormGroup
form = new FormArray(this.nestedArray.map(x=>{
const group=new FormGroup({})
group.addControl(x.id,this.getGroup(x))
return group
}));
getGroup(data: any) {
if (!data.children)
return new FormControl()
const group = new FormGroup({});
if (data.children) {
data.children.forEach(x=>{
group.addControl(x.id,this.getGroup(x))
})
}
return group;
}
See stackblitz
Update really the answer is wrong, becaouse we has no form control to the "parents". the function must be like
form = new FormArray(this.nestedArray.map(x=>this.getGroup(x)))
getGroup(data: any) {
if (!data.children)
{
return new FormControl)
}
const group = new FormGroup({});
group.addControl(data.id,new FormControl())
if (data.children) {
data.children.forEach(x=>{
group.addControl(x.id,this.getGroup(x))
})
}
return group;
}
Well, the question is how conrol this crazy form. We can try create a "recursive template", but I think it's better another aproach. Imagine you has an array of elements, each one with three porperties: "index","label" and "control". And control is a reference to the FormControl of our form. Then we can construct an .html like
<div *ngFor="let item of controls">
<span [style.margin-left]="item.index*10+'px'">{{item.label}}</span>
<input [formControl]="item.control">
<span *ngIf="item.control.errors?.required">*</span>
</div>
easy no? just use directly [formControl] and item.control.errors or item.control.touched
Well, for this first declare an empty array
controls:any[]=[];
And change the fuction so we add the group in two steps: 1.-create the formControl, 2.-Add to the group, the third step is add to our array an object with {label,control and "index"}. Well it's typical when we has a recursive function to want know the "index" of the recursion.
//we pass as index 0 at first
form = new FormArray(this.nestedArray.map(x=>this.getGroup(x,0)))
getGroup(data: any,index:number) {
if (!data.children)
{
//create the control
const control=new FormControl('',data.required?Validators.required:null)
//add to the array this.controls
this.controls.push({control:control,label:data.id,index:index})
//return control
return control
}
const group = new FormGroup({});
index++
//create the control
const control=new FormControl('',data.required?Validators.required:null)
//add to the array this.controls
this.controls.push({control:control,label:data.id,index:index})
//add the control to the group
group.addControl(data.id,control)
if (data.children) {
data.children.forEach(x=>{
group.addControl(x.id,this.getGroup(x,index++))
})
}
return group;
}
Well, In the stackblitz add the property "value" to our complex object, to show how can do it, and we use as "label" the "id" -perhafs our complex object need a new property label-
just a pseudo code below, but you will need to iterate and create the final key-value object to get similar structure...
anyhow there are some tools solving similar issues, consider this: https://jsonforms.io/ - i'd consider those first.
function patch (array: any[]): any {
let res: any = {};
for (let a of array) {
res[a.id] = a;
if (a.children && a.children.length) res[a.id]['children'] = patch(a.children)
}
return res;
}
Based on the firestore database, I've created my own user model.
class ReceiverModel {
final String id;
final String name;
final String contactNumber;
final String bloodGroup;
final String city;
final String accepted;
ReceiverModel({ this.id, this.name, this.contactNumber, this.bloodGroup, this.city, this.accepted });
}
And, in my database file I added a stream to get all the data:
// Receiver List from snapshot
List<ReceiverModel> _receiverListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc){
return ReceiverModel(
id: doc.data['id'] ?? '',
name: doc.data['name'] ?? '',
contactNumber: doc.data['contactNumber'] ?? '',
bloodGroup: doc.data['bloodGroup'] ?? '',
city: doc.data['city'] ?? '',
accepted: doc.data['accepted'] ?? '',
);
}).toList();
}
//get receiverCollection stream
Stream<List<ReceiverModel>> get receivers {
return receiverCollection.snapshots()
.map(_receiverListFromSnapshot);
}
#override
Widget build(BuildContext context) {
return StreamProvider<List<ReceiverModel>>.value(
initialData: List(),
value: ReceiverDatabaseService().receivers,
child: SafeArea(
class _ReceiverListState extends State<ReceiverList> {
#override
Widget build(BuildContext context) {
final receivers = Provider.of<List<ReceiverModel>>(context);
return ListView.builder(
itemCount: receivers.length,
itemBuilder: (context, index) {
return ReceiverTile(receiverModel: receivers[index]);
},
);
}
}
Then I used it to pass data through my screens, and later on I displayed all the data that came throuh the stream by a listview. Anyways, my question is, now as I'm showing all data, I want to create a search button to display any specific bloodGroups or any specific cities, not all the datas. How can I do that?
return StreamProvider<List<ReceiverModel>>.value(
initialData: List(),
value: ReceiverDatabaseService().receivers,
You can use Firestore Query methods against a collection reference to query particular Document Records
The various options you have to query data are like where, limit, orderBy, etc. API Reference
Edit - Takes in input whether you want to fetch all data or search it
In your case, you can change snapshots functions to something like this
Stream<List<ReceiverModel>> fetchReceivers({String bloodGroup}) {
var query = receiverCollection;
if(bloodGroup!=null)
query = query.where("bloodGroup",isEqualTo:bloodGroup);
return query.snapshots().map(_receiverListFromSnapshot);
}
For adding a search button with a strict search you can call the function with the typed-in value.
I hope I have solved your query. Thanks and have a good day
I am using Fluent UI DetailsList. My table looks like below:
I need filters below every column (text or drop-down) as shown below:
Please let me know if this is possible? Or maybe a way to display custom header (using html) ?
This actually turned out to be easier than I thought it'd be...
If you're ok with clicking the column header to reveal the choices (vs having the dropdown directly under the title) then this can be achieved using the ContextualMenu component in conjunction with DetailsList. I got it working by tweaking from the variable row height example in the official docs: https://developer.microsoft.com/en-us/fluentui#/controls/web/detailslist/variablerowheights.
Add a ContextualMenu underneath your DetailsList:
<DetailsList
items={items}
columns={columns}
/>
{this.state.contextualMenuProps && <ContextualMenu {...this.state.contextualMenuProps} />}
Inside your column definition, set the hasDropdown action so the user gets a UI indicator that they can/should click the header, and call a contextMenu method (note I'm using onColumnContextMenu as well as onColumnClick so it doesn't matter if they left or right click the header:
{
key: 'dept',
name: 'Department',
fieldName: 'dept',
minWidth: 125,
maxWidth: 200,
onColumnContextMenu: (column, ev) => {
this.onColumnContextMenu(column, ev);
},
onColumnClick: (ev, column) => {
this.onColumnContextMenu(column, ev);
},
columnActionsMode: ColumnActionsMode.hasDropdown,
}
When the onColumnContextMenu method gets invoked, we need to build the context menu properties that will get consumed by the ContextualMenu component. Note the dismissal method as well, which clears out the state so the menu is hidden.
private onContextualMenuDismissed = (): void => {
this.setState({
contextualMenuProps: undefined,
});
}
private onColumnContextMenu = (column: IColumn, ev: React.MouseEvent<HTMLElement>): void => {
if (column.columnActionsMode !== ColumnActionsMode.disabled) {
this.setState({
contextualMenuProps: this.getContextualMenuProps(ev, column),
});
}
};
Finally, inside of getContextualMenuProps you need to determine what the options should be for the user to click. In this example, I'm simply giving sort options (you'll need to add an onClick handler to actually do something when the user clicks the item), but I'll use the column to determine what those items should actually be and paint the filters into the items collection so the user can select one to filter.
private getContextualMenuProps = (ev: React.MouseEvent<HTMLElement>, column: IColumn): IContextualMenuProps => {
const items: IContextualMenuItem[] = [
{
key: 'aToZ',
name: 'A to Z',
iconProps: { iconName: 'SortUp' },
canCheck: true,
checked: column.isSorted && !column.isSortedDescending,
},
{
key: 'zToA',
name: 'Z to A',
iconProps: { iconName: 'SortDown' },
canCheck: true,
checked: column.isSorted && column.isSortedDescending,
}
];
return {
items: items,
target: ev.currentTarget as HTMLElement,
directionalHint: DirectionalHint.bottomLeftEdge,
gapSpace: 10,
isBeakVisible: true,
onDismiss: this.onContextualMenuDismissed,
}
}
Note the target on the ContextualMenuProps object, which is what tells the ContextualMenu where to lock itself onto (in this case, the column header that you clicked to instantiate the menu.
Detail list filter for each column without context menu -
https://codesandbox.io/s/rajesh-patil74-jzuiy?file=/src/DetailsList.CustomColumns.Example.tsx
For instance - Providing filter in text field associated with each column will apply filter on color column.
I added TextEditingController to my TextFormField for reading the user input, but I want to read the input at every update in the TextFormField, and the Controller shows previous updates. In short I want an alternative to something like onChanged method in TextField, since I'm using this for a form, I need to use TextFormField. Suggest me something.
Just add a listener to the TextEditingController.
something like below.
#override
void initState() {
super.initState();
_editingController.addListener(() {
print(_editingController.text);
});
}
#override
void dispose() {
// Clean up the controller when the Widget is disposed
_editingController.dispose();
super.dispose();
}
Hope it helps!
use the onChange(text) function of the TextFormField with a ValueNotifier, this should help you out.
TextFormField(
controller: _Controller,
label: "Input",
currentNode: _Node,
nextNode: FocusNode(),
keyboard: TextInputType.visiblePassword,
onChanged: (text) {
_avalueNotifier.value = text;
},
inputFormatters: <TextInputFormatter>[
BlacklistingTextInputFormatter.singleLineFormatter,
],
validator: (String value) {
if (value.isEmpty) {
return 'Please make inputs';
}
return null;
},
),