mydomain
No ADS
No ADS

FlutterArtist Filter FormBuilderField ex1

  1. Cấu trúc của ví dụ
  2. CompanyTreeView
  3. Employee04aShelf
  4. Employee04aFilterModel
  5. Employee04aFilterPanel
  6. Employee04aBlock
Như đã biết, trong FlutterArtist tất cả các trường dữ liệu được sử dụng trong FormView hoặc FilterPanel đều phải tuân thủ theo các tiêu chuẩn của thư viện flutter_form_builder. Trong đó, FormBuilderDropdown là một trường dữ liệu quen thuộc và thông dụng, nó có thể sử dụng trong FilterPanel cho phép người dùng lựa chọn một tiêu chí trong một danh sách các tiêu chí.
Hãy xem, bạn có thể lựa chọn một "Company" trong một FormBuilderDropdown giống thế này:
Và lựa chọn một "Company" trên một tiện ích cây (tree widget) như thế này.
Câu hỏi là cách thức nào có thể mang lại trải nghiệm tốt hơn cho người dùng?
Về cơ bản tiện ích cây (tree widget) ở trên không phải là một trường đầu vào và nó cũng không tuân thủ các tiêu chuẩn của flutter_form_builder. Tuy nhiên nếu bạn gói tiện ích cây trong một FormBuilderField thì 2 điều trên đều được thoả mãn.
Trong ví dụ này chúng ta sẽ gói tiện ích cây trong một FormBuilderField và có thể sử dụng nó như một trường đầu vào trên FilterPanel, và có thể sử dụng nó để thay thế cho một FormBuilderDropdown quen thuộc.

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

2. CompanyTreeView

Để tạo một TreeView bạn có thể sử dụng gói thư viện animated_tree_view.
pubspect.yaml (*)
dependencies:
  animated_tree_view: ^2.3.0
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,
  })
  ...
}
Mã nguồn đầy đủ của CompanyTreeView sẵn có trong "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

Cấu trúc của Employee04aShelf là đơn giản, chỉ bao gồm một FilterModel và một 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

Cấu trúc FilterModel:
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()
Phương thức performLoadMultiOptTildeCriterionXData() được sử dụng để tải dữ liệu cho một MultiOptTildeFilterCriterion. Với trường hợp này dữ liệu mong đợi mà bạn đang nghĩ tới là CompanyTree. Tuy nhiên phương thức này yêu cầu trả về một XData, vì vậy chúng ta cần gói CompanyTree trong một 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;
      },
      addNotFoundTreeItem: (CompanyTreeItem item) {
        companyTree.addNotFoundCompany(item);
      },
      removeNotFoundTreeItem: (CompanyTreeItem item) {
        companyTree.removeNotFoundCompany(item);
      },
    );
  }
  //
  return null;
}
Lý do tại sao phương thức FilterModel.performLoadMultiOptTildeCriterionXData() được thiết kế để trả về một XData được giải thích trong các bài viết dưới đây:
  • FlutterArtist ListXData
  • FlutterArtist XData

5. Employee04aFilterPanel

CompanyTreeView chỉ là một tiện ích (widget) thông thường, nó không phải là một trường dữ liệu và cũng không tuân theo các tiêu chuẩn của flutter_form_builder. Tuy nhiên cả 2 tiêu chuẩn trên sẽ được thoả mãn nếu bạn gói nó trong một FormBuilderField.
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);
      },
    );
  },
),
Code đầy đủ của 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();
  }
}

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
No ADS