mydomain
No ADS
No ADS

FlutterArtist FilterModel MultiOptFilterCriterion ex1

  1. Cấu trúc của ví dụ
  2. Employee03aShelf
  3. Employee03aFilterModel
  4. Employee03aBlock
  5. Employee03aFilterPanel
In this article, we will explore how to configure a filter with parent-child criteria, such as the relationship between "Company" and "Department."
The core discussion revolves around the Demo03a example in FlutterArtist Demo. In this case, Employee03aFilterModel includes three search criteria: Employee Name, Company, and Department — where Company and Department share a hierarchical relationship. Specifically, when the selected Company changes, the department list in the second Dropdown must automatically reload based on that selection.
Chú ý: Nếu bạn mới bắt đầu với FlutterArtist Filter, hãy xem ví dụ dưới đây trước thay vì ví dụ này.
  • Download FlutterArtist Demo
No ADS

1. Cấu trúc của ví dụ

2. Employee03aShelf

In this example, the structure of the Shelf is quite simple, consisting only of a FilterModel and a Block:
employee03a_shelf.dart
class Employee03aShelf extends Shelf {
  @override
  ShelfStructure defineShelfStructure() {
    return ShelfStructure(
      filterModels: {
        Employee03aFilterModel.filterName: Employee03aFilterModel(),
      },
      blocks: [
        Employee03aBlock(
          name: Employee03aBlock.blkName,
          description: null,
          config: BlockConfig(),
          filterModelName: Employee03aFilterModel.filterName,
          formModel: null,
          childBlocks: [],
        ),
      ],
    );
  }

  Employee03aFilterModel findEmployee03aFilterModel() {
    return findFilterModel(Employee03aFilterModel.filterName)
        as Employee03aFilterModel;
  }

  Employee03aBlock findEmployee03aBlock() {
    return findBlock(Employee03aBlock.blkName) as Employee03aBlock;
  }
}

3. Employee03aFilterModel

First, let's look at how the filter structure is defined. Pay close attention to the "company" and "department" criteria:
Employee03aFilterModel.defineFilterModelStructure()
@override
FilterModelStructure defineFilterModelStructure() {
  return FilterModelStructure(
    criteriaStructure: FilterCriteriaStructure(
      simpleCriterionDefs: [
        SimpleFilterCriterionDef<String>(criterionBaseName: "searchText"),
      ],
      multiOptCriterionDefs: [
        // Multi Options Single Selection Criterion.
        MultiOptFilterCriterionDef<CompanyInfo>.singleSelection(
          criterionBaseName: "company",
          fieldName: 'companyId',
          toFieldValue: (CompanyInfo? rawValue) {
            return SimpleVal.ofInt(rawValue?.id);
          },
          children: [
            // Multi Options Single Selection Criterion.
            MultiOptFilterCriterionDef<DepartmentInfo>.singleSelection(
              criterionBaseName: "department",
              fieldName: 'departmentId',
              toFieldValue: (DepartmentInfo? rawValue) {
                return SimpleVal.ofInt(rawValue?.id);
              },
            ),
          ],
        ),
      ],
    ),
    conditionStructure: FilterConditionStructure(
      connector: FilterConnector.and,
      conditionDefs: [
        FilterConditionDef.simple(
          tildeCriterionName: "searchText~",
          operator: FilterOperator.containsIgnoreCase,
        ),
        FilterConditionDef.simple(
          tildeCriterionName: "company~",
          operator: FilterOperator.equalTo,
        ),
        FilterConditionDef.simple(
          tildeCriterionName: "department~",
          operator: FilterOperator.equalTo,
        ),
      ],
    ),
  );
}
No ADS
How to define a multi-option criterion where the user can select at most one option.
singleSelection
MultiOptFilterCriterionDef<DepartmentInfo>.singleSelection(
  criterionBaseName: "department",
)
How to define a multi-option criterion that allows the user to select multiple options. 
multiSelection
MultiOptFilterCriterionDef<DepartmentInfo>.multiSelection(
  criterionBaseName: "department",
)
Based on the defined criteria and condition structures, three TildeFilterCriterion(s) are created. Each corresponds 1-1 with an input field on the FilterPanel.
FilterModel.performLoadMultiOptTildeCriterionXData()
This method is invoked sequentially for each MultiOptTildeFilterCriterion to fetch its data. The rule is that root criteria are 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<PageData<CompanyInfo>>
    ApiResult<CompanyInfoPage> result = await _companyProvider.queryAll();
    // IMPORTANT: Throw ApiError.
    result.throwIfError();
    // IMPORTANT: Generics should be declared explicitly.
    var companyListXData = ListXData<int, CompanyInfo>.fromPageData(
      pageData: result.data,
      getItemId: (item) => item.id,
    );
    return companyListXData;
  } else if (multiOptTildeCriterionName == "department~") {
    //
    // Because "department" is child of "company".
    // So [parentMultiOptTildeCriterionValue] value is definitely non-null,
    // this guaranteed by the library.
    //
    CompanyInfo companyInfo =
        parentMultiOptTildeCriterionValue as CompanyInfo;
    // ApiResult<PageData<DepartmentInfo>>
    ApiResult<DepartmentInfoPage> result = await _departmentProvider
        .queryAllByCompanyId(companyId: companyInfo.id);
    // IMPORTANT: Throw ApiError if API Error.
    result.throwIfError();
    // IMPORTANT: Generics should be declared explicitly.
    return ListXData<int, DepartmentInfo>.fromPageData(
      pageData: result.data,
      getItemId: (item) => item.id,
    );
  }
  return null;
}
No ADS
For root criterion "company~":
  • Initially, this method is invoked to load data for "company~". The complete list of companies retrieved from the API is wrapped into a ListXData object, making it ready for display.
For the Child Criterion "department~":
  • The key highlight is here: the "department~" criterion has a direct dependency on "company~". When a user selects a specific company, the system automatically triggers this method to fetch data for "department~".
Library Guarantee:
  • You don't need to worry about null-checking the parent data. The library guarantees that if a criterion is defined as a "child," its data-loading method is only invoked once the "parent" criterion holds a valid value.
  • FlutterArtist ApiResult
  • FlutterArtist XData
No ADS
Debug Filter Model Viewer
The Debug Filter Model Viewer is a tool that allows you to view a list of criteria defined in the FilterModel and their values. This tool is accessed via the FilterControlBar or code.
  • FlutterArtist Debug Filter Model Viewer
FilterModel.createNewFilterCriteria()
When a user performs a Block or Scalar query, this method is called to instantiate the FilterCriteria object (in this case, Employee03aFilterCriteria).
createNewFilterCriteria()
@override
Employee03aFilterCriteria createNewFilterCriteria({
  required Map<String, dynamic> tildeCriteriaMap,
}) {
  return Employee03aFilterCriteria(
    searchText: tildeCriteriaMap["searchText~"], // String?
    company: tildeCriteriaMap["company~"], // CompanyInfo?
    department: tildeCriteriaMap["department~"], // DepartmentInfo?
  );
}
No ADS
Finally, the resulting FilterCriteria object is passed as a parameter to the performQuery() method of the Block or Scalar.
// Block:
Future<ApiResult<PageData<ITEM>?>> performQuery({
  required Object? parentBlockCurrentItem,
  required FILTER_CRITERIA filterCriteria,
  required SortableCriteria sortableCriteria,
  required Pageable pageable,
});

// Scalar:
Future<ApiResult<VALUE>> performQuery({
  required Object? parentScalarValue,
  required FILTER_CRITERIA filterCriteria,
});

4. Employee03aBlock

Please pay attention to the performQuery() method within Employee03aBlock. The filterCriteria parameter is a Dart object containing the selected criteria values, which are used directly to query the data.
employee03a_block.dart (*)
class Employee03aBlock
    extends
        Block<
          int, //
          EmployeeInfo,
          EmployeeData,
          EmptyFilterInput,
          Employee03aFilterCriteria,
          EmptyFormInput,
          EmptyAdditionalFormRelatedData
        > {
  static const blkName = "employee03a-block";

  final employeeRestProvider = EmployeeRestProvider();

  Employee03aBlock({
    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 Employee03aFilterCriteria filterCriteria,
    required SortableCriteria sortableCriteria,
    required Pageable pageable,
  }) async {
    return await employeeRestProvider.query(
      searchText: filterCriteria.searchText,
      companyId: filterCriteria.company?.id,
      departmentId: filterCriteria.department?.id,
      pageable: pageable,
    );
  }
  ...
}
  • FlutterArtist FilterCriteria

5. Employee03aFilterPanel

The Employee03aFilterPanel class is responsible for building the user interface, allowing users to input and select various filtering criteria.
employee03a_filter_panel.dart
class Employee03aFilterPanel extends FilterPanel<Employee03aFilterModel> {
  const Employee03aFilterPanel({required super.filterModel, super.key});

  @override
  Widget buildContent(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        buildFilterBar(context),
        Divider(),
        FaFormBuilderTextField.topLabel(
          name: "searchText~",
          labelText: "Search Text",
          maxLines: 1,
          onChanged: (_) async {
            await _queryEmployee03a();
          },
        ),
        SizedBox(height: 10),
        FaFormBuilderDeselectableDropdown<CompanyInfo>.topLabel(
          name: "company~",
          labelText: "Company",
          items: filterModel.getMultiOptTildeCriterionData("company~") ?? [],
          getItemText: (item) => item.name,
          onChanged: (_) async {
            await _queryEmployee03a();
          },
        ),
        SizedBox(height: 10),
        FaFormBuilderDeselectableDropdown<DepartmentInfo>.topLabel(
          name: "department~",
          labelText: "Department",
          items: filterModel.getMultiOptTildeCriterionData("department~") ?? [],
          getItemText: (item) => item.name,
          onChanged: (_) async {
            await _queryEmployee03a();
          },
        ),
      ],
    );
  }

  Future<void> _queryEmployee03a() async {
    FlutterArtist.codeFlowLogger.addMethodCall(
      ownerClassInstance: this,
      currentStackTrace: StackTrace.current,
      parameters: null,
    );
    //
    await filterModel.queryAll();
  }
}
  • FlutterArtist Fa FormBuilder
No ADS

FlutterArtist

Show More
No ADS