skip to Main Content

I have made an HTML table and I’ve populated using Local JSON data. I’ve made the table such that it has an edit button and I want it such that when I click on the button it makes the input boxes editable on the respective row.

I’ve got it somewhat working but my code makes it such that when I click on the edit button icon for any row, it will only make it that the TOP row is editable and not the current row that I clicked the edit icon on. Here is my code.


function buildTable(data) {
var table =document.getElementById("myTable")

for (let i =0; i < data.length; i++) {

let row = `<tr>
             <td>${[i+1]}</td>
             <td id=customerKey>${data[i].customerKey}</td>
             <td id=currency>${data[i].currency}</td>
             <td id=currency>${data[i].suffix}</td>
             <td><button type="button" onclick="editRow()>
          </tr>`

table.innerHTML += row
}
}

My function for editRow() is

function editRow() {
         document.getElementById("customerKey").contentEditable = true;
         document.getElementById("currency").contentEditable = true;
         document.getElementById("suffix").contentEditable = true;

I am not sure why its only targeting the top row of the table and not the respective row that I’ve clicked the icon on though? Or is there a better way to do this?

3

Answers


  1. For one, ids must be unique – yours are the same on each row and it’s finding the first one. Then, you need to find all the related <td>s and make them contentEditable = true. Also, you don’t have any input boxes. You have table cells that you are making the content editable. You could easily use inputs and initialize them as disabled and then set disabled to false.

    function buildTable(data) {
      var table = document.getElementById("myTable")
    
      for (let i = 0; i < data.length; i++) {
    
        let row = `<tr>
                 <td>${[i+1]}</td>
                 <td class="editable" >${[i+1]}</td>
                 <td class="editable" >${[i+1]}</td>
                 <td><button type="button" onclick="editRow(this);">
              </tr>`
    
        table.innerHTML += row
      }
    }
    
    function editRow(button) {
      button.closest("tr").querySelectorAll("td.editable").forEach(
        function(e) {
          e.contentEditable = true;
        }
      );
    Login or Signup to reply.
  2. You can choose to assign a unique id for each of your table cells / elements:

    function buildTable(data) {
      var table = document.getElementById('myTable');
    
      for (let i = 0; i < data.length; i++) {
        let row = `<tr>
               <td id="id-${i}">${i + 1}</td>
               <td id="customerKey-${i}">${data[i].customerKey}</td>
               <td id="currency-${i}">${data[i].currency}</td>
               <td id="suffix-${i}">${data[i].suffix}</td>
               <td><button type="button" onclick="editRow(${i})">Edit</button></td>
            </tr>`;
    
        table.innerHTML += row;
      }
    }
    function editRow(i) {
      document.getElementById(`customerKey-${i}`).contentEditable = true;
      document.getElementById(`currency-${i}`).contentEditable = true;
      document.getElementById(`suffix-${i}`).contentEditable = true;
    }
    
    buildTable([
      { customerKey: '1', currency: 'USD', suffix: '.exe' },
      { customerKey: '2', currency: 'USD', suffix: '.jpg' },
    ]);
    <table id="myTable" border="1"></table>
    Login or Signup to reply.
  3. The other answers have explained why your current code doesn’t work: ids need to be unique. So a) when you loop over your data you’re adding the same ids to each row, and b) when you come to target those row cells the browser will return the first element that matches your query selector ie only the cells in the first row. You can overcome this, as they suggest, by adding the index to the id for each cells, or by using classes or…

    Data attributes can be very useful here rather than element ids. You can add one identifying the row by index, and one on each cell to identify its type. When you click a button/edit a field you can use closest to find the closest row which in turn will identify which object in the array should be updated/delete etc.

    For example: when you iterate over the data you would add an rowid data attribute to the row:

    <tr data-rowid="${id}">
    

    and a celltype to each cell:

    <td>
      <input
        data-celltype="${key}"
        type="text"
        value="${value}"
      />
    </td>
    

    When you click on a button, for example, you can then use closest to find the closest row, and perform an action on it. Here we’re removing a row from the table when a "delete" button is clicked.

    const parent = e.target.closest('tr');
    parent.remove();
    

    The question remains how you’re going to ensure that the changes to the table are fed back into the dataset. I don’t know if you’ve got this far but I took the liberty of writing and documenting a small example. It deviates a little from your code question but you might find some of the code here useful.

    1. It uses basic state-management principles from Redux – a reducer that returns a new state based on actions passed to it, and a store that returns two function dispatch and getState which can be used to update the state, and get data from it.

    2. The table cells are editable from the outset so no need to click a button to enable editing. The only button is the delete button.

    3. When the delete button is clicked the handler grabs the rowid from the row element’s data attribute, dispatches an action to the store to remove the row object form the data, and then removes the row.

    4. When an input field is changed the handler grabs the rowid, the cell’s celltype data attribute value, and the input value, and dispatches that information to the store which returns a new state where the object identified by the rowid is updated with the new value.

    const initialState=[{id:1,key:"111",currency:"GBP",suffix:"pla"},{id:2,key:"2222",currency:"EUR",suffix:"xyz"},{id:3,key:"333",currency:"USD",suffix:"abc"}];
    
    // The reducer returns a new state dependant on the
    // type of action dispatched to it along with a payload
    // The payload may be a single value (see "delete")
    // or it maybe an object (see "edit") which can be used
    // to update the the state with new values.
    function reducer(state, action) {
    
      // Destructure the type and payload from
      // the action object
      const { type, payload } = action;
    
      switch (type) {
    
        // `filter` out the object the id of which
        // matches the rowid passed in the payload
        case 'delete': {
          return state.filter(obj => {
            return obj.id !== Number(payload);
          });
        }
    
        // `map` over the state and return a new array
        // updating the value of the celltype of the
        // appropriate row
        case 'edit': {
          const { rowid, celltype, value } = payload;
          return state.map(obj => {
            if (obj.id === Number(rowid)) {
              obj[celltype] = value;
              return obj;
            }
            return obj;
          });
        }
    
        default: return state;
    
      }
    
    }
    
    // The store allows us to dispatch and get the
    // current state. It returns an object with the
    // "dispatch" and "getState" functions
    function createStore(reducer, initialState) {
    
      let state = [ ...initialState ];
    
      function dispatch(action) {
        state = reducer(state, action);
      }
    
      function getState() {
        return state;
      }
    
      return { dispatch, getState };
    
    }
    
    // initialise the store
    const { dispatch, getState } = createStore(reducer, initialState);
    
    // Cache the DOM elements
    const button = document.querySelector('.showstate');
    const tbody = document.querySelector('tbody');
    
    // Attach a listener to the "showstate" button,
    // and two listeners to the table body, one to listen
    // for input events, the other to listen for click events
    button.addEventListener('click', showState);
    tbody.addEventListener('input', handleInput);
    tbody.addEventListener('click', handleClick);
    
    // Shows the current state
    function showState() {
      console.log(JSON.stringify(getState()));
    }
    
    // If the tbody listener catches a click event
    // it checks to see if it came from a "delete" button.
    // Then it finds the closest row element, and grabs its
    // rowid from the data attribute. It dispatches an action
    // to the store (which removes that object from the state),
    // and then removes the row from the table
    function handleClick(e) {
      if (e.target.matches('.delete')) {
        const parent = e.target.closest('tr');
        const { rowid } = parent.dataset;
        dispatch({ type: 'delete', payload: rowid });
        parent.remove();
      }
    }
    
    
    // If the tbody listener catches an input event it checks to
    // see if the element matches an input element, grabs the rowid
    // from the row element, and the celltype from the input element.
    // Then it dispatches an action to the store to update the value
    // of the object property where the rowid matches the object id
    function handleInput(e) {
      if (e.target.matches('input')) {
        const { dataset: { rowid } } = e.target.closest('tr');
        const { dataset: { celltype }, value } = e.target;
        dispatch({ type: 'edit', payload: { rowid, celltype, value } });
        e.target.classList.add('edited');
      }
    }
    
    // Creates an input cell
    function createCell(key, value) {
      return `
        <td>
          <input
            data-celltype="${key}"
            type="text"
            value="${value}"
          />
        </td>
      `;
    }
    
    // `map` over the state data. For each object create
    // a row of cell data using its values. As `map` returns an
    // array we need to `join` it up into an HTML string so we can
    // add it to the table body
    const html = getState().map(obj => {
      
      const { id, key, currency, suffix } = obj;
      
      return `
        <tr data-rowid="${id}">
          <td>${id}</td>
          ${createCell('key', key)}
          ${createCell('currency', currency)}
          ${createCell('suffix', suffix)}
          <td>
            <button type="button" class="delete">
              Delete
            </button>
          </td>
        </tr>
      `;
    
    }).join('');
    
    // Add the HTML to the table body
    tbody.innerHTML = html;
    table { border-collapse: collapse; }
    thead { background-color: #efefef; }
    th { text-transform: uppercase; }
    td { padding: 0.25rem; border: 1px solid #dfdfdf; width: fit-content; }
    input { width: 100px; }
    .showstate { margin-top: 0.25rem; }
    .edited { background-color: #aaEEaa; }
    .as-console-wrapper { max-height: 20% !important; }
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>key</th>
          <th>currency</th>
          <th>suffix</th>
        </tr>
      </thead>
      <tbody>
      </tbody>
    </table>
    <button type="button" class="showstate">
      Show state
    </button>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search