En mi proyecto angular tengo un componente que es una especie de grid designer y celdas personalizables donde puedo entrar en su modo edición cargo componentes. cuando elijo un componente este se mete a modo de array en el json en su content.
Ahora he añadido un modo "añadir Multiselección" y la intención de eso es que al activarlo, pueda elegir varios kpi en el modo edición y una vez elegidos poder  cambiar entre ellos fuera del modo edición (sin tocar el switch actual)
Qué se te ocurre que podría hacer? Debe funcionar tanto la selección singular como la multiselección por separado.

import { Component, OnInit, ViewContainerRef, ViewChildren, QueryList, AfterViewInit, NgModule, HostListener, ViewChild, ElementRef,inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { NgxSpinnerModule, NgxSpinnerService } from 'ngx-spinner';


import * as ace from 'ace-builds';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-dracula';

import { GaugeComponent } from '../gauge/gauge.component';
import { DataGridComponent } from '../data-grid/data-grid.component';
import { KpiListComponent } from '../kpis/kpi-list.component';
import { KpiService } from '../../services/kpi.services'
import { NgSelectModule } from '@ng-select/ng-select';

@Component({
  selector: 'app-grid-designer',
  standalone: true,
  imports: [CommonModule, FormsModule, NgSelectModule, NgxSpinnerModule],
  templateUrl: './grid-designer.component.html',
  styleUrl: './grid-designer.component.scss'
})
export class GridDesignerComponent implements OnInit, AfterViewInit {
  @ViewChildren('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainers!: QueryList<ViewContainerRef>;
  @ViewChild('contextMenu') contextMenuElement!: ElementRef;
  private kpiService = inject(KpiService);

  
  gridData: any[] = [];
  gridDataString: string = '';
  isHidden = false;
  jsonValid: boolean | null = null;
  private editor: ace.Ace.Editor | null = null;
  editingCell: any = null;
  contextMenuVisible = false;
  contextMenuX = 0;
  contextMenuY = 0;
  selectedCell: any = null;
  componentOptions = [''];
  switchActiveSide: string = 'left';




  constructor(private http: HttpClient,
              private toastr: ToastrService,
              private spinner: NgxSpinnerService
  ) {}

  ngOnInit(): void {
    this.getListKPI()
    this.http.get('assets/grid-default.json').subscribe((data: any) => {
      this.gridData = this.initializeGridData(data);
      this.gridDataString = JSON.stringify(this.gridData, null, 2);
      this.updateEditorValue();
      this.configToastr();
    });
  }

  ngAfterViewInit(): void {
    this.initAceEditor();
    setTimeout(() => this.renderDynamicComponents(), 100);
  }

  getListKPI(): void {
    this.kpiService.getListKPI().subscribe(
      (data: any[]) => {        
        this.componentOptions = data.map(item => `kpi-${item.idKPI} - ${item.representacion} - ${item.nombre}`);                
      },
        
      (error: any) => {
        
      }
    );
  }
  
  

  private initializeGridData(data: any[]): any[] {
    return data.map(row => ({
      ...row,
      columns: row.columns.map((col: any) => ({
        ...col,
        content: col.content || null,
        showSwitch: false,
        showMultiSelection: false,
        alternativeContent: null,
        switchLeftText: 'Izquierda',  // texto por defecto
        switchRightText: 'Derecha',  // texto por edfecto
        switchActiveSide: 'left' // switch izquierdo por defecto
      })),
      height: row.height || '20%',
    }));
  }

  private initAceEditor(): void {
    const editorElement = document.getElementById('editor');
    if (editorElement) {
      this.editor = ace.edit(editorElement);
      this.editor.setTheme('ace/theme/dracula');
      this.editor.session.setMode('ace/mode/json');
      this.editor.setOptions({
        fontSize: '14px',
        showPrintMargin: false,
        showGutter: true,
        highlightActiveLine: true,
        enableBasicAutocompletion: true,
        enableLiveAutocompletion: true,
        enableSnippets: true
      });

      this.updateEditorValue();

      this.editor.getSession().on('change', () => {
        this.gridDataString = this.editor?.getValue() ?? '';
        this.jsonValid=false;
      });
    } else {
      console.error('Error al inicializar el Editor Ace');
    }
  }

  onRightClick(event: MouseEvent, cell: any) {
    event.preventDefault();
    this.contextMenuVisible = true;
    this.contextMenuX = event.clientX;
    this.contextMenuY = event.clientY;
    this.selectedCell = cell;
  }

  @HostListener('document:click')
  onDocumentClick() {
    this.contextMenuVisible = false;
  }

  addColumnToRow() {
    if (this.selectedCell) {
      const rowIndex = this.gridData.findIndex(row => row.columns.includes(this.selectedCell));
      if (rowIndex !== -1) {
        const newColumn = { content: '', colSpan: 1, rowSpan: 1, isComponent: false, width: 'auto' };
        this.gridData[rowIndex].columns.push(newColumn);
        this.updateJsonAndEditor();
      }
    }
    this.contextMenuVisible = false;
  }

  changeRowHeight(event: Event, row: any) {
    const inputElement = event.target as HTMLInputElement;
    row.height = this.validateHeight(inputElement.value);
    this.updateJsonAndEditor();
  }

  private validateHeight(value: string): string {
    if (value.endsWith('px') || value.endsWith('%')) {
      return value;
    }
    const num = parseInt(value);
    if (isNaN(num)) return '20%';
    return `${Math.max(5, Math.min(100, num))}%`;
  }

  changeWidth(event: Event) {
    if (this.selectedCell) {
      const inputElement = event.target as HTMLInputElement;
      this.selectedCell.width = this.validateWidth(inputElement.value);
      this.updateJsonAndEditor();
    }
  }

  private validateWidth(value: string): string {
    if (value.endsWith('px') || value.endsWith('%')) {
      return value;
    }
    const num = parseInt(value);
    if (isNaN(num)) return '100%';
    return `${Math.max(10, Math.min(100, num))}%`;
  }

  adjustWidth(change: number) {
    if (this.selectedCell) {
      if (!this.selectedCell.width || this.selectedCell.width === 'auto') {
        this.selectedCell.width = '100%';
      }

      if (this.selectedCell.width.endsWith('px')) {
        const currentPx = parseInt(this.selectedCell.width);
        this.selectedCell.width = `${Math.max(50, currentPx + change)}px`;
      } else {
        const currentPercent = parseInt(this.selectedCell.width);
        this.selectedCell.width = `${Math.max(10, Math.min(100, currentPercent + change))}%`;
      }
      this.updateJsonAndEditor();
    }
  }

  adjustRow(row: any, change: number) {
    if (!row.height || row.height === 'auto') {
      row.height = '20%';
    }

    if (row.height.endsWith('px')) {
      const currentPx = parseInt(row.height);
      row.height = `${Math.max(50, Math.min(900, currentPx + change))}px`;
    } else {
      const currentPercent = parseInt(row.height);
      row.height = `${Math.max(5, Math.min(100, currentPercent + change))}%`;
    }
    this.updateJsonAndEditor();
  }


  getSelectedRow(): any {
    return this.gridData.find(row => row.columns.includes(this.selectedCell));
  }

  adjustColSpan(change: number) {
    if (this.selectedCell) {
      this.selectedCell.colSpan = Math.max(1, (this.selectedCell.colSpan || 1) + change);
      this.updateJsonAndEditor();
    }
  }

  adjustRowSpan(change: number) {
    if (this.selectedCell) {
      this.selectedCell.rowSpan = Math.max(1, (this.selectedCell.rowSpan || 1) + change);
      this.updateJsonAndEditor();
    }
  }

  deleteCell() {
    if (this.selectedCell) {
      const rowIndex = this.gridData.findIndex(row => row.columns.includes(this.selectedCell));
      if (rowIndex !== -1) {
        const columnIndex = this.gridData[rowIndex].columns.indexOf(this.selectedCell);
        this.gridData[rowIndex].columns.splice(columnIndex, 1);
        if (this.gridData[rowIndex].columns.length === 0) {
          this.gridData.splice(rowIndex, 1);
        }
        this.updateJsonAndEditor();
      }
    }
    this.contextMenuVisible = false;
  }

  
  addRow() {
    const newRow = {
      height: '100px',
      columns: [{ content: '', colSpan: 1, rowSpan: 1, isComponent: false, width: '100%' }]
    };
    this.gridData.push(newRow);
    this.updateJsonAndEditor();
  }

  addColumn() {
    this.gridData.forEach(row => {
      row.columns.push({ content: '', colSpan: 1, rowSpan: 1, isComponent: false });
    });
    this.updateJsonAndEditor();
  }

   updateJsonAndEditor() {
    this.gridDataString = JSON.stringify(this.gridData, null, 2);
    this.updateEditorValue();
    //setTimeout(() => this.renderDynamicComponents() );
  }

  private updateEditorValue(): void {
    if (this.editor && this.gridDataString) {
      this.editor.setValue(this.gridDataString);
      this.editor.clearSelection();
    }
  }


  configToastr(){
    this.toastr.toastrConfig.positionClass = 'toast-top-full-width';
    this.toastr.toastrConfig.extendedTimeOut = 0;
    this.toastr.toastrConfig.timeOut = 2900;
  }

  toggleSidebar() {
    this.isHidden = !this.isHidden;
}

  getGridTemplateColumns(columns: any[]): string {
    return columns.map(col => col.width).join(' ');
  }

  getGridTemplateRows(): string {
    return this.gridData.map(row => row.height).join(' ');
  }

  validateAndApplyJson() {
    try {
      const parsedData = JSON.parse(this.gridDataString);
      this.gridData = parsedData;
      this.jsonValid = true;
      setTimeout(() => this.renderDynamicComponents(), 100);
    } catch (error) {
      this.jsonValid = false;
      this.toastr.warning(`Error al procesar el JSON: ${error}`, 'Advertencia');
    }
  }

  renderDynamicComponents(specificCol?: any): void {
    setTimeout(() => {
      if (!this.dynamicComponentContainers) return;

      let containerIndex = 0;
      this.gridData.forEach((row, rowIndex) => {
        row.columns.forEach((col: any, colIndex: number) => {
          if (col.isComponent) {
            if (!specificCol || col === specificCol) {
              const container = this.dynamicComponentContainers.get(containerIndex);
              if (container) {
                container.clear();
                this.createComponent(col.content, container, col, rowIndex, colIndex);
              }
            }
            containerIndex++;
          }
        });
      });
    }, 100);
  }
  
  

  async createComponent(componentName: string, container: ViewContainerRef, col: any, rowIndex: number, colIndex: number): Promise<void> {
    this.spinner.show();
    try {
      const spinnerName = `spinner-${rowIndex}-${colIndex}`;
      this.spinner.show(spinnerName);

      let ComponentType: any;
      const kpiNumber = componentName[0]; // Extract the number from the array
  
      switch (kpiNumber) {
        case 'kpi-56':
          ComponentType = await this.loadDynamicComponent(`kpi-${kpiNumber}`);
          break;
        default:
          ComponentType = await this.loadDynamicComponent(`kpi-${kpiNumber}`);
          break;
      }
  
      const componentRef = container.createComponent(ComponentType);
      (componentRef.instance as any).kpiNumber = kpiNumber;
        
        
      // Check if the component has a card representation
      if ((componentRef.instance as any).isCard) {        
        col.isCard = true;
      } else {
        col.isCard = false;
      }
      this.spinner.hide(spinnerName);

    } catch (error) {
      this.spinner.hide(`spinner-${rowIndex}-${colIndex}`);

      this.toastr.error(`Error al crear el componente: ${componentName}`);
    } finally {
      // this.spinner.hide(); // ocultar el spinner
    }
  }
  

private async loadDynamicComponent(componentName: string): Promise<any> {
    const kpiNumber = parseInt(componentName.split('-')[1]);    
    const kpiRequest = {
        fechaDesde: "2023-01-01",
        fechaHasta: "2023-12-31",
        listKpi: [kpiNumber],
        sucursales: ""
    };
    
    const data = await this.kpiService.getKPI(kpiRequest).toPromise();
    console.log("🔰 ~ loadDynamicComponent ~ data:", data)

    let representacion: string | null = null;

    if (data.representacion) {
        representacion = data.representacion;
    }

    if (!representacion) {
        this.toastr.error(`No se encuentra "representacion" en el json para: ${componentName}`);
        throw new Error(`No se encuentra "representacion" en el json para: ${componentName}`);
    }

    const module = await import(`../kpi-components/kpi-${representacion}/kpi-${representacion}.component`);
    return module[`Kpi${this.pascalCase(representacion)}Component`];
}



  // convertir a pascalcase el string para que funcione
  private pascalCase(str: string): string {
    return str.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('');
  }

  
  editKPI(col: any) {
    if (Array.isArray(col.content)) {
      col.content = col.content.map((item: number) => `kpi-${item}`).join(', ');
    } else if (typeof col.content !== 'string' || !col.content.startsWith('kpi-')) {
      col.content = null;
    }
    
    col.switchLeftText = col.switchLeftText || 'Izquierda';
    col.switchRightText = col.switchRightText || 'Derecha';
    this.editingCell = col;
  }
  
    toggleSwitch(col: any) {            
      console.log("🔰 ~ toggleSwitch ~ col:", col)
      if (!col.showSwitch) {
        col.alternativeContent = null;
      }
      this.updateJsonAndEditor();
    }

    toggleMultiSelection(col: any) {
      console.log("Multiselección ", col);
    }
  
    switchComponent(col: any, side: string) {
      [col.content, col.alternativeContent] = [col.alternativeContent, col.content];
      col.switchActiveSide = side;
      this.renderDynamicComponents(col);
      this.updateJsonAndEditor();
    }

    applyKPI() {
      if (this.editingCell.content) {
        const kpiNumbers = this.editingCell.content.split(',').map((item: string) => this.extractKpiNumber(item.trim()));
        this.editingCell.content = kpiNumbers;
      }
    
      if (this.editingCell.showSwitch && this.editingCell.alternativeContent) {
        const altKpiNumbers = this.editingCell.alternativeContent.split(',').map((item: string) => this.extractKpiNumber(item.trim()));
        this.editingCell.alternativeContent = altKpiNumbers;
      }
    
      this.editingCell.isComponent = this.editingCell.content.length > 0;
      this.gridDataString = JSON.stringify(this.gridData, null, 2);
      this.updateEditorValue();
      this.renderDynamicComponents(this.editingCell);

      this.editingCell = null;
    }
    
    private extractKpiNumber(content: string): number {
      const match = content.match(/\d+/);
      return match ? parseInt(match[0], 10) : 0;
    }


}

<div class="dashboard-container">
  <div class="sidebar" [ngClass]="{ hidden: isHidden }">    
    <ng-select [items]="componentOptions"
    bindLabel="label"
    bindValue="value"
    [searchable]="true"
    placeholder="Elige un tab">
</ng-select>


    <div id="editor" class="editor-size"></div>
    <button class="button-apply-json" (click)="validateAndApplyJson()">
      Aceptar
    </button>
  </div>
  <button class="slider-button" (click)="toggleSidebar()">
    <i class="material-icons" [ngClass]="{ inverted: !isHidden }">slideshow</i>
  </button>
  <div class="main" [style.grid-template-rows]="getGridTemplateRows()">
    <div *ngFor="let row of gridData; let rowIndex = index" class="row"
      [ngStyle]="{ 'grid-template-columns': getGridTemplateColumns(row.columns) }">
      <div *ngFor="let col of row.columns; let colIndex = index" class="column"
        [ngStyle]="{
          'grid-row': col.rowSpan ? 'span ' + col.rowSpan : 'auto', 
          'grid-column': col.colSpan ? 'span ' + col.colSpan : 'auto',
          'border': col.isCard ? '0' : null
        }"
        (contextmenu)="onRightClick($event, col)">
        <button *ngIf="col != editingCell" class="buttonAddKpi" (click)="editKPI(col)">
          <i class="material-icons">build</i>
        </button>


        <div *ngIf="col === editingCell" class="edit-container">
          <div class="cell-options-container">
            <label>
              <input type="checkbox" [(ngModel)]="col.showMultiSelection" >
              Activar multiselección
            </label>
          </div>

          <div class="cell-options-container">
            <label>
              <input type="checkbox" [(ngModel)]="col.showSwitch" (change)="toggleSwitch(col)">
              Activar switch
            </label>
          </div>
          
          <!-- listbox del primer kpi -->
        <ng-select class="kpi-select"
          [searchable]="true"
          [(ngModel)]="col.content" 
          *ngIf="col === editingCell" 
          [items]="componentOptions" 
          bindLabel="label" 
          bindValue="value"
          placeholder="Escribe para buscar"
          ></ng-select>
          
          <!-- listbox del kpi alternativo -->
          <ng-select *ngIf="col.showSwitch" class="kpi-select"
            [searchable]="true"
            [(ngModel)]="col.alternativeContent" 
            [items]="componentOptions" 
            bindLabel="label" 
            bindValue="value"
            placeholder="Componente alternativo"
          ></ng-select>
        
          <div class="button-container">
            <button class="button-add" *ngIf="col.showMultiSelection" class="button-add">
              +
            </button>
            <button class="button-apply" (click)="applyKPI()">
              Aplicar
            </button>
          </div>
      </div>
        <ng-container *ngIf="!col.isComponent">{{ col.content }}</ng-container>
        <ng-container *ngIf="col.isComponent">
          <div class="cell-content">
            <div *ngIf="col.showSwitch || col.showMultiSelection" class="cell-options-container">
              <span *ngIf="col.showMultiSelection == false" class="left-title">Título</span>
              <!-- listbox de la multiselección -->
              <ng-select class="kpi-select-viewer"
              id="kpi-select-viewer"              
              ></ng-select>

              <!-- switch central -->
              <div *ngIf="col.showSwitch" class="switch">
                <div 
                  class="left" 
                  [class.active]="col.switchActiveSide === 'left'" 
                  (click)="switchComponent(col,'left')">
                  {{ col.switchLeftText }}
                </div>
                <div 
                  class="right" 
                  [class.active]="col.switchActiveSide === 'right'" 
                  (click)="switchComponent(col,'right')">
                  {{ col.switchRightText }}
                </div>
              </div>
              <!-- parte de botones -->
              <span class="right-title">Botones</span>
            </div>
            <div class="component-container">
              <ng-container #dynamicComponentContainer>
                <ngx-spinner
                [name]="'spinner-' + rowIndex + '-' + colIndex"
                bdColor="rgba(51,51,51,0.3)"
                size="medium"
                color="#fff"
                type="ball-scale-multiple"
                [fullScreen]="false"
              >  
              </ngx-spinner>
              </ng-container>
            </div>
          </div>
        </ng-container>
      </div>
    </div>
  </div>

  <div class="context-menu" *ngIf="contextMenuVisible" [style.left.px]="contextMenuX" [style.top.px]="contextMenuY"
    (click)="$event.stopPropagation()">
    <div class="context-menu-item">
      <span class="label">ColSpan</span>
      <div class="controls">
        <button (click)="adjustColSpan(-1)">-</button>
        <span>{{ selectedCell?.colSpan || 1 }}</span>
        <button (click)="adjustColSpan(1)">+</button>
      </div>
    </div>
    <div class="context-menu-item">
      <span class="label">RowSpan</span>
      <div class="controls">
        <button (click)="adjustRowSpan(-1)">-</button>
        <span>{{ selectedCell?.rowSpan || 1 }}</span>
        <button (click)="adjustRowSpan(1)">+</button>
      </div>
    </div>
    <div class="context-menu-item">
      <span class="label">Ancho</span>
      <div class="controls">
        <button (click)="adjustWidth(-5)">-</button>
        <input type="text" [value]="selectedCell?.width || ''" (change)="changeWidth($event)" />
        <button (click)="adjustWidth(5)">+</button>
      </div>
    </div>
    <div class="context-menu-item">
      <span class="label">Alto Fila</span>
      <div class="controls">
        <button (click)="adjustRow(getSelectedRow(), -5)">-</button>
        <input type="text" [value]="getSelectedRow()?.height || ''"
          (change)="changeRowHeight($event, getSelectedRow())" />
        <button (click)="adjustRow(getSelectedRow(), 5)">+</button>
      </div>
    </div>
    <div class="context-menu-item add" (click)="addColumnToRow()">
      <span class="label">Añadir Columna</span>
    </div>
    <div class="context-menu-item add" (click)="addRow()">
      <span class="label">Añadir Fila (Al final)</span>
    </div>
    <div class="context-menu-item delete" (click)="deleteCell()">
      <span class="label">Eliminar Celda</span>
    </div>
  </div>
</div>