mydomain
No ADS
No ADS

FlutterArtist Filter FormBuilderMultiDropDown Ex1

  1. Structure of the example
  2. Employee20aShelf
  3. Employee20aFilterModel
  4. Employee20aFilterCriteria
  5. Employee20aBlock
  6. Employee20aFilterPanel
In this article, we will explore how to configure an advanced filter with parent-child criteria. The highlight of this example is that the child criterion, "departments" allows Multi-selection, providing a flexible and powerful filtering experience.
Our discussion focuses on the "Demo20a" example in the FlutterArtist Demo. Here, Employee20aFilterModel sets up three search criteria: Employee Name, Company, and Departments. Notably, Company and Departments are bound by a parent-child relationship: once a Company is selected, the corresponding Department list reloads to ensure data integrity.
Note: If you are new to FlutterArtist Filter, please look at the example below first instead of this one.
  • Download FlutterArtist Demo

1. Structure of the example

2. Employee20aShelf

The structure of the Shelf in this example is very simple, consisting only of a FilterModel and a Block.
employee20a_shelf.dart
class Employee20aShelf extends Shelf {
  @override
  ShelfStructure defineShelfStructure() {
    return ShelfStructure(
      filterModels: {
        Employee20aFilterModel.filterName: Employee20aFilterModel(),
      },
      blocks: [
        Employee20aBlock(
          name: Employee20aBlock.blkName,
          description: null,
          config: BlockConfig(),
          filterModelName: Employee20aFilterModel.filterName,
          formModel: null,
          childBlocks: [],
        ),
      ],
    );
  }

  Employee20aFilterModel findEmployee20aFilterModel() {
    return findFilterModel(Employee20aFilterModel.filterName)
        as Employee20aFilterModel;
  }

  Employee20aBlock findEmployee20aBlock() {
    return findBlock(Employee20aBlock.blkName) as Employee20aBlock;
  }
}

3. Employee20aFilterModel

This is the most critical part of the article. Pay close attention to how we define the "company" (Parent) and "departments" (Child) criteria.
Employee20aFilterModel.defineFilterModelStructure()
@override
FilterModelStructure defineFilterModelStructure() {
  return FilterModelStructure(
    simpleCriterionDefs: [
      SimpleCriterionDef<String>(criterionBaseName: "searchText"),
    ],
    multiOptCriterionDefs: [
      MultiOptCriterionDef<CompanyInfo>.singleSelection(
        criterionBaseName: "company",
        fieldName: 'companyId',
        toFieldValue: (CompanyInfo? rawValue) {
          return SimpleVal.ofInt(rawValue?.id);
        },
        children: [
          // Multi Option Multi Selection Criterion.
          MultiOptCriterionDef<DepartmentInfo>.multiSelection(
            criterionBaseName: "departments",
            fieldName: 'departmentId',
            toFieldValue: (DepartmentInfo? rawValue) {
              return SimpleVal.ofInt(rawValue?.id);
            },
          ),
        ],
      ),
    ],
    conditionConnector: ConditionConnector.and,
    conditionDefs: [
      ConditionDef.condition(
        tildeCriterionName: "searchText~",
        operator: FilterOperator.containsIgnoreCase,
      ),
      ConditionDef.condition(
        tildeCriterionName: "company~",
        operator: FilterOperator.equalTo,
      ),
      ConditionDef.condition(
        tildeCriterionName: "departments~",
        operator: FilterOperator.inCollection,
      ),
    ],
  );
}
Let's look at how to define the criterion "departments":
MultiOptCriterionDef<DepartmentInfo>.multiSelection
MultiOptFilterCriterionDef<DepartmentInfo>.multiSelection(
  criterionBaseName: "departments",
  fieldName: 'departmentId',
  toFieldValue: (DepartmentInfo? rawValue) {
    return SimpleVal.ofInt(rawValue?.id);
  },
),
Note on the difference between singleSelection and multiSelection:
In the FlutterArtist system, MultiOptFilterCriterionDef.multiSelection() is specifically designed to handle multi-item picklists. However, there is a crucial rule to remember: unlike singleSelection (which can act as a "Parent" and contain nested children), multiSelection is always treated as a "Leaf" criterion. This means it CANNOT contain any further child criteria beneath it.
No ADS
FilterModel.performLoadMultiOptTildeCriterionXData()
The performLoadMultiOptTildeCriterionXData() method is invoked sequentially for each MultiOptTildeFilterCriterion. The library follows a golden rule: Root criteria data is always loaded first, followed by Child criteria, and finally the Leaf criteria.
performLoadMultiOptTildeCriterionXData()
@override
Future<XData?> performLoadMultiOptTildeCriterionXData({
  required String multiOptTildeCriterionName,
  required String multiOptCriterionBaseName,
  required Object? parentMultiOptTildeCriterionValue,
  required SelectionType selectionType,
  required EmptyFilterInput? filterInput,
}) async {
  if (multiOptTildeCriterionName == "company~") {
    ApiResult<CompanyInfoPage> result = await _companyProvider.queryAll();
    // Throw ApiError
    result.throwIfError();
    //
    return ListXData<int, CompanyInfo>.fromPageData(
      pageData: result.data,
      getItemId: (item) => item.id,
    );
  } else if (multiOptTildeCriterionName == "departments~") {
    //
    // Because "departments" is child of "company".
    // So [parentMultiOptTildeCriterionValue] value is definitely non-null,
    // this guaranteed by the library.
    //
    CompanyInfo company = parentMultiOptTildeCriterionValue as CompanyInfo;
    ApiResult<DepartmentInfoPage> result = await _departmentRestProvider
        .queryAllByCompanyId(companyId: company.id);
    result.throwIfError();
    //
    return ListXData<int, DepartmentInfo>.fromPageData(
      pageData: result.data,
      getItemId: (item) => item.id,
    );
  }
  return null;
}
  • FlutterArtist Debug Filter Model Viewer
  • FlutterArtist ApiResult
  • FlutterArtist XData
FilterModel.specifyDefaultValueForMultiOptTildeCriterion()
This method is called to specify a default value for each MultiOptTildeFilterCriterion.
specifyDefaultValueForMultiOptTildeCriterion()
@override
OptValueWrap? specifyDefaultValueForMultiOptTildeCriterion({
  required String multiOptTildeCriterionName,
  required String multiOptCriterionBaseName,
  required Object? parentMultiOptTildeCriterionValue,
  required SelectionType selectionType,
  required XData multiOptTildeCriterionXData,
}) {
  if (multiOptTildeCriterionName == "company~") {
    var listXData =
        multiOptTildeCriterionXData as ListXData<int, CompanyInfo>;
    List<CompanyInfo> list = listXData.data;
    if (list.isNotEmpty) {
      return OptValueWrap.single(list.first);
    }
    return null;
  } else if (multiOptTildeCriterionName == "departments~") {
    CompanyInfo company = parentMultiOptTildeCriterionValue as CompanyInfo;
    var listXData =
        multiOptTildeCriterionXData as ListXData<int, DepartmentInfo>;
    List<DepartmentInfo> departments = listXData.data;
    // Select all department in MultiDropDown.
    return OptValueWrap.multi(departments);
  }
  return null;
}
FilterModel.createNewFilterCriteria()
createNewFilterCriteria()
@override
Employee20aFilterCriteria createNewFilterCriteria({
  required Map<String, dynamic> tildeCriteriaMap,
}) {
  return Employee20aFilterCriteria(
    searchText: tildeCriteriaMap["searchText~"],
    company: tildeCriteriaMap["company~"],
    departments: tildeCriteriaMap["departments~"],
  );
}
No ADS

4. Employee20aFilterCriteria

employee20a_filter_criteria.dart
class Employee20aFilterCriteria extends FilterCriteria {
  final String? searchText;
  final CompanyInfo? company;
  final List<DepartmentInfo>? departments;

  const Employee20aFilterCriteria({
    required this.searchText,
    required this.company,
    required this.departments,
  }); 
}
  • FlutterArtist Debug Filter Criteria Viewer

5. Employee20aBlock

Finally, in the Block.performQuery() method, we use the filterCriteria object to call the API. Notice how departmentIdsAsString is extracted from the list of selected departments to be sent to the Server.
employee20a_block.dart (*)
class Employee20aBlock
    extends
        Block<
          int, //
          EmployeeInfo,
          EmployeeData,
          EmptyFilterInput,
          Employee20aFilterCriteria,
          EmptyFormInput,
          EmptyAdditionalFormRelatedData
        > {
  static const blkName = "employee20a-block";

  final employeeRestProvider = EmployeeRestProvider();

  Employee20aBlock({
    required super.name,
    required super.description,
    required super.config,
    required super.filterModelName,
    required super.formModel,
    required super.childBlocks,
  });

  @override
  Future<ApiResult<PageData<EmployeeInfo>?>> performQuery({
    required Object? parentBlockCurrentItem,
    required Employee20aFilterCriteria filterCriteria,
    required SortableCriteria sortableCriteria,
    required Pageable pageable,
  }) async {
    return await employeeRestProvider.queryAdvanced(
      pageable: pageable,
      searchText: filterCriteria.searchText,
      companyId: filterCriteria.company?.id,
      departmentIdsAsString: filterCriteria.departmentIdsAsString,
    );
  }
  ...
}
  • FlutterArtist FilterCriteria
No ADS

6. Employee20aFilterPanel

The Employee20aFilterPanel class creates the user interface for the filter.
employee20a_filter_panel.dart
class Employee20aFilterPanel extends FilterPanel<Employee20aFilterModel> {
  const Employee20aFilterPanel({required super.filterModel, super.key});

  @override
  Widget buildContent(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        buildFilterBar(context),
        const Divider(),
        FaFormBuilderTextField.topLabel(
          name: "searchText~",
          labelText: "Search Text",
          maxLines: 1,
          onChanged: _onSearchTextChanged,
        ),
        const SizedBox(height: 10),
        FaFormBuilderDeselectableDropdown<CompanyInfo>.topLabel(
          key: const Key("filter-company"),
          name: "company~",
          labelText: "Company",
          // List<CompanyInfo>:
          items: filterModel.getMultiOptTildeCriterionData("company~") ?? [],
          getItemText: (item) => item.name,
          onChanged: _onSelectCompany,
        ),
        const SizedBox(height: 10),
        FaFormBuilderMultiDropdown<int, DepartmentInfo>.topLabel(
          key: const Key("filter-department"),
          name: "departments~",
          dropdownTitle: 'Select departments',
          labelText: "Department",
          // List<DepartmentInfo>:
          items:
              filterModel.getMultiOptTildeCriterionData("departments~") ?? [],
          getItemText: (item) => item.name,
          onChanged: _onChangeDepartment,
        ),
      ],
    );
  }

  Future<void> _onSearchTextChanged(String? text) async {
    FlutterArtist.codeFlowLogger.addMethodCall(
      ownerClassInstance: this,
      currentStackTrace: StackTrace.current,
      parameters: {},
    );
    //
    await filterModel.queryAll();
  }

  Future<void> _onSelectCompany(CompanyInfo? company) async {
    FlutterArtist.codeFlowLogger.addMethodCall(
      ownerClassInstance: this,
      currentStackTrace: StackTrace.current,
      parameters: {"company": company},
    );
    //
    await filterModel.queryAll();
  }

  Future<void> _onChangeDepartment(List<DepartmentInfo>? departments) async {
    FlutterArtist.codeFlowLogger.addMethodCall(
      ownerClassInstance: this,
      currentStackTrace: StackTrace.current,
      parameters: {"departments": departments},
    );
    //
    await filterModel.queryAll();
  }
}
No ADS

FlutterArtist

Show More
No ADS