如何在Ember.js下拉菜单中实现动态筛选功能?
问题描述
开发Ember.js应用时,需实现两个联动下拉菜单:第二个菜单的选项根据第一个的选中值动态筛选,具体需求:
- 第二个下拉菜单选项从API端点获取
- 第一个下拉值变更时,第二个菜单选项自动筛选
- 无页面刷新,动态完成当前问题:第一个下拉选中新值后,第二个下拉选项未更新。已将第一个下拉的选中值
selectedBU从父组件传递到第二个下拉组件,但新数据未基于该值筛选。
第二个下拉组件代码
second_dropdown.hbs
{{! @glint-nocheck - not typesafe yet }} {{! https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ }} <h3>{{@this.selectedBU}}</h3> <div data-test-product-select> {{#if this.teams}} {{#if @formatIsBadge}} <Inputs::BadgeDropdownList @items={{this.teams}} @listIsOrdered={{true}} @onItemClick={{this.onChange}} @selected={{@selected}} @placement={{@placement}} @isSaving={{@isSaving}} @renderOut={{@renderOut}} @icon={{this.icon}} class="w-80 product-select-dropdown-list" ...attributes > <:item as |dd|> <dd.Action data-test-product-select-badge-dropdown-item> <Inputs::TeamSelect::Item @product={{dd.value}} @selected={{dd.selected}} /> </dd.Action> </:item> </Inputs::BadgeDropdownList> {{else}} <X::DropdownList @items={{this.teams}} @listIsOrdered={{true}} @onItemClick={{this.onChange}} @selected={{@selected}} @placement={{@placement}} @isSaving={{@isSaving}} @renderOut={{@renderOut}} class="w-[300px] product-select-dropdown-list" ...attributes > <:anchor as |dd|> <dd.ToggleAction class="x-dropdown-list-toggle-select product-select-default-toggle hds-button hds-button--color-secondary hds-button--size-medium" > <FlightIcon @name={{or (get-product-id @selected) "folder"}} /> <span class="product-select-selected-value {{unless @selected 'text-color-foreground-faint'}}" > {{or @selected "Select your team/pod"}} </span> {{#if this.selectedProductAbbreviation}} <span class="product-select-toggle-abbreviation"> {{this.selectedProductAbbreviation}} </span> {{/if}} <FlightIcon @name="caret" class="product-select-toggle-caret" /> </dd.ToggleAction> </:anchor> <:item as |dd|> <dd.Action class="pr-5"> <Inputs::TeamSelect::Item @product={{dd.value}} @selected={{dd.selected}} @abbreviation={{dd.attrs.abbreviation}} /> </dd.Action> </:item> </X::DropdownList> {{/if}} {{else if this.fetchteams.isRunning}} <FlightIcon data-test-product-select-spinner @name="loading" /> {{else}} <div class="absolute top-0 left-0" {{did-insert (perform this.fetchteams)}} ></div> {{/if}} </div>
second_dropdown.ts
import { assert } from "@ember/debug"; import {action, computed} from "@ember/object"; import { inject as service } from "@ember/service"; import { Placement } from "@floating-ui/dom"; import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { task } from "ember-concurrency"; import FetchService from "hermes/services/fetch"; import getProductId from "hermes/utils/get-product-id"; interface InputsTeamSelectSignature { Element: HTMLDivElement; Args: { selectedBU: string | null; selected?: string; onChange: (value: string, attributes?: TeamArea) => void; formatIsBadge?: boolean; placement?: Placement; isSaving?: boolean; renderOut?: boolean; }; } type TeamAreas = { [key: string]: TeamArea; }; export type TeamArea = { abbreviation: string; perDocDataType: unknown; BU: string }; export default class InputsTeamSelectComponent extends Component<InputsTeamSelectSignature> { @service("fetch") declare fetchSvc: FetchService; @tracked selected = this.args.selected; @tracked teams: TeamAreas | undefined = undefined; @tracked selectedBU: string | null = null; @computed('args.selectedBU') get ReFetchTeams() { this.selectedBU = this.args.selectedBU; return this.fetchteams.perform(); } get icon(): string { let icon = "folder"; if (this.selected && getProductId(this.selected)) { icon = getProductId(this.selected) as string; } return icon; } get selectedProductAbbreviation(): string | null { if (!this.selected) { return null; } const selectedProduct = this.teams?.[this.selected]; assert("selected Team must exist", selectedProduct); return selectedProduct.abbreviation; } @action onChange(newValue: any, attributes?: TeamArea) { this.selected = newValue; this.args.onChange(newValue, attributes); } // @action onBUChange(){ // this.selectedBU = this.args.selectedBU; // this.fetchteams.perform(); // } // protected fetchProducts = task(async () => { // try { // let products = await this.fetchSvc // .fetch("/api/v1/products") // .then((resp) => resp?.json()); // this.products = products; // } catch (err) { // console.error(err); // throw err; // } // }); protected fetchteams = task(async () => { try { // Filter the teams based on the selected business unit console.log("parent injected value is: ",this.args.selectedBU) let teams = await this.fetchSvc .fetch("/api/v1/teams") .then((resp) => resp?.json()); // Filter the teams based on the selected business unit const filteredTeams: TeamAreas = {}; for (const team in teams) { if (Object.prototype.hasOwnProperty.call(teams, team)) { const teamData: TeamArea | undefined = teams[team]; if (teamData && teamData.BU === this.args.selectedBU) { filteredTeams[team] = teamData; } } } console.log("the filtered teams are: ",filteredTeams); this.teams = filteredTeams; console.log(this.teams); } catch (err) { console.error(err); throw err; } }); } declare module "@glint/environment-ember-loose/registry" { export default interface Registry { "Inputs::TeamSelect": typeof InputsTeamSelectComponent; } }
已尝试@tracked、@did-update等方法,移除did-insert后下拉菜单直接消失,需技术指导。
解决方案
1. 替换计算属性为参数变化监听
旧版@computed语法在Glimmer组件中无法可靠触发任务执行,改用@didUpdate修饰的动作监听args.selectedBU变化:
import { didUpdate, init } from '@ember/object'; // ... 其他代码 @tracked previousSelectedBU: string | null = this.args.selectedBU; @init protected init() { super.init(...arguments); // 组件初始化时触发首次请求 if (this.args.selectedBU) { this.fetchteams.perform(); } } @didUpdate protected onBUChange() { // 仅当BU值实际变化时触发请求 if (this.args.selectedBU !== this.previousSelectedBU) { this.previousSelectedBU = this.args.selectedBU; this.fetchteams.perform(); } }
2. 优化任务逻辑,避免重复请求
给fetchteams任务添加restartable()修饰符,确保新请求触发时取消未完成的旧请求;同时缓存当前BU值,避免异步过程中参数变化导致筛选错误:
import { task, restartable } from 'ember-concurrency'; // ... 其他代码 protected fetchteams = task(async () => { try { const currentBU = this.args.selectedBU; if (!currentBU) { this.teams = {}; return; } let teams = await this.fetchSvc.fetch("/api/v1/teams").then(resp => resp?.json()); const filteredTeams: TeamAreas = {}; for (const team in teams) { if (Object.prototype.hasOwnProperty.call(teams, team)) { const teamData = teams[team]; if (teamData && teamData.BU === currentBU) { filteredTeams[team] = teamData; } } } this.teams = filteredTeams; } catch (err) { console.error(err); throw err; } }).apply(restartable());
3. 修复模板初始加载逻辑
移除模板中冗余的空div,优化状态判断逻辑:
{{#if this.teams}} {{! 现有菜单渲染代码 }} {{else if this.fetchteams.isRunning}} <FlightIcon data-test-product-select-spinner @name="loading" /> {{else if this.args.selectedBU}} <div {{did-insert (perform this.fetchteams)}}></div> {{else}} <div>请先选择业务单元</div> {{/if}}
4. 移除冗余跟踪属性
删除组件中未使用的@tracked selectedBU,直接使用this.args.selectedBU,避免状态不一致。
核心问题总结
- 旧版
@computed语法不适用于Glimmer组件的参数变化监听 - 任务未添加
restartable修饰符,导致多次变更时请求堆积 - 模板初始加载逻辑未覆盖参数变更后的重新执行场景
- 冗余跟踪属性可能引发状态不一致
内容的提问来源于stack exchange,提问作者Yash Sharma
相关产品推荐
相关产品推荐

