FlutterArtist Form Parent-child MultiOptFormProp ex1
During application development, you will often encounter Form with dependent fields. One field acts as the parent, while another acts as the child, meaning the value of the child field changes depending on the selection made in the parent field.
To illustrate this scenario, let's look at an example of an Employment History form for an employee.
In this example, the user selects an employee from the list on the left side of the screen to view a list of that employee's "employment history" records. The user can then add or modify these "employment history" records on a Form with the following information:
- From date – To date
- Company
- Department
- Job position

No ADS
In this form, pay attention to the Company and Department fields. These two fields have a parent–child relationship.
- The user first selects a company from the Dropdown 1 (Company)
- After that, the Dropdown 2 (Department) will automatically update the list of departments corresponding to the selected company.
In other words, the Department data depends on the selected Company.
Note: If you are new to Form in FlutterArtist, you should first check out the basic example below. It explains the fundamental concepts before moving on to Form with dependent fields.
- Download FlutterArtist Demo
2. EmpEmploymentHistory36aShelf
EmpEmploymentHistory36aShelf contains two Blocks that have a parent–child relationship. In this structure, EmploymentHistory36aBlock is the child Block, and it contains a FormModel used to manage the form data.
emp_employment_history36a_shelf.dart
class EmpEmploymentHistory36aShelf extends Shelf {
@override
ShelfStructure defineShelfStructure() {
return ShelfStructure(
filterModels: {},
blocks: [
Employee36aBlock(
name: Employee36aBlock.blkName,
description: null,
config: BlockConfig(),
filterModelName: null,
formModel: null,
childBlocks: [
EmploymentHistory36aBlock(
name: EmploymentHistory36aBlock.blkName,
description: null,
config: BlockConfig(),
filterModelName: null,
formModel: EmploymentHistory36aFormModel(),
childBlocks: null,
),
],
),
],
);
}
Employee36aBlock findEmployee36aBlock() {
return findBlock(Employee36aBlock.blkName) as Employee36aBlock;
}
EmploymentHistory36aBlock findEmploymentHistory36aBlock() {
return findBlock(EmploymentHistory36aBlock.blkName)
as EmploymentHistory36aBlock;
}
}3. EmploymentHistory36aFormModel
The list of Form properties is defined in the EmploymentHistory36aFormModel class.
employment_history36a_form_model.dart
class EmploymentHistory36aFormModel
extends
FormModel<
int, //
EmploymentHistoryData,
EmptyFormInput,
EmptyAdditionalFormRelatedData
> {
@override
FormModelStructure defineFormModelStructure() {
return FormModelStructure(
simplePropDefs: [
SimpleFormPropDef<int>(propName: "id"),
SimpleFormPropDef<int>(propName: "employeeId"),
SimpleFormPropDef<DateTime>(propName: "fromDate"),
SimpleFormPropDef<DateTime>(propName: "toDate"),
],
multiOptPropDefs: [
// Multi Option Single Selection Prop.
MultiOptFormPropDef<EmployeePositionInfo>.singleSelection(
propName: 'employeePosition',
),
// Multi Option Single Selection Prop.
MultiOptFormPropDef<CompanyInfo>.singleSelection(
propName: "company",
children: [
// Multi Option Single Selection Prop.
MultiOptFormPropDef<DepartmentInfo>.singleSelection(
propName: "department",
),
],
),
],
);
}
...
}In this example, pay attention to how the department property is nested inside the company property. This declaration clearly expresses the parent–child relationship between the two fields.

MultiOptFormPropDef.singleSelection()
Defines a MultiOptFormProp where the user can select at most one option.
MultiOptFormPropDef<DepartmentInfo>.singleSelection
// Multi Option Single Selection Prop.
MultiOptFormPropDef<DepartmentInfo>.singleSelection(
propName: "department",
)
MultiOptFormPropDef.multiSelection()
Defines a MultiOptFormProp where the user can select multiple options simultaneously.
MultiOptFormPropDef<DepartmentInfo>.multiSelection
// Multi Option Multi Selection Prop.
MultiOptFormPropDef<DepartmentInfo>.multiSelection(
propName: "department",
)
FormModel.performLoadMultiOptPropXData()
The performLoadMultiOptPropXData() method is called sequentially for each MultiOptFormProp to load their data. The core rule here is: root properties are loaded first, followed by child properties, and finally leaf properties.
performLoadMultiOptPropXData()
@override
Future<XData?> performLoadMultiOptPropXData({
required String multiOptPropName,
required SelectionType selectionType,
required Object? parentMultiOptPropValue,
required EmptyFormInput? formInput,
required EmploymentHistoryData? itemDetail,
required EmptyAdditionalFormRelatedData formRelatedData,
}) async {
if (multiOptPropName == "employeePosition") {
ApiResult<EmployeePositionInfoPage> result =
await _employeePositionRestProvider.queryAll();
// IMPORTANT:
result.throwIfError();
// IMPORTANT: Generics should be declared explicitly.
return ListXData<int, EmployeePositionInfo>.fromPageData(
pageData: result.data,
getItemId: (item) => item.id,
);
} else if (multiOptPropName == "company") {
ApiResult<CompanyInfoPage> result = await _companyRestProvider.queryAll();
// IMPORTANT:
result.throwIfError();
// IMPORTANT: Generics should be declared explicitly.
return ListXData<int, CompanyInfo>.fromPageData(
pageData: result.data,
getItemId: (item) => item.id,
);
} else if (multiOptPropName == "department") {
CompanyInfo company = parentMultiOptPropValue as CompanyInfo;
ApiResult<DepartmentInfoPage> result = await _departmentRestProvider
.queryAllByCompanyId(companyId: company.id);
// IMPORTANT:
result.throwIfError();
// IMPORTANT: Generics should be declared explicitly.
return ListXData<int, DepartmentInfo>.fromPageData(
pageData: result.data,
getItemId: (item) => item.id,
);
}
return null;
}Guaranteed Parent Value:
For the department property, the library guarantees that parentMultiOptPropValue is non-null before execution. This eliminates common "null pointer" errors in nested dropdowns.
- FlutterArtist ApiResult
- FlutterArtist XData
FormModel.extractSimplePropValuesFromItemDetail()
When a user selects an existing ITEM, the library fetches the full record (ITEM_DETAIL). The extractSimplePropValuesFromItemDetail() method is then called to extract values from this record and update the corresponding SimpleFormProp(s).
extractSimplePropValuesFromItemDetail()
@override
Map<String, dynamic>? extractSimplePropValuesFromItemDetail({
required Object? parentBlockCurrentItemId,
required EmploymentHistoryData itemDetail,
required EmptyAdditionalFormRelatedData formRelatedData,
}) {
return {
"id": itemDetail.id,
"employeeId": itemDetail.employee.id,
"fromDate": itemDetail.fromDate,
"toDate": itemDetail.toDate,
};
}No ADS
FormModel.extractMultiOptPropValueFromItemDetail()
Similarly, the extractMultiOptPropValueFromItemDetail() method is called for each MultiOptFormProp. Its task is to identify the values for fields like company or department within the detailed record to render them correctly on the form.
extractMultiOptPropValueFromItemDetail()
@override
OptValueWrap? extractMultiOptPropValueFromItemDetail({
required String multiOptPropName,
required SelectionType selectionType,
required XData multiOptPropXData,
required Object? parentMultiOptPropValue,
required EmploymentHistoryData itemDetail,
required EmptyAdditionalFormRelatedData formRelatedData,
}) {
if (multiOptPropName == "employeePosition") {
// Use "OptValueWrap.single" for MultiOptFormPropDef.singleSelection:
return OptValueWrap.single(itemDetail.employeePosition);
} else if (multiOptPropName == "company") {
// Use "OptValueWrap.single" for MultiOptFormPropDef.singleSelection:
return OptValueWrap.single(itemDetail.company);
} else if (multiOptPropName == "department") {
// Use "OptValueWrap.single" for MultiOptFormPropDef.singleSelection:
return OptValueWrap.single(itemDetail.department);
}
return null;
}4. EmploymentHistory36aFormView
The EmploymentHistory36aFormView class creates the user interface for the Form.

Pay attention to the input fields; these are standard input fields from the flutter_form_builder library package. Each input field has a one-to-one name corresponding to a FormProp of the same name defined in FormModel.
- FlutterArtist Fa FormBuilder
employment_history36a_form_view.dart
class EmploymentHistory36aFormView
extends FormView<EmploymentHistory36aFormModel> {
const EmploymentHistory36aFormView({super.key, required super.formModel});
@override
Widget buildContent(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: FaFormBuilderDateTimePicker.topLabel(
name: "fromDate",
labelText: "From Date",
pattern: "yyyy-MM-dd",
inputType: InputType.date,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
),
),
SizedBox(width: 10),
Expanded(
child: FaFormBuilderDateTimePicker.topLabel(
name: "toDate",
labelText: "To Date",
pattern: "yyyy-MM-dd",
inputType: InputType.date,
),
),
],
),
SizedBox(height: 12),
FaFormBuilderDropdown.topLabel(
name: "company",
labelText: "Company",
items: formModel.getMultiOptPropData("company") ?? [],
getItemText: (item) => item.name,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
),
SizedBox(height: 12),
FaFormBuilderDropdown.topLabel(
name: "department",
labelText: "Department",
items: formModel.getMultiOptPropData("department") ?? [],
getItemText: (item) => item.name,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
),
SizedBox(height: 12),
FaFormBuilderDropdown<EmployeePositionInfo>.topLabel(
name: "employeePosition",
labelText: "Position",
items: formModel.getMultiOptPropData("employeePosition") ?? [],
getItemText: (item) => item.name,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
),
],
);
}
}No ADS
FlutterArtist
- Basic concepts in Flutter Artist
- FlutterArtist Block ex1
- FlutterArtist Filter Example
- FlutterArtist FilterModel MultiOptFilterCriterion ex1
- FlutterArtist FilterInput Example 1
- FlutterArtist Form ex1
- The idea of designing filter models in FlutterArtist
- FlutterArtist FormModel.patchFormFields() Ex1
- FlutterArtist BlockQuickItemUpdateAction Example
- FlutterArtist BlockNumberPagination Ex1
- FlutterArtist GridView Infinite Scroll Example
- FlutterArtist BlockQuickMultiItemCreationAction Example
- FlutterArtist ListView Infinite Scroll Pagination Example
- FlutterArtist Pagination
- FlutterArtist Sort DropdownSortPanel Example
- FlutterArtist Dio
- FlutterArtist BlockBackendAction Example
- FlutterArtist BackgroundWebDownloadAction Example
- FlutterArtist StorageBackendAction ex1
- FlutterArtist Block External Shelf Event ex1
- FlutterArtist Filter FormBuilderMultiDropDown Ex1
- FlutterArtist Master-detail Blocks ex1
- FlutterArtist Scalar ex1
- FlutterArtist Pagination Davi table Infinite Scroll Ex1
- FlutterArtist Filter Tree FormBuilderField ex1
- FlutterArtist Filter FormBuilderRadioGroup ex1
- FlutterArtist Form Parent-child MultiOptFormProp ex1
- FlutterArtist Manual Sorting ReorderableGridView Example
- FlutterArtist Manual Sorting ReorderableListView
- FlutterArtist Scalar External Shelf Event ex1
- FlutterArtist Code Flow Viewer
- FlutterArtist Log Viewer
- FlutterArtist config
- FlutterArtist StorageStructure
- FlutterArtist Debug Storage Viewer
- FlutterArtist DebugMenu
- FlutterArtist Debug UI Components Viewer
- FlutterArtist Debug Shelf Structure Viewer
- FlutterArtist Context Provider Views
- FlutterArtist FilterModelStructure ex1
- FlutterArtist FilterModelStructure ex2
- FlutterArtist FilterModelStructure ex3
- FlutterArtist Internal Shelf Event ex1
- FlutterArtist Deferring External Shelf Events (Ex1)
- FlutterArtist DropdownSortPanel
Show More
