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

Angular中D3无法选中API渲染的ng-container内div的解决办法

问题

在Angular项目中使用*ngFor遍历API响应数据生成表格,展开行内的<div id="court-container">无法被D3选中以绘制可视化图表。静态数组测试时D3能正常选中元素,但使用API返回数据时失败。尝试在ngAfterViewInit中等待API数据渲染完成后再选择元素,依然无效。

问题根源
  1. 重复ID冲突:*ngFor循环生成了多个相同id="court-container"的元素,而DOM要求ID必须唯一,D3通过ID选择时只能匹配到第一个元素,后续元素无法被正确选中。
  2. 视图渲染时机不匹配:API数据返回后,Angular尚未完成视图的变更检测与渲染,就触发了D3的选择逻辑,此时目标元素还未出现在DOM中。
  3. 默认隐藏元素的影响:展开行默认处于display:none状态,即使元素存在,D3在元素不可见时的选择或渲染逻辑可能无法正常执行。
解决方案

1. 为每个容器生成唯一ID

修改HTML模板,为每个展开行内的容器添加索引后缀,确保ID唯一:

<div id="court-container-{{i}}">dwewedwed</div>

2. 延迟D3初始化到行展开时

将D3图表的绘制逻辑移到toggleRow方法中,确保只有当行展开、元素可见时才执行:

toggleRow(index: number) {
  this.isRowExpanded[index] = !this.isRowExpanded[index];
  
  // 行展开时初始化图表
  if (this.isRowExpanded[index]) {
    setTimeout(() => {
      const courtContainer = d3.select(`#court-container-${index}`);
      this.drawCourt(this.apiResponse.games[index].shots, courtContainer);
    }, 0);
  } else {
    // 行收起时清理图表
    const courtContainer = d3.select(`#court-container-${index}`);
    courtContainer.selectAll("*").remove();
  }
}

3. 确保视图渲染完成后再执行逻辑

在API数据赋值后手动触发变更检测,确保视图已更新:

fetchApiResponse(): void {
  this.playersService.getPlayerSummary(this.playerID).pipe(untilDestroyed(this)).subscribe(data => {
    this.endpoint = data.endpoint;
    this.apiResponse = data.apiResponse;
    // 初始化展开状态数组
    this.isRowExpanded = new Array(this.apiResponse.games.length).fill(false);
    // 触发变更检测,确保视图渲染完成
    this.cdr.detectChanges();
  });
}

4. 移除无效的全局监听

删除ngAfterViewInit中基于dataReady$的订阅,因为逻辑已移到行展开事件中:

ngAfterViewInit(): void {
  // 移除原有订阅逻辑
}
修改后的完整代码

component.ts

import {
  ChangeDetectorRef,
  Component,
  ElementRef,
  OnDestroy,
  OnInit,
  ViewEncapsulation
} from '@angular/core';

import { ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import * as d3 from 'd3';
import { PlayersService } from '../_services/players.service';

@UntilDestroy()
@Component({
  selector: 'player-summary-component',
  templateUrl: './player-summary.component.html',
  styleUrls: ['./player-summary.component.scss'],
  encapsulation: ViewEncapsulation.None,
})
export class PlayerSummaryComponent implements OnInit, OnDestroy {
  
  endpoint: any;
  apiResponse: any;
  isRowExpanded: boolean[] = [];
  columnCount = 20;
  playerID: number = 1;
  gameID: number = 0;

  constructor(
    protected activatedRoute: ActivatedRoute,
    protected cdr: ChangeDetectorRef,
    protected playersService: PlayersService,
    private el: ElementRef
  ) { }

  ngOnInit(): void {
    this.fetchApiResponse();
  }
  
  toggleRow(index: number) {
    this.isRowExpanded[index] = !this.isRowExpanded[index];
    
    if (this.isRowExpanded[index]) {
      // 延迟执行确保DOM已更新
      setTimeout(() => {
        const courtContainer = d3.select(`#court-container-${index}`);
        this.drawCourt(this.apiResponse.games[index].shots, courtContainer);
      }, 0);
    } else {
      // 清理已绘制的图表
      const courtContainer = d3.select(`#court-container-${index}`);
      courtContainer.selectAll("*").remove();
    }
  }
  
  changeParams(): void {
    this.fetchApiResponse();
  }

  fetchApiResponse(): void {
    this.playersService.getPlayerSummary(this.playerID).pipe(untilDestroyed(this)).subscribe(data => {
      this.endpoint = data.endpoint;
      this.apiResponse = data.apiResponse;
      this.isRowExpanded = new Array(this.apiResponse.games.length).fill(false);
      this.cdr.detectChanges();
    });
  }

  drawCourt(shots: any[], container: d3.Selection<HTMLElement, any, any, any>) {
    // 你的D3绘制逻辑
    console.log("绘制图表到容器:", container);
    console.log("投篮数据:", shots);
  }

  ngOnDestroy(): void {
    // 清理逻辑
  }
}

HTML模板

<tbody>
  <ng-container *ngFor="let game of apiResponse?.games; let i = index">
    <tr [id]="'row-' + i">
      <td class="left hidden">{{ game.date }}</td>
      <td class="left first__left">{{ game.isStarter }}</td>
      <td class="left">{{ game.minutes }}</td>
      <td class="left"></td>
      <td class="left hidden">{{ game.points }}</td>
      <td class="right highlight highlight--first switch">
        {{ game.assists }}
      </td>
      <td class="right highlight highlight switch">
        {{ game.offensiveRebounds }}
      </td>
      <td class="right highlight highlight --last switch">
        {{ game.defensiveRebounds }}
      </td>
      <td class="right switch">{{ game.steals }}</td>
      <td class="right switch">{{ game.blocks }}</td>
      <td class="right switch">{{ game.turnovers }}</td>
      <td class="right switch">
        {{ game.defensiveFouls }}
      </td>
      <td class="right switch">
        {{ game.offensiveFouls }}
      </td>
      <td class="right switch">
        {{ game.freeThrowsMade }}
      </td>
      <td class="right switch">
        {{ game.freeThrowsAttempted }}
      </td>
      <td class="right switch">
        {{ game.twoPointersMade }}
      </td>
      <td class="right switch">
        {{ game.twoPointersAttempted }}
      </td>
      <td class="right switch">
        {{ game.threePointersMade }}
      </td>
      <td class="right switch">
        {{ game.threePointersAttempted }}
      </td>
      <td class="right switch" (click)="toggleRow(i)">
        {{ isRowExpanded[i] ? "▲" : "▼" }}
      </td>
    </tr>
    <tr
      [id]="'expanded-row-' + i"
      [style.display]="isRowExpanded[i] ? 'table-row' : 'none'"
    >
      <td [attr.colspan]="columnCount">
        <div id="court-container-{{i}}">dwewedwed</div>
      </td>
    </tr>
  </ng-container>
</tbody>

内容的提问来源于stack exchange,提问作者Notryan Notnet

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 19:53:10