如何在Ember.js下拉列表中实现动态过滤?

carvr3hs  于 2023-08-04  发布在  其他
关注(0)|答案(1)|浏览(161)

我正在开发一个Ember.js应用程序,我需要实现两个下拉列表,其中第二个下拉列表中的选项根据第一个下拉列表中的选定值进行过滤。我有以下要求:
第二个下拉列表应该从API端点获取其选项。每当在第一个下拉列表中选择一个值时,第二个下拉列表中的选项应相应地进行筛选。过滤应该是动态进行的,不需要页面刷新。我尝试过实现这个功能,但遇到了一些问题。在第一个下拉列表中选择新值时,第二个下拉列表不会更新其选项。如何实现这种动态过滤行为?
下面是我目前拥有的代码的简化版本:我有一个家长,然后2下拉组件内。我把第一个下拉列表的选择值从父列表发送到第二个下拉列表。但问题是,新数据不会根据第一个下拉列表的值进行过滤(即:selectedBU(从母体注射))。这个组件非常复杂,因此我只发布了index.js和index.hbs用于第二个dropsown。

second_dropdown.hbs

  1. {{! @glint-nocheck - not typesafe yet }}
  2. {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }}
  3. <h3>{{@this.selectedBU}}</h3>
  4. <div data-test-product-select>
  5. {{#if this.teams}}
  6. {{#if @formatIsBadge}}
  7. <Inputs::BadgeDropdownList
  8. @items={{this.teams}}
  9. @listIsOrdered={{true}}
  10. @onItemClick={{this.onChange}}
  11. @selected={{@selected}}
  12. @placement={{@placement}}
  13. @isSaving={{@isSaving}}
  14. @renderOut={{@renderOut}}
  15. @icon={{this.icon}}
  16. class="w-80 product-select-dropdown-list"
  17. ...attributes
  18. >
  19. <:item as |dd|>
  20. <dd.Action data-test-product-select-badge-dropdown-item>
  21. <Inputs::TeamSelect::Item
  22. @product={{dd.value}}
  23. @selected={{dd.selected}}
  24. />
  25. </dd.Action>
  26. </:item>
  27. </Inputs::BadgeDropdownList>
  28. {{else}}
  29. <X::DropdownList
  30. @items={{this.teams}}
  31. @listIsOrdered={{true}}
  32. @onItemClick={{this.onChange}}
  33. @selected={{@selected}}
  34. @placement={{@placement}}
  35. @isSaving={{@isSaving}}
  36. @renderOut={{@renderOut}}
  37. class="w-[300px] product-select-dropdown-list"
  38. ...attributes
  39. >
  40. <:anchor as |dd|>
  41. <dd.ToggleAction
  42. class="x-dropdown-list-toggle-select product-select-default-toggle hds-button hds-button--color-secondary hds-button--size-medium"
  43. >
  44. <FlightIcon @name={{or (get-product-id @selected) "folder"}} />
  45. <span
  46. class="product-select-selected-value
  47. {{unless @selected 'text-color-foreground-faint'}}"
  48. >
  49. {{or @selected "Select your team/pod"}}
  50. </span>
  51. {{#if this.selectedProductAbbreviation}}
  52. <span class="product-select-toggle-abbreviation">
  53. {{this.selectedProductAbbreviation}}
  54. </span>
  55. {{/if}}
  56. <FlightIcon @name="caret" class="product-select-toggle-caret" />
  57. </dd.ToggleAction>
  58. </:anchor>
  59. <:item as |dd|>
  60. <dd.Action class="pr-5">
  61. <Inputs::TeamSelect::Item
  62. @product={{dd.value}}
  63. @selected={{dd.selected}}
  64. @abbreviation={{dd.attrs.abbreviation}}
  65. />
  66. </dd.Action>
  67. </:item>
  68. </X::DropdownList>
  69. {{/if}}
  70. {{else if this.fetchteams.isRunning}}
  71. <FlightIcon data-test-product-select-spinner @name="loading" />
  72. {{else}}
  73. <div
  74. class="absolute top-0 left-0"
  75. {{did-insert (perform this.fetchteams)}}
  76. ></div>
  77. {{/if}}
  78. </div>

字符串
和/或

second_dropdown.ts

  1. import { assert } from "@ember/debug";
  2. import {action, computed} from "@ember/object";
  3. import { inject as service } from "@ember/service";
  4. import { Placement } from "@floating-ui/dom";
  5. import Component from "@glimmer/component";
  6. import { tracked } from "@glimmer/tracking";
  7. import { task } from "ember-concurrency";
  8. import FetchService from "hermes/services/fetch";
  9. import getProductId from "hermes/utils/get-product-id";
  10. interface InputsTeamSelectSignature {
  11. Element: HTMLDivElement;
  12. Args: {
  13. selectedBU: string | null;
  14. selected?: string;
  15. onChange: (value: string, attributes?: TeamArea) => void;
  16. formatIsBadge?: boolean;
  17. placement?: Placement;
  18. isSaving?: boolean;
  19. renderOut?: boolean;
  20. };
  21. }
  22. type TeamAreas = {
  23. [key: string]: TeamArea;
  24. };
  25. export type TeamArea = {
  26. abbreviation: string;
  27. perDocDataType: unknown;
  28. BU: string
  29. };
  30. export default class InputsTeamSelectComponent extends Component<InputsTeamSelectSignature> {
  31. @service("fetch") declare fetchSvc: FetchService;
  32. @tracked selected = this.args.selected;
  33. @tracked teams: TeamAreas | undefined = undefined;
  34. @tracked selectedBU: string | null = null;
  35. @computed('args.selectedBU')
  36. get ReFetchTeams() {
  37. this.selectedBU = this.args.selectedBU;
  38. return this.fetchteams.perform();
  39. }
  40. get icon(): string {
  41. let icon = "folder";
  42. if (this.selected && getProductId(this.selected)) {
  43. icon = getProductId(this.selected) as string;
  44. }
  45. return icon;
  46. }
  47. get selectedProductAbbreviation(): string | null {
  48. if (!this.selected) {
  49. return null;
  50. }
  51. const selectedProduct = this.teams?.[this.selected];
  52. assert("selected Team must exist", selectedProduct);
  53. return selectedProduct.abbreviation;
  54. }
  55. @action onChange(newValue: any, attributes?: TeamArea) {
  56. this.selected = newValue;
  57. this.args.onChange(newValue, attributes);
  58. }
  59. // @action onBUChange(){
  60. // this.selectedBU = this.args.selectedBU;
  61. // this.fetchteams.perform();
  62. // }
  63. // protected fetchProducts = task(async () => {
  64. // try {
  65. // let products = await this.fetchSvc
  66. // .fetch("/api/v1/products")
  67. // .then((resp) => resp?.json());
  68. // this.products = products;
  69. // } catch (err) {
  70. // console.error(err);
  71. // throw err;
  72. // }
  73. // });
  74. protected fetchteams = task(async () => {
  75. try {
  76. // Filter the teams based on the selected business unit
  77. console.log("parent injected value is: ",this.args.selectedBU)
  78. let teams = await this.fetchSvc
  79. .fetch("/api/v1/teams")
  80. .then((resp) => resp?.json());
  81. // Filter the teams based on the selected business unit
  82. const filteredTeams: TeamAreas = {};
  83. for (const team in teams) {
  84. if (Object.prototype.hasOwnProperty.call(teams, team)) {
  85. const teamData: TeamArea | undefined = teams[team];
  86. if (teamData && teamData.BU === this.args.selectedBU) {
  87. filteredTeams[team] = teamData;
  88. }
  89. }
  90. }
  91. console.log("the filtered teams are: ",filteredTeams);
  92. this.teams = filteredTeams;
  93. console.log(this.teams);
  94. } catch (err) {
  95. console.error(err);
  96. throw err;
  97. }
  98. });
  99. }
  100. declare module "@glint/environment-ember-loose/registry" {
  101. export default interface Registry {
  102. "Inputs::TeamSelect": typeof InputsTeamSelectComponent;
  103. }
  104. }


任何一种指导都将受到高度赞赏!谢谢你,谢谢
我试过使用@traked,@did-update等,但似乎没有什么工作,即使我删除“did-insert”下拉菜单完全消失。

7uhlpewt

7uhlpewt1#

感谢您发布了一个如此详细的问题和示例代码!
我认为这里的答案是 * 派生数据 *(你已经在做了!但是有些东西有点不对劲)。(对于这个答案的观察者来说)“衍生数据”的意思是,我们需要定义“拥有一个值意味着什么”,然后每个选择都基于此 * 衍生 *。
我在您的代码片段中注意到了两个主要的问题

  • ember并发任务被用作一个副作用--因为它正在设置它不需要的数据。相反,它应该返回您希望传递给Select的数据,以便Select可以响应 * 任务 *,而不是您管理的属性(此策略不易出错,但不是 * 问题 *(如下所示))。
  • 由于您希望在@selectedBU更改时重新获取数据,因此派生数据将使这一点变得非常容易--我们不想考虑“效果,”因为它使跟踪行为变得困难,也使以后的维护变得困难(即使在React/Vue/Svelte/Solid/etc中,我们也不想使用效果,而希望使用派生数据)。

由于您提供的代码是特定于您的应用程序的(我无法运行),因此我制作了一个(简化的)演示,显示了您所描述的情况(再次感谢您对预期行为的详细描述)。
我已经使用open StarWars API来模拟请求数据,这些选择将从这些数据中填充。
interactive version is here
下面是相关代码:

  1. <label>
  2. Select {{selectedAPI.current}}
  3. {{!
  4. it's important to model async behavior, and this util makes that a bit easier.
  5. Docs here: https://ember-resources.pages.dev/funcs/util_remote_data.RemoteData }}
  6. {{#let (RemoteData (urlForDataSource selectedAPI.current)) as |request|}}
  7. {{#if request.isLoading}}
  8. Loading...
  9. {{/if}}
  10. {{#if request.value}}
  11. <Select
  12. @options={{names request.value.results}}
  13. @onChange={{(fn setSelected selectedAPI.current)}}
  14. >
  15. <:option as |item|>
  16. {{item.name}}
  17. </:option>
  18. </Select>
  19. {{/if}}
  20. {{/let}}
  21. </label>

字符串
一些注解:

  • 不使用ember-concurrency(尽管仍然可以使用,只是不需要)
  • 当数据发生变化时,一个新的请求通过推导URL发生了变化而开始(这是关键部分)--这里大量使用了函数,因为函数本质上是React性的--还使用了资源,因为这是一种更容易对“最终值”建模并与派生数据保持一致的方法。
  • 当源数据发生变化时,这会 * 删除 * select--您的UI/UX可能不希望这样,并且您不会被锁定在这里的任何特定行为中。
展开查看全部

相关问题