Moving Rows that have "Array of Object" Data Binding Approach

Tags: #<Tag:0x00007efc64820ae8>

When i try to drag and drop a row, the row does not move and i get an error:
“Uncaught TypeError: node is undefined” which is coming from the render function in view.mjs file.

It seem like the error is happening when i use the Array of Object Approach.
Here is my implementation code:

Dummy Data:

export const DummyData = [
  { id: 1, name: {first: 'Ted', last: 'Right'}, address: 'sdfs', selected:false },
  { id: 2, name: {first: '', last: ''}, address: 'sdfs', selected:false },// Add empty properties for consistency
  { id: 3, name: {first: 'Joan', last: 'Right'}, address: 's', selected:false },
  { id: 4, name: {first: 'Ted', last: 'Right'}, address: 'zxc', selected:false },
  { id: 5, name: {first: 'Joan', last: 'Well'}, address: 'zxc', selected:false },
  { id: 6, name: {first: 'Joan', last: 'Well-Well'}, address: 'qw', selected:false },
  { id: 7, name: {first: 'Joan', last: 'Well'}, address: 'zxc', selected:false },
  { id: 8, name: {first: 'Ted', last: 'Right'}, address: 'zxsc', selected:false },
  { id: 9, name: {first: 'Med', last: 'Right-Left'}, address: 'asd', selected:false },
  { id: 10, name: {first: 'Med', last: 'Right'}, address: 'asd', selected:false },
  { id: 11, name: {first: 'Med', last: 'Right'}, address: 'asd', selected:true }
];

export const getDummyData=()=>{return DummyData};

Mapping of data

// where is the data located
  mapColumns=()=>{
    return [
      {data:'name.first'},
      {data:'name.last'},
      {data:'address'}
    ]
  }

HTML:

<div>

    <hot-table
      #table
      class="hot"
      [data]="dataset"
      height="1000"
      [colWidths]="[140, 192, 100, 90, 90, 110, 97, 100, 126]"
      [colHeaders]="colHeaders"
      [rowHeaders]="true"
      [dropdownMenu]="true"
      [hiddenColumns]="hiddenColumns"
      [contextMenu]="true"
      [multiColumnSorting]="true"
      [filters]="true"
      [afterOnCellMouseDown]="changeCheckboxCell"
      [afterGetColHeader]="alignHeaders"
      [afterGetRowHeader]="drawCheckboxInRowHeaders"
      [beforeRenderer]="addClassesToRows"
      [afterRowMove]="afterRowMove"
      [manualRowMove]="true"
      [columns]="mapColumns()"
      licenseKey="non-commercial-and-evaluation"
    >
    </hot-table>

</div>

i basically took the demo code and just change the data to the more practical data list. And added the mapColumns function.

That’s about it. Looks like it is because the dummy data list is an array of objects. But i followed the documentation. Binding to data - JavaScript Data Grid | Handsontable

Any ideas why this is happening? Thank you

Hi @aaron.dasani

Could you please share a demo and your environmental settings?

There are some variables missing:

  • addClassesToRows
  • drawCheckboxInRowHeaders
  • changeCheckboxCell
  • alignHeaders
  • afterRowMove
  • hiddenColumns

Especially, afterRowMove and changeCheckboxCell seems to be crucial to this case.

Definitely,

Here they are (those are the predefined functions the demo give us an is located in utils/hooks-callbacks.ts. except for the afterRowMove function which i added):

import Handsontable from "handsontable";
import {
  SELECTED_CLASS,
  ODD_ROW_CLASS
} from "../constants/data.const";

const headerAlignments = new Map([
  ["9", "htCenter"],
  ["10", "htRight"],
  ["12", "htCenter"]
]);

type AddClassesToRows = (
  TD: HTMLTableCellElement,
  row: number,
  column: number,
  prop: number | string,
  value: any,
  cellProperties: Handsontable.CellProperties
) => void;

export const addClassesToRows: AddClassesToRows = (
  TD,
  row,
  column,
  prop,
  value,
  cellProperties
) => {
  // Adding classes to `TR` just while rendering first visible `TD` element
  if (column !== 0) {
    return;
  }

  const parentElement = TD.parentElement;

  if (parentElement === null) {
    return;
  }

  // Add class to selected rows
  if (cellProperties.instance.getDataAtRowProp(row, "0")) {
    Handsontable.dom.addClass(parentElement, SELECTED_CLASS);
  } else {
    Handsontable.dom.removeClass(parentElement, SELECTED_CLASS);
  }

  // Add class to odd TRs
  if (row % 2 === 0) {
    Handsontable.dom.addClass(parentElement, ODD_ROW_CLASS);
  } else {
    Handsontable.dom.removeClass(parentElement, ODD_ROW_CLASS);
  }
};

type DrawCheckboxInRowHeaders = (
  this: Handsontable,
  row: number,
  TH: HTMLTableCellElement
) => void;

export const drawCheckboxInRowHeaders: DrawCheckboxInRowHeaders = function drawCheckboxInRowHeaders(
  row,
  TH
) {
  const input = document.createElement("input");

  input.type = "checkbox";

  if (row >= 0 && this.getDataAtRowProp(row, "0")) {
    input.checked = true;
  }

  Handsontable.dom.empty(TH);

  TH.appendChild(input);
};

export function alignHeaders(this: Handsontable, column: number, TH: HTMLTableCellElement) {
  if (column < 0) {
    return;
  }

  const alignmentClass = this.isRtl() ? "htRight" : "htLeft";

  if (TH.firstChild) {
    if (headerAlignments.has(column.toString())) {
      Handsontable.dom.removeClass(TH.firstChild as HTMLElement, alignmentClass);
      Handsontable.dom.addClass(TH.firstChild as HTMLElement, headerAlignments.get(column.toString()) as string);
    } else {
      Handsontable.dom.addClass(TH.firstChild as HTMLElement, alignmentClass);
    }
  }
};

type ChangeCheckboxCell = (
  this: Handsontable,
  event: MouseEvent,
  coords: { row: number; col: number }
) => void;

export const changeCheckboxCell: ChangeCheckboxCell = function changeCheckboxCell(
  event,
  coords
) {
  const target = event.target as HTMLInputElement;

  if (coords.col === -1 && event.target && target.nodeName === "INPUT") {
    event.preventDefault(); // Handsontable will render checked/unchecked checkbox by it own.

    this.setDataAtRowProp(coords.row, "0", !target.checked);
  }
};

//
export function afterRowMove(this: Handsontable, movedRows:number[], finalIndex:number, dropIndex:number | undefined, movePossible:boolean, orderChanged:boolean){
  if(!orderChanged) return;
  
  console.log("------")
  console.log(movedRows)
  console.log("finalIndex",finalIndex)
  console.log("dropIndex",dropIndex)
  console.log("movePossible",movePossible)
  console.log("orderChanged",orderChanged)
  console.log(this.getDataAtRow(finalIndex))
  console.log("------")
  
  //this.render();
}

In the component side:

  dataset = getDummyData();
  alignHeaders = alignHeaders
  drawCheckboxInRowHeaders = drawCheckboxInRowHeaders;
  addClassesToRows = addClassesToRows;
  changeCheckboxCell = changeCheckboxCell;
  progressBarRenderer = progressBarRenderer;
  starsRenderer = starsRenderer;
  afterRowMove = afterRowMove;

  hiddenColumns = {
    indicators: true
  };

let me know if i can provide you with anything…
Also upon further digging, it looks like when i use data list that have an “array of array” format the issue does not happen. But i really need the “array of object” format to work.

Thank you

Yes. If you could share that as a demo (JSFiddle/ codesandbox, stackblitz) that would be better as then we’ll be able to debug the code together and share some implementation tips.

1 Like

here it is: Handsontable Data Grid v12.4.0 - Angular Demo (forked) - CodeSandbox

I hope you are able to access it. I just randomly stumble into what function is causing the issue. it looks like it the mapColumns() function that tell the table where the data.

Im not sure why that’s causing it. i followed the documentation.

Im gonna bring my computer home, so i can reply quickly, instead of being the next day. thank you

Alright this is weird. So if i add the return array value from mapColumns() in the [columns] attribute in the HTML like such:

I don’t receive the node undefined error, when moving the rows. This is confusing on why this occurs.

does it have to do with the [columns] attribute having the evaluate the function every time a render event happens, instead of just having the array value implicit stated without having to run a function to get the value

Sorry for the delay @aaron.dasani

I was able to replicate the issue. But it seems that the issue comes from you custom code of the addClassesToRows. When I comment it out the error is not replicable. Then we are just blocked with the move row action due to the custom logic of afterRowMove.

No problem aleksandra :slight_smile:
The addClassesToRows was from the documentation demo. So i did not touch or create that piece.

However, I was also able to resolve the issue by explicitly adding the return value of mapColumns() function in the [columns] attribute in the HTML. Which is kinda annoying but i can deal with for now. I don’t know the reason why this is an issue.

Hi @aaron.dasani

Aleksandra is out of the office for few days. I’m glad that you were able to find the solution for this problem. Can I close this topic now?

Hey Adrian,
I have one more question. I notice that when I merge a cell, the value in the cell get updated to “null” in the data-source. Why would the data-source get updated when the merge cell happen? Should it just update the dataset HandsonTable make?

Hi @aaron.dasani

This is how mergeCells plugin is designed. Did you encounter any issues because of this solution?

Hi Adrian,

Definitely encountering some issues. I wrote a piece of algorithm that merge the cells in the table if the cell value are the same. Im doing that at the start of the application an also when a user goes to view-mode/readonly mode.

I’m also required to unmerge those merge cells when a user goes to edit mode.

However because the merge functionality make the datatsource value for each merge cell null , i have to keep track of the old value of the cell that is getting merge before they get updated to null. This make the whole logic complex and kinda expensive in my opinion.

It would be nice if the cell value in the datasouce does not become null.

im basically doing something like that when unmerging those cells:

// The table will be rendered once after executing the callback

this.handsontable.batchRender(() => {

  mergeCellsPlugin.unmerge(0,  0, this.dataset.length-1, this.mapColumns.length-1);

  // add previous value back in the unmerge row
  this.trackMergeCells.map(t=>{
    setSourceDataAtCell(this.handsontable,t.row,t.field,t.value);
  })
});

// empty track merge cells list
this.trackMergeCells = [];

As you can see i need to use setSourceDataAtCell to make the cells null value become their old value again
It also important to mention that using setSourceDataAtCell in turn make all the cells that got updated to be consider as being edited. Which mean i will also need to make sure they become unedited.

This is the workaround i have right now, but i would like to see if there is a solution for this issue in the near future.

Also It would be nice if the unmerge cell update the value automatically for me. Is this something that is in work for the near future?

Also, are we able to have a different source trigger for merging cells.

Currently, merging/unmerging cells programmatically will call my beforeChange event and the source is “edit”. Can we have a souce called “MergeCells.edit” as a source so we can know if the beforeChange event was called because of user updating the value in a cell or just a merging cell.

If that something that will come in the future?

If not, is there a work around for this?

Hi @aaron.dasani

Thank you very much for the detailed insight about your concerns and possible ideas. For the time being we are not planning any work on mergeCells plugin, but we always value input from our users. Can I ask you to post your ideas here? https://github.com/handsontable/handsontable/discussions

I’m sure they will be valuable for our developers :slight_smile:

1 Like

definitely adrian

you can close this adrian. i try to do it but couldn’t find the button for it.
Have a good day