-2

I'm trying to build a game based on a 2d grid in JavaScript.

Inside a nested for loop, I set the data attribute of a cell like this:

cell.setAttribute('data-pos',[i,j]);

In my event handler I get the data like this:

let pos = event.target.getAttribute('data-pos');

When I try to access the elements of pos something strange happens.

For a cell with data-pos of [1, 3],

I get the following values when I use console.log:

pos[0] = 1
pos[1] = ,
pos[2] = 3

Somehow the comma is being treated as an array element.

Why is this and how can I correctly pass values for i and j to the event handler please?

Yevhen Horbunkov
  • 14,965
  • 3
  • 20
  • 42
Robin Andrews
  • 3,514
  • 11
  • 43
  • 111
  • 2
    You've stringified the array. I don't really recommend storing state in the DOM like this because you'll be serializing and de-serializing it. Recommend a redesign using a data structure. Can you show a [mcve]? Thanks! – ggorlen Feb 22 '20 at 21:07
  • How did I do that? `let pos = event.target.getAttribute('data-pos');` gives an error without the quotes. – Robin Andrews Feb 22 '20 at 21:10
  • Attributes in the DOM are always strings--if you put an array like `[i,j]` into the setter, JS calls `[i,j].toString()` for you. – ggorlen Feb 22 '20 at 21:12

1 Answers1

1

As @ggorlen pointed out in the comments, the line cell.setAttribute('data-pos',[i,j]) assigns the the string value of '1,3' to your data-pos attribute (since its value cannot be an array).

If that's fine for you, you may get your data back into array with:

let pos = event.target.getAttribute('data-pos').split(',')

If you prefer to pass your data to DOM and get it back upon click and avoid parsing stringified values you may introduce 2 data-* attributes, like data-pos-i/data-pos-j:

const cells = ((i,j) => Array.from(
                {length:i*j},
                (_,k) => ({id:k, i:k%j, j:0|(k/j)%i}))
              )(5,3),
      matrix = (flatArr => 
                  flatArr.reduce((r,{id,i,j},k) => 
                    (r[i]=(r[i]||[]), r[i][j] = {id, i, j}, r), [])
               )(cells),
      tableHTML = (tableData => {
                    const rowsHTML = tableData
                                      .map(tableRow => 
                                          '<tr>'+tableRow.map(({id,i,j}) => `<td id=${id} data-pos-i="${i}" data-pos-j="${j}"></td>`).join('')+'</tr>')
                                      .join('')
                    return `<table>${rowsHTML}</table>`
                  })(matrix),
      mytable = document.getElementById('mytable')
      
mytable.innerHTML = tableHTML

mytable.addEventListener('click', ({target}) => {
  const cellData = {i:target.dataset.posI, j:target.dataset.posJ}
  console.log(cellData)
})
#mytable td {
  display: table-cell;
  width: 20px;
  height: 20px;
  background: grey;
  cursor: pointer;
}

#mytable td:hover {
  background: #fff;
}
<table id="mytable"></table>

However, the better approach would be not to bounce your data back and forth between state and DOM.

Instead, you may store your cells as a flat array of objects where each cell-object has i,j-coordinates bound to unique id (along with other necessary data), thus upon click you may look-up for any bound data by id:

const cells = ((i,j) => Array.from(
                {length:i*j},
                (_,k) => ({id:k, i:k%j, j:0|(k/j)%i}))
              )(5,3),
      matrix = (flatArr => 
                  flatArr.reduce((r,{id,i,j},k) => 
                    (r[i]=(r[i]||[]), r[i][j] = id, r), [])
               )(cells),
      tableHTML = (tableData => {
                    const rowsHTML = tableData
                                      .map(tableRow => 
                                          '<tr>'+tableRow.map(cellData => `<td id=${cellData}></td>`).join('')+'</tr>')
                                      .join('')
                    return `<table>${rowsHTML}</table>`
                  })(matrix),
      mytable = document.getElementById('mytable')
      
mytable.innerHTML = tableHTML

mytable.addEventListener('click', ({target}) => {
  const cellData = cells.find(({id}) => id == target.id)
  console.log(cellData)
})
#mytable td {
  display: table-cell;
  width: 20px;
  height: 20px;
  background: grey;
  cursor: pointer;
}

#mytable td:hover {
  background: #fff;
}
<table id="mytable"></table>
Yevhen Horbunkov
  • 14,965
  • 3
  • 20
  • 42
  • Fine, but even this doesn't quite get the data back. You need to parse the ints with `event.target.getAttribute('data-pos').split(',').map(Number)`. Ouch. All that extra work should reveal that this is fundamentally poor design. Hopefully OP can explain what they're really trying to do and avoid the [x-y problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) this likely is. – ggorlen Feb 22 '20 at 21:13
  • So help me to use a better design - how can I pass 2d array indices to an event handler properly, or is even trying to do that part of the bad design? – Robin Andrews Feb 22 '20 at 21:15
  • 1
    @ggorlen : yes, initial design requires improvement, so the fact that element attributes cannot store an array should encourage OP to introduce 2 attributes, like `data-pos-i`/`data-pos-j` to store 2 values within separate attributes – Yevhen Horbunkov Feb 22 '20 at 21:16
  • @RobinAndrews : look through my comment for the hint regarding better approach – Yevhen Horbunkov Feb 22 '20 at 21:16
  • No, I wouldn't do that either. If OP is making a game, keep all the state in the script, not the DOM. Write a `const cells = []` array and put objects in it (or something like that, it's impossible to tell because no [mcve]). Then, when it's time to render the cells, render them based on data from the JS script. – ggorlen Feb 22 '20 at 21:16
  • @ggorlen : if we're talking about some framework, like React, I would more than agree, but I cannot see such tags to have a clue about better approach – Yevhen Horbunkov Feb 22 '20 at 21:18
  • My approach is to have an array storing the state of the game as a logical representation, and to use a DOM based function to render the grid off of that representation. I also need a way to go the other way (which cell was clicked?), hence the idea of attaching row/column indices as data. I'm curious to hear better approaches. – Robin Andrews Feb 22 '20 at 21:19
  • I'm just talking vanilla here. Stringifying an integer array to stick into DOM elements only to have to parse again is an antipattern regardless of framework, but the presence of React would make it even more obvious. Anyway, it's all speculation until OP shows the code--it's not completely inconceivable that this is an appropriate use case, only very unlikely. – ggorlen Feb 22 '20 at 21:19
  • @YevgenGorbunkov That sorted it. Thanks. – Robin Andrews Feb 22 '20 at 21:30
  • You should use event listeners to determine which cell was clicked. When you generate the grid, add a listener to it. The callback will have the x/y coordinates plus any other arbitrary information you need. Much better than stringifying and parsing the data which is slow, error prone and awkward to work with. [Here's an example](https://stackoverflow.com/a/9141943/6243352). – ggorlen Feb 22 '20 at 21:31
  • @RobinAndrews : regarding better approach you may look through my updated answer for a quick clue and the live-demo – Yevhen Horbunkov Feb 22 '20 at 22:13