mydomain
No ADS
No ADS

FlutterArtist Form Parent-child MultiOptFormProp ex1

  1. Structure of the example
  2. EmpEmploymentHistory36aShelf
  3. EmploymentHistory36aFormModel
  4. EmploymentHistory36aFormView
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

1. Structure of the example

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

Show More
No ADS