tengo un componente en angular que es una especie de grid designer donde en cada celda cargo un componente y todo queda reflejado en un json.
Cómo puedo hacer que cuando edito una celda, solo se actualice su parte del json y no se actualice el json al completo?
ocurre cuando pulso un botón que va a applyKPI(), que además de actualizar el json, renderiza y crea el componente y debe seguir creándolo

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 = [''];




  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) => {
        
      }
    );
  }

  onMultiselectionListChange(e:any, col:any){
  console.log("🔰 ~ onMultiselectionListChange ~ col:", col)
  console.log("🔰 ~ onMultiselectionListChange ~ e:", e)
  this.renderDynamicComponents(col);
  }
  
  

  private initializeGridData(data: any[]): any[] {
    return data.map(row => ({
      ...row,
      columns: row.columns.map((col: any) => ({
        ...col,
        content: col.content || null,
        alternativeContent: null,
        showSwitch: false,        
        switchLeftText: 'Izquierda',  // texto por defecto
        switchRightText: 'Derecha',  // texto por edfecto
        switchActiveSide: 'left', // switch izquierdo por defecto
        multiSelection: false
      })),
      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 {    
    console.log("🔰 ~ renderDynamicComponents ~ specificCol:", specificCol)
    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> {
    console.log("🔰 ~ createComponent ~ componentName:", componentName)
    this.spinner.show();
    try {
      const spinnerName = `spinner-${rowIndex}-${colIndex}`;
      this.spinner.show(spinnerName);

      if (!Array.isArray(componentName)) {
        componentName = [componentName];
      }

      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) {
    console.log("🔰 ~ editKPI ~ col:", col)
    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) {
      col.multiSelection =!col.multiSelection;
      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) {
        console.log("🔰 ~ applyKPI ~ this.editingCell.content:", 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);
      console.log("🔰 ~ applyKPI ~ this.gridData:", this.gridData)
      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;
    }


}