mydomain
No ADS
No ADS

FlutterArtist Filter Tree FormBuilderField ex1

  1. Structure of the example
  2. CompanyTreeView
  3. Employee04aShelf
  4. Employee04aFilterModel
  5. Employee04aFilterPanel
  6. Employee04aBlock
As we know, in FlutterArtist, all data fields used in FormView or FilterPanel must comply with the standards of the flutter_form_builder library. Among them, FormBuilderDropdown is a familiar and common data field, which can be used in FilterPanel to allow users to select an option from a list of options.
Let's see, you can select a Company in a FormBuilderDropdown like this:
And selecting a Company on a tree widget like this.
The question is: which method can provide a better user experience?
Basically, the tree widget above is not an input field, nor does it comply with flutter_form_builder standards. However, if you wrap the tree widget in a FormBuilderField, both of these conditions are met.
In this example, we will wrap the tree widget in a FormBuilderField so it can be used as an input field on the FilterPanel, replacing the familiar FormBuilderDropdown.

1. Structure of the example

2. CompanyTreeView

To create a TreeView, you can use the animated_tree_view library.
pubspect.yaml (*)
dependencies:
  animated_tree_view: ^2.3.0
CompanyTreeView in this example creates a tree structure based on the CompanyTree parameter. The full source code for this example is available on FlutterArtist Demo.
CompanyTreeView
class CompanyTreeView extends StatefulWidget {
  final CompanyTree? companyTree;
  final CompanyTreeItem? selectedCompanyTreeItem;
  final Function(CompanyTreeItem companyTreeItem) onSelectCompanyTreeItem;
  final EdgeInsets padding;

  const CompanyTreeView({
    super.key,
    required this.companyTree,
    this.padding = EdgeInsets.zero,
    required this.selectedCompanyTreeItem,
    required this.onSelectCompanyTreeItem,
  })
  ...
}
The full source code for CompanyTreeView is available in "FlutterArtist Demo".
  • Download FlutterArtist Demo
No ADS
CompanyTree / CompanyTreeItem (Model)
@JsonSerializable()
class CompanyTree {
  @JsonKey(name: 'rootCompanyTreeItems', defaultValue: [])
  List<CompanyTreeItem> rootCompanyTreeItems = [];
  ..
}

@JsonSerializable()
class CompanyTreeItem implements Identifiable<int> {
  @override
  @JsonKey(name: 'id')
  late int id;

  @JsonKey(name: 'name')
  late String name;

  @JsonKey(name: 'children', defaultValue: [])
  List<CompanyTreeItem> children = [];

  @JsonKey(name: 'imagePath')
  String? imagePath;

  ....
}  

3. Employee04aShelf

The structure of Employee04aShelf is simple, consisting of only a FilterModel and a Block:
employee04a_shelf.dart
class Employee04aShelf extends Shelf {
  @override
  ShelfStructure defineShelfStructure() {
    return ShelfStructure(
      filterModels: {
        Employee04aFilterModel.filterName: Employee04aFilterModel(),
      },
      blocks: [
        Employee04aBlock(
          name: Employee04aBlock.blkName,
          description: null,
          config: BlockConfig(),
          filterModelName: Employee04aFilterModel.filterName,
          formModel: null,
          childBlocks: [],
        ),
      ],
    );
  }

  Employee04aBlock findEmployee04aBlock() {
    return findBlock(Employee04aBlock.blkName) as Employee04aBlock;
  }
}

4. Employee04aFilterModel

FilterModel structure:
defineFilterModelStructure()
@override
FilterModelStructure defineFilterModelStructure() {
  return FilterModelStructure(
    criteriaStructure: FilterCriteriaStructure(
      simpleCriterionDefs: [
        SimpleFilterCriterionDef<String>(criterionBaseName: "searchText"),
      ],
      multiOptCriterionDefs: [
        MultiOptFilterCriterionDef<CompanyTreeItem>.singleSelection(
          criterionBaseName: "company",
          fieldName: 'companyId',
          toFieldValue: (CompanyTreeItem? 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,
        ),
      ],
    ),
  );
}
performLoadMultiOptTildeCriterionXData()
The performLoadMultiOptTildeCriterionXData() method is used to load data for a MultiOptTildeFilterCriterion. In this case, the expected data you might be thinking of is a CompanyTree. However, this method requires returning an XData, so we need to wrap the CompanyTree within a TreeXData.
No ADS
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<CompanyTree> result = await _companyTreeProvider
        .getCompanyTree();
    // Throw ApiError
    result.throwIfError();
    //
    CompanyTree? companyTree = result.data;
    if (companyTree == null) {
      return null;
    }
    return TreeXData<int, CompanyTreeItem, CompanyTree>(
      treeData: companyTree,
      getItemId: (item) => item.id,
      getRootTreeItems: () {
        return companyTree.rootCompanyTreeItems;
      },
      getChildren: (CompanyTreeItem item) {
        return item.children;
      },
      addOrphanTreeItem : (CompanyTreeItem item) {
        companyTree.addOrphanCompany(item);
      },
      removeOrphanTreeItem: (CompanyTreeItem item) {
        companyTree.removeOrphanCompany(item);
      },
    );
  }
  //
  return null;
}
In this example, XData is not a ListXData as seen in typical FormBuilderDropdown cases. Here, XData is a TreeXData, which is particularly useful in a FormModel because it effectively handles "orphan data" (items that were previously selected but no longer exist in the current list). TreeXData simply appends these orphan TreeItem(s) as a final root Node of the tree to ensure display data integrity.
Note: The reason why the FilterModel.performLoadMultiOptTildeCriterionXData() method is designed to return an XData is explained in detail in the following articles:
  • FlutterArtist XData

5. Employee04aFilterPanel

CompanyTreeView is just a regular widget; it is not a data field and does not comply with flutter_form_builderstandards. To transform it into an input field, you need to wrap it in a FormBuilderField like this:
No ADS
FormBuilderField(CompanyTreeView)
FormBuilderField<CompanyTreeItem>(
  name: "company~",
  builder: (FormFieldState<CompanyTreeItem> field) {
    return CompanyTreeView(
      companyTree: filterModel.getMultiOptTildeCriterionData(
        "company~",
      ),
      selectedCompanyTreeItem: field.value,
      onSelectCompanyTreeItem: (CompanyTreeItem item) {
        // IMPORTANT (If you wrap a CompanyTreeView in a FormBuilderField):
        field.didChange(item);
        //
        _onSelectCompanyTreeItem(item);
      },
    );
  },
),
When the user changes the CompanyTreeItem on the CompanyTreeView, you must call field.didChange(item)within the onSelectCompanyTreeItem() method. This is the way to synchronize the state between CompanyTreeView and FormBuilderField.
Below is the full source code for Employee04aFilterPanel:
employee04a_filter_panel.dart
class Employee04aFilterPanel extends FilterPanel<Employee04aFilterModel> {
  const Employee04aFilterPanel({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 filterModel.queryAll();
          },
        ),
        SizedBox(height: 10),
        Expanded(
          child: FormBuilderField<CompanyTreeItem>(
            name: "company~",
            builder: (FormFieldState<CompanyTreeItem> field) {
              return CompanyTreeView(
                companyTree: filterModel.getMultiOptTildeCriterionData(
                  "company~",
                ),
                selectedCompanyTreeItem: field.value,
                onSelectCompanyTreeItem: (CompanyTreeItem item) {
                  // IMPORTANT (If you wrap a CompanyTreeView in a FormBuilderField):
                  field.didChange(item);
                  //
                  _onSelectCompanyTreeItem(item);
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Future<void> _onSelectCompanyTreeItem(CompanyTreeItem treeItem) async {
    FlutterArtist.codeFlowLogger.addMethodCall(
      ownerClassInstance: this,
      currentStackTrace: StackTrace.current,
      parameters: {"treeItem": treeItem},
    );
    //
    await filterModel.queryAll();
  }
}
Using FormBuilderField allows you to create complex input fields and adhere to flutter_form_builder standards. Furthermore, this approach makes maintenance extremely easy. If you later want to replace animated_tree_view with another library or a custom solution, you only need to change the code inside the FormBuilderField builder without modifying any logic in the FilterModel or the defined Criterion(s).

6. Employee04aBlock

Employee04aBlock.performQuery()
@override
Future<ApiResult<PageData<EmployeeInfo>?>> performQuery({
  required Object? parentBlockCurrentItem,
  required Employee04aFilterCriteria filterCriteria,
  required SortableCriteria sortableCriteria,
  required Pageable pageable,
}) async {
  return await employeeRestProvider.query(
    companyId: filterCriteria.companyTreeItem?.id,
    searchText: filterCriteria.searchText,
    pageable: pageable,
  );
}
No ADS

FlutterArtist

Show More
No ADS