You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 18:01:56