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

Ionic7中多实例自定义子组件数据与事件绑定异常问题

问题描述

在Ionic7项目中,父页面包含三个<app-select>自定义组件实例,分别传入districts、parishes、villages不同数组数据。组件初始可正常展示对应数据,但点击任意实例时,弹出的模态框始终显示第一个实例的districts数据,选中值也始终绑定到第一个实例。尝试通过open方法传参后数据显示正常,但选中事件仍触发第一个实例的回调函数。


父组件代码

home.page.html

<ion-content>
        <app-select
          title="Select District"
          [data]="districts"
          itemTextField="text"
          [multiple]=false
          (click)="select.open()"
          (selectedChanged)="districtChanged($event)"
        #select></app-select>

        <app-select
          title="Select Parish"
          [data]="parishes"
          itemTextField="text"
          [multiple]=false
          (click)="select.open()"
          (selectedChanged)="parishChanged($event)"
        #select></app-select>

        <app-select
          title="Select Village"
          [data]="villages"
          itemTextField="text"
          [multiple]=false
          (click)="select.open()"
          (selectedChanged)="villageChanged($event)"
        #select></app-select>
</ion-content>

home.page.ts

import { Component, OnInit, ViewChild } from '@angular/core';
import { IonDatetime, IonModal } from '@ionic/angular';

export interface Item {
  text: string;
  value: string;
}

@Component({
  selector: 'app-add-exporter',
  templateUrl: './add-exporter.page.html',
  styleUrls: ['./add-exporter.page.scss'],
})
export class AddExporterPage implements OnInit {

  @ViewChild('modal', { static: true }) modal!: IonModal;

  public selectedDistrict: String = '';
  public selectedParish: String = '';
  public selectedVillage: String = '';

  public district: any;
  public districts: Item[] = [
    { text: 'District 1', value: 'District 1' },
    { text: 'District 2', value: 'District 2' },
    { text: 'District 3', value: 'District 3' },
    { text: 'District 4', value: 'District 4' },
    { text: 'District 5', value: 'District 5' },
    { text: 'District 6', value: 'District 6' },
    { text: 'District 7', value: 'District 7' },
    { text: 'District 8', value: 'District 8' },
    { text: 'District 9', value: 'District 9' }
  ];

  public parish: any;
  public parishes: Item[] = [
    { text: 'Parish 1', value: 'Parish 1' },
    { text: 'Parish 2', value: 'Parish 2' },
    { text: 'Parish 3', value: 'Parish 3' },
    { text: 'Parish 4', value: 'Parish 4' },
    { text: 'Parish 5', value: 'Parish 5' },
    { text: 'Parish 6', value: 'Parish 6' },
    { text: 'Parish 7', value: 'Parish 7' },
    { text: 'Parish 8', value: 'Parish 8' },
    { text: 'Parish 9', value: 'Parish 9' }
  ];

  public village: any;
  public villages: Item[] = [
    { text: 'Village 1', value: 'Village 1' },
    { text: 'Village 2', value: 'Village 2' },
    { text: 'Village 3', value: 'Village 3' },
    { text: 'Village 4', value: 'Village 4' },
    { text: 'Village 5', value: 'Village 5' },
    { text: 'Village 6', value: 'Village 6' },
    { text: 'Village 7', value: 'Village 7' },
    { text: 'Village 8', value: 'Village 8' },
    { text: 'Village 9', value: 'Village 9' }
  ];

  constructor() {
    
  }

  ngOnInit() {
    
  }

  districtChanged(event: any){
    this.selectedDistrict = event[0].text;
  }

  parishChanged(event: any){
    this.selectedParish = event[0].text;
  }

  villageChanged(event: any){
    this.selectedVillage = event[0].text;
  }

}

子组件代码

select.component.html

<div *ngIf="selected.length; else placeholder">
  <span *ngFor="let item of selected; let last = last">
    {{ leaf(item) }}{{ last ? '' : ', '}}
  </span>
</div>
<ng-template #placeholder>Select</ng-template>

<ion-modal [isOpen]="isOpen" (willDismiss)="cancel()">
  <ng-template>
    <ion-header class="ion-no-border">
      <ion-toolbar color="primary">
        <ion-title>{{ title }}</ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content class="ion-padding-horizontal">
      <ion-item lines="full" *ngFor="let item of filtered">
        <ion-checkbox
          slot="start"
          labelPlacement="end"
          justify="start"
          [(ngModel)]="item.selected"
          (ionChange)="itemSelected()"
        >
          {{ leaf(item) }}
        </ion-checkbox>
      </ion-item>
    </ion-content>

  </ng-template>
</ion-modal>

select.component.ts

import { CommonModule } from '@angular/common';
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges} from '@angular/core';
import { IonicModule, SearchbarCustomEvent } from '@ionic/angular';

@Component({
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule],
  selector: 'app-select',
  templateUrl: './select.component.html',
  styleUrls: ['./select.component.scss'],
})
export class SelectComponent  implements OnChanges {

  @Input() title = 'Search';
  @Input() data: any = [];
  @Input() multiple: boolean = false;
  @Input() itemTextField = 'name';
  @Output() selectedChanged: EventEmitter<any> = new EventEmitter();
  
  isOpen: boolean = false;
  selected: any[] = [];
  filtered: any[] = [];

  constructor() { }

  ngOnChanges(changes: SimpleChanges): void {
    this.filtered = this.data;
  }

  open(){
    this.isOpen = true;
    this.filtered = this.data;
  }

  cancel(){
    this.isOpen = false;
  }

  select(){
    this.isOpen = false;
  }

  leaf(obj: any){
    return this.itemTextField.split('.').reduce((val, el) => val[el], obj);
  }

  itemSelected(){
    if(!this.multiple){
      if(this.selected.length){
        this.selected[0].selected = false;
      }
      this.selected = this.data.filter((item: any) => item.selected);
      this.selectedChanged.emit(this.selected);
      this.isOpen = false;
    }
  }

}

修复方案

1. 父组件模板:使用唯一模板引用变量

三个<app-select>使用了重复的#select引用变量,Angular只会识别第一个匹配的组件,导致所有点击都触发第一个组件的open方法。修改为唯一变量:

<ion-content>
        <app-select
          title="Select District"
          [data]="districts"
          itemTextField="text"
          [multiple]=false
          (click)="districtSelect.open()"
          (selectedChanged)="districtChanged($event)"
        #districtSelect></app-select>

        <app-select
          title="Select Parish"
          [data]="parishes"
          itemTextField="text"
          [multiple]=false
          (click)="parishSelect.open()"
          (selectedChanged)="parishChanged($event)"
        #parishSelect></app-select>

        <app-select
          title="Select Village"
          [data]="villages"
          itemTextField="text"
          [multiple]=false
          (click)="villageSelect.open()"
          (selectedChanged)="villageChanged($event)"
        #villageSelect></app-select>
</ion-content>

2. 子组件:避免修改原数据,创建副本处理状态

子组件直接修改传入的data数组元素的selected属性,会导致原数组被污染,多个组件共享引用时状态混乱。修改为深拷贝数据:

import { CommonModule } from '@angular/common';
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges} from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule],
  selector: 'app-select',
  templateUrl: './select.component.html',
  styleUrls: ['./select.component.scss'],
})
export class SelectComponent implements OnChanges {

  @Input() title = 'Search';
  @Input() data: any = [];
  @Input() multiple: boolean = false;
  @Input() itemTextField = 'name';
  @Output() selectedChanged: EventEmitter<any> = new EventEmitter();
  
  isOpen: boolean = false;
  selected: any[] = [];
  filtered: any[] = [];

  constructor() { }

  ngOnChanges(changes: SimpleChanges): void {
    // 深拷贝数据,避免修改原数组
    this.filtered = JSON.parse(JSON.stringify(this.data));
  }

  open(){
    this.isOpen = true;
    // 每次打开重置数据,同步已选中状态
    this.filtered = JSON.parse(JSON.stringify(this.data));
    this.filtered.forEach(item => {
      item.selected = this.selected.some(selectedItem => selectedItem.value === item.value);
    });
  }

  cancel(){
    this.isOpen = false;
  }

  leaf(obj: any){
    return this.itemTextField.split('.').reduce((val, el) => val[el], obj);
  }

  itemSelected(){
    if(!this.multiple){
      // 重置所有选中状态
      this.filtered.forEach(item => item.selected = false);
      // 获取当前点击的选中项
      const clickedItem = this.filtered.find(item => item.selected);
      if(clickedItem){
        clickedItem.selected = true;
        this.selected = [clickedItem];
        // 传递副本给父组件,避免外部修改内部状态
        this.selectedChanged.emit([{...clickedItem}]);
      }
      this.isOpen = false;
    } else {
      this.selected = this.filtered.filter(item => item.selected);
      this.selectedChanged.emit([...this.selected]);
    }
  }

}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 22:27:03