Lattice Grid Buy a licence

demo D280

Bring rows in from a CSV

Paste a block or drop a file, each column typed and mapped onto your fields, shown as a preview so nothing lands until you confirm; a spreadsheet block copied from Excel pastes the same way

grid.import.preview() · apply()

Building…
Loading a live grid…

The configuration

'csv-import': () => ({
  rows: [],
  config: {},
  foot: [
    'paste a block of comma or tab separated text, or drop a CSV file straight onto the grid',
    'each column’s type is inferred and mapped onto the grid’s own fields, and the parse is shown as a preview first',
    'nothing changes until you confirm, so a ragged or mismatched file is caught before it lands',
    'a spreadsheet block copied from Excel pastes the same way, tabs and all',
  ],
  mount: (el: HTMLElement, LG: any) => {
    const columns = [
      { field: 'sku', title: 'SKU', layout: { width: 110, pin: 'start' } },
      { field: 'name', title: 'Product', layout: { flex: 1, min: 160 } },
      { field: 'category', title: 'Category', filter: { type: 'set' } },
      { field: 'price', title: 'Price', type: 'number', format: { style: 'currency', currency: 'GBP' }, total: 'sum' },
      { field: 'stock', title: 'In stock', type: 'number', total: 'sum' },
    ];
    const rows = [
      { id: 'A-100', sku: 'A-100', name: 'Oak desk', category: 'Furniture', price: 240, stock: 12 },
      { id: 'A-101', sku: 'A-101', name: 'Task chair', category: 'Furniture', price: 130, stock: 40 },
      { id: 'B-200', sku: 'B-200', name: 'Desk lamp', category: 'Lighting', price: 35, stock: 88 },
    ];
    const sample =
      'sku,name,category,price,stock\n' +
      'B-201,Floor lamp,Lighting,72,24\n' +
      'C-300,Bookshelf,Furniture,180,9\n' +
      'C-301,Filing cabinet,Furniture,150,15\n' +
      'D-400,Whiteboard,Office,60,30\n';
    const wrap = document.createElement('div');
    wrap.style.cssText = 'display:flex;flex-direction:column;gap:12px;height:560px';
    const controls = document.createElement('div');
    controls.style.cssText = 'display:flex;flex-direction:column;gap:8px';
    const area = document.createElement('textarea');
    area.value = sample;
    area.setAttribute('aria-label', 'CSV to import');
    area.spellcheck = false;
    area.style.cssText = 'font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;padding:8px 10px;border:1px solid var(--rule);border-radius:7px;background:var(--paper);color:var(--ink);resize:vertical;min-height:78px';
    const bar = document.createElement('div');
    bar.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;align-items:center';
    const mkBtn = (label: string) => {
      const b = document.createElement('button');
      b.type = 'button';
      b.textContent = label;
      b.style.cssText = 'font:inherit;padding:6px 12px;border:1px solid var(--rule);background:var(--paper);color:var(--ink);border-radius:7px;cursor:pointer';
      return b;
    };
    const previewBtn = mkBtn('Preview');
    const confirmBtn = mkBtn('Confirm and add');
    const cancelBtn = mkBtn('Cancel');
    confirmBtn.disabled = true;
    cancelBtn.disabled = true;
    const readoutEl = document.createElement('span');
    readoutEl.style.cssText = 'font-size:.8125rem;color:var(--ink-2)';
    bar.append(previewBtn, confirmBtn, cancelBtn, readoutEl);
    controls.append(area, bar);
    const gridEl = document.createElement('div');
    gridEl.style.cssText = 'flex:1;min-height:0;border:1px solid var(--rule);border-radius:9px;background:var(--paper)';
    wrap.append(gridTitle('Paste or drop a CSV, preview, then confirm'), controls, gridEl);
    el.append(wrap);
    const grid = LG.createGrid(gridEl, {
      rowKey: 'id',
      columns,
      rows,
      import: true,
    });
    let pending: any = null;
    const resetPreview = () => {
      pending = null;
      confirmBtn.disabled = true;
      cancelBtn.disabled = true;
    };
    previewBtn.addEventListener('click', () => {
      try {
        const preview = grid.import.preview(area.value);
        pending = preview;
        const n = preview.rowCount ?? (preview.records?.length ?? 0);
        const cols = preview.columns?.length ?? 0;
        const warn = preview.warnings?.length ?? 0;
        readoutEl.textContent =
          `Ready to add ${n} row${n === 1 ? '' : 's'} across ${cols} columns` +
          (warn ? `, ${warn} warning${warn === 1 ? '' : 's'}` : '') + '.';
        readoutEl.style.color = warn ? '#c0392b' : 'var(--ink-2)';
        confirmBtn.disabled = n === 0;
        cancelBtn.disabled = false;
      } catch (err) {
        readoutEl.textContent = `Could not read that text: ${(err as Error)?.message ?? err}`;
        readoutEl.style.color = '#c0392b';
        resetPreview();
      }
    });
    confirmBtn.addEventListener('click', () => {
      if (!pending) return;
      grid.import.apply(pending, { mode: 'append' });
      readoutEl.textContent = 'Rows added.';
      readoutEl.style.color = 'var(--ink-2)';
      resetPreview();
    });
    cancelBtn.addEventListener('click', () => {
      readoutEl.textContent = 'Preview discarded.';
      readoutEl.style.color = 'var(--ink-2)';
      resetPreview();
    });
    return () => grid?.destroy?.();
  },
})