#6623·grapesjs

BUG: Custom Dynamic List freezes after binding array and expanding rows to match length

Author: zhengtulymGhCreated Oct 10, 2025Updated Oct 10, 2025

GrapesJS version

  • I confirm to use the latest version of GrapesJS

What browser are you using?

140.0.7339.214(正式版本) (x86_64)

Reproducible demo link

https://codesandbox.io/p/devbox/d27l6n

Describe the bug

I want to create a custom dynamic list component based on GrapesJS (with 3 rows by default). It can bind to an array variable and dynamically change the number of rows in the container according to the array length. Now I'm encountering a freeze issue. The reproduction steps are as follows:

  1. Click the Variables tab on the left and create a new array variable with a length of 4.

    enter image description here

  2. Go back to the Components tab and drag a Dynamic List container component onto the canvas.

  3. Select the Dynamic List component you just dragged in. On the right, the Styles tab will show a dropdown to bind an array variable. Choose the array variable created in step 1. After selection, the dynamic list on the canvas changes from 3 rows to 4 rows, which is expected.

    enter image description here

  4. Move the mouse over the dynamic list component (now 4 rows) on the canvas. You'll notice the page freezes.

After my investigation, the key issue lies in the logic that adjusts the number of container rows based on the array length, but I couldn't pinpoint the cause.

editor.on('component:update:attributes', (comp: any, attrs?: any, opts?: any) => {
  console.log('component:update:attributes')
  if (isSyncing(editor)) return;

  if (comp.get && comp.get('type') === DL_TYPE) {
    const dlAttrs = comp.getAttributes?.() || {};
    const varName = dlAttrs[DL_ATTR_ARRAY_VAR];

    const arrData = varName ? getDLArrayData(editor, comp) : undefined;

    if (!varName || !Array.isArray(arrData)) {
      // clearBindingsAndResetDefaults(editor, comp);
    } else {
      ensureRowCountByArray(editor, comp);
    }

    return;
  }
});
function cloneIntoTarget(sourceRow: any, targetRow: any) {
  console.log('cloneIntoTarget')
  sourceRow.components().each((child: any) => {
    const cloned = child.clone();
    targetRow.append(cloned);
  });
}

function ensureRowCountByArray(editor: any, dl: any) {
  console.log('ensureRowCountByArray')
  if (isSyncing(editor)) return;
  const arr = getDLArrayData(editor, dl);
  const targetCount = Array.isArray(arr) ? (arr.length > 0 ? arr.length : 3) : 3;

  markSyncing(editor, true);
  try {
    let rows = getRows(dl);
    const curCount = rows.length;
    if (curCount !== targetCount) {
      if (rows.length === 0) {
        dl.append({ type: ROW_TYPE });
        rows = getRows(dl);
      }
      const baseRow = rows[0];

      if (curCount < targetCount) {
        const need = targetCount - curCount;
        for (let i = 0; i < need; i++) {
          const newRow = dl.append({ type: ROW_TYPE })[0];
          cloneIntoTarget(baseRow, newRow);
        }
      } else {
        for (let i = curCount - 1; i >= targetCount; i--) {
          rows[i].remove();
        }
      }
    }
  } finally {
    markSyncing(editor, false);
  }
}

What's going on here?

Code of Conduct

  • I agree to follow this project's Code of Conduct