最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Editable React table: pass data between components - Stack Overflow

programmeradmin2浏览0评论

Ok, I'm new to React and need help in reading/referencing HTML element data attributes or even the content within an HTML tag - normally with plain JS (and this is what I am doing in my React app) I read like this:

const postRow = document.getElementById(tableRowId);
    ;
const author = postRow.getElementsByTagName('td')[0].getAttribute('data-fieldvalue');
const title = postRow.getElementsByTagName('td')[1].getAttribute('data-fieldvalue');
const description = postRow.getElementsByTagName('td')[2].getAttribute('data-fieldvalue');

I have a functional ponent called Table that I use like this:

        <Table
            headers={this.state.postsHeaders}
            rows={this.state.posts}
            editable={this.state.editable}
            deletable={this.state.deletable}
            onUpdateIconClicked={this.toggleUpdatePostModalView}
            onDeleteIconClicked={this.toggleDeletePostModalView}
        />

where rows property is the data that I am retrieving with axios. My table gets generated find with several tr > td rows. I have an 'edit' CTA for each row that on click I open up a modal where I pass my data to be edited for each row. Onclick of the CTA calls this function that works just fine:

    toggleUpdatePostModalView = (postId, tableRowId) => {
    // toggle the confirm delete post view
    let showUpdatePostModal = !this.state.showUpdatePostModal;
    // when postId and tableRowId are both null that means
    // that the view delete confirm modal must remain not
    // visible (closed) so have to override the toggle
    if (postId === null && tableRowId === null) {
        showUpdatePostModal = false;
    }

    const postRow = document.getElementById(tableRowId);
    ;
    const author = postRow.getElementsByTagName('td')[0].getAttribute('data-fieldvalue');
    const title = postRow.getElementsByTagName('td')[1].getAttribute('data-fieldvalue');
    const description = postRow.getElementsByTagName('td')[2].getAttribute('data-fieldvalue');
    // dont get the elements directly like above .html

    this.setState({
        ...this.state,
        showUpdatePostModal: showUpdatePostModal,
        postToUpdate: {
            postId: postId,
            tableRowId: tableRowId,
            author: author,
            title: title,
            description: description
        }
    });
}

The issue that someone pointed out is that I should NOT use the JS way of getting the data (the getElementById and getElementsByTagName functions because of virtual DOM and real DOM sync issues. So I was pointed toward .html but this seems to work if my tr were to be a ponent itself but as it is, it is just HTML in my Table render function as so:

const table = (props) => {

// the following code creates an array from props.header object
// that is an indexed array (0, 1, ..., n) and each value 
// contains the key properties that pose object props.header,
// and so, even though in .map() the parameter says 'key'
// this is misleading because in reality it is the VALUE 
// (since the key of the array is 0 or 1 or n) but it is  called 
// 'key' because it is the key in the props.headers object that we
// need to get the info for (.map(function(currentValue, index, arr))
const headersArray = Object.keys(props.headers);
const tableHeaders = headersArray.map(key => {
    return <th key={key}>{props.headers[key]}</th>;
});
const editHeader = props.editable === true ? <th key="edit">Edit</th> : null;
const deleteHeader = props.deletable === true ? <th key="delete">Delete</th> : null;

let tableRows = null;
if (props.rows) {

    tableRows = props.rows.map((row, key) => {

        return (
            <tr id={`tr-${key}`} key={key}>
                {/* inner loop to dynamically generate the <td>
                    depending on how many headers there are since
                    each header corresponds to a key or column in
                    the table */}
                {headersArray.map(tdKey => {
                    return <td key={tdKey} data-fieldname={tdKey} data-fieldvalue={row[tdKey]} >{row[tdKey]}</td>
                })}

                {props.editable === true ? <td key="edit"><PencilIcon onClick={() => props.onUpdateIconClicked(row.postId, `tr-${key}`)} /></td> : null}
                {props.deletable === true ? <td className="delete-icon-container" key="delete"><TrashIcon onClick={() => props.onDeleteIconClicked(row.postId, `tr-${key}`)} /></td> : null}
            </tr>
        );

    });

}

return (

    <table className="table is-striped">
        <thead>
            <tr>
                {tableHeaders}
                {editHeader}
                {deleteHeader}
            </tr>
        </thead>

        <tbody>
            {tableRows}
        </tbody>
    </table>

);

}

I also read that these refs should not be used to often - so what if I have a table with 100 rows? 200? I am not sure how to proceed and do this the React way...can anyone help?

Ok, I'm new to React and need help in reading/referencing HTML element data attributes or even the content within an HTML tag - normally with plain JS (and this is what I am doing in my React app) I read like this:

const postRow = document.getElementById(tableRowId);
    ;
const author = postRow.getElementsByTagName('td')[0].getAttribute('data-fieldvalue');
const title = postRow.getElementsByTagName('td')[1].getAttribute('data-fieldvalue');
const description = postRow.getElementsByTagName('td')[2].getAttribute('data-fieldvalue');

I have a functional ponent called Table that I use like this:

        <Table
            headers={this.state.postsHeaders}
            rows={this.state.posts}
            editable={this.state.editable}
            deletable={this.state.deletable}
            onUpdateIconClicked={this.toggleUpdatePostModalView}
            onDeleteIconClicked={this.toggleDeletePostModalView}
        />

where rows property is the data that I am retrieving with axios. My table gets generated find with several tr > td rows. I have an 'edit' CTA for each row that on click I open up a modal where I pass my data to be edited for each row. Onclick of the CTA calls this function that works just fine:

    toggleUpdatePostModalView = (postId, tableRowId) => {
    // toggle the confirm delete post view
    let showUpdatePostModal = !this.state.showUpdatePostModal;
    // when postId and tableRowId are both null that means
    // that the view delete confirm modal must remain not
    // visible (closed) so have to override the toggle
    if (postId === null && tableRowId === null) {
        showUpdatePostModal = false;
    }

    const postRow = document.getElementById(tableRowId);
    ;
    const author = postRow.getElementsByTagName('td')[0].getAttribute('data-fieldvalue');
    const title = postRow.getElementsByTagName('td')[1].getAttribute('data-fieldvalue');
    const description = postRow.getElementsByTagName('td')[2].getAttribute('data-fieldvalue');
    // dont get the elements directly like above https://reactjs/docs/refs-and-the-dom.html

    this.setState({
        ...this.state,
        showUpdatePostModal: showUpdatePostModal,
        postToUpdate: {
            postId: postId,
            tableRowId: tableRowId,
            author: author,
            title: title,
            description: description
        }
    });
}

The issue that someone pointed out is that I should NOT use the JS way of getting the data (the getElementById and getElementsByTagName functions because of virtual DOM and real DOM sync issues. So I was pointed toward https://reactjs/docs/refs-and-the-dom.html but this seems to work if my tr were to be a ponent itself but as it is, it is just HTML in my Table render function as so:

const table = (props) => {

// the following code creates an array from props.header object
// that is an indexed array (0, 1, ..., n) and each value 
// contains the key properties that pose object props.header,
// and so, even though in .map() the parameter says 'key'
// this is misleading because in reality it is the VALUE 
// (since the key of the array is 0 or 1 or n) but it is  called 
// 'key' because it is the key in the props.headers object that we
// need to get the info for (.map(function(currentValue, index, arr))
const headersArray = Object.keys(props.headers);
const tableHeaders = headersArray.map(key => {
    return <th key={key}>{props.headers[key]}</th>;
});
const editHeader = props.editable === true ? <th key="edit">Edit</th> : null;
const deleteHeader = props.deletable === true ? <th key="delete">Delete</th> : null;

let tableRows = null;
if (props.rows) {

    tableRows = props.rows.map((row, key) => {

        return (
            <tr id={`tr-${key}`} key={key}>
                {/* inner loop to dynamically generate the <td>
                    depending on how many headers there are since
                    each header corresponds to a key or column in
                    the table */}
                {headersArray.map(tdKey => {
                    return <td key={tdKey} data-fieldname={tdKey} data-fieldvalue={row[tdKey]} >{row[tdKey]}</td>
                })}

                {props.editable === true ? <td key="edit"><PencilIcon onClick={() => props.onUpdateIconClicked(row.postId, `tr-${key}`)} /></td> : null}
                {props.deletable === true ? <td className="delete-icon-container" key="delete"><TrashIcon onClick={() => props.onDeleteIconClicked(row.postId, `tr-${key}`)} /></td> : null}
            </tr>
        );

    });

}

return (

    <table className="table is-striped">
        <thead>
            <tr>
                {tableHeaders}
                {editHeader}
                {deleteHeader}
            </tr>
        </thead>

        <tbody>
            {tableRows}
        </tbody>
    </table>

);

}

I also read that these refs should not be used to often - so what if I have a table with 100 rows? 200? I am not sure how to proceed and do this the React way...can anyone help?

Share Improve this question edited Feb 27, 2020 at 15:29 Yevhen Horbunkov 15.6k3 gold badges27 silver badges45 bronze badges asked Feb 26, 2020 at 17:22 Pippy LongstockingPippy Longstocking 3036 silver badges12 bronze badges 0
Add a ment  | 

1 Answer 1

Reset to default 5

Refs are NOT an appropriate tool to use here.

Instead, you should do lifting state up (a lot).

In order to do that, I would remend

  • to break your table ponent into smaller pieces (like <Header />, <Row />, etc) within mon parent <App /> and a separate <EditDialog />/<DeleteDialog /> ponents to edit/delete row data) - smaller ponents are easier to maintain and troubleshoot
  • store your table data (preferably, with unique record id's) within parent (<Table />) ponent and pass data entries that correspond to table rows as params to <Row /> ponents
  • pass abstract onEdit() and onDelete() event handlers as props to <Row /> ponents and attach those to onClick() handlers of Edit/Delete buttons
  • bind those child props (onEdit(), onDelete()) to the callbacks within parent that will trigger editing/deleting dialogs
  • upon record editing/deletion update state of the <Table /> accordingly.

Here's the full-blown demo of what's described above (I have used MaterialUI for styling not to overburden the demo with tons of CSS, you may proceed with your custom ponents, hopefully, that doesn't make my example less clear for you):

const { useState } = React,
      { render } = ReactDOM,
      { TableContainer, Table, TableHead, TableBody, TableRow, TableCell, IconButton, Dialog, DialogTitle, DialogContent, DialogContentText, Button, TextField, FormGroup } = MaterialUI
      
const srcData = [{id:0, author: 'Author1', title: 'Post 1', description: 'Some description'},{id:1, author: 'Author2', title: 'Post 2', description: 'Some other description'},{id:2, author: 'Author3', title: 'Post 3', description: 'Something else'}],
      dataFields = [{id: 0, title: 'Author', key: 'author'},{id: 1, title: 'Title', key: 'title'},{id:2, title: 'Description', key: 'description'}]

const EditButton = ({handleClick}) => (
  <IconButton onClick={handleClick} >
    <i className="material-icons">create</i>
  </IconButton>
)

const DeleteButton = ({handleClick}) => (
  <IconButton onClick={handleClick} >
    <i className="material-icons">delete</i>
  </IconButton>
)
      
const DeleteDialog = ({isOpen, onDialogClose, onConfirmDelete, recordId}) => (
  <Dialog open={isOpen} onClose={onDialogClose} >
    <DialogTitle>Delete record</DialogTitle>
    <DialogContent>
      <DialogContentText>Are you sure you want to delete this record?</DialogContentText>
      <FormGroup>
        <Button onClick={() => onConfirmDelete(recordId)}>Yes</Button>
        <Button onClick={onDialogClose}>No</Button>
      </FormGroup>
    </DialogContent>
  </Dialog>
)

const EditDialog = ({isOpen, onDialogClose, onSubmitEdit, recordData, fields}) => {
  const [data, setData] = useState(),
        handleEdit = (key,value) => setData({...data, [key]:value})
  return (
    <Dialog open={isOpen} onClose={onDialogClose} >
      <DialogTitle>Edit record</DialogTitle>
      <DialogContent>
        <FormGroup>
          {
            fields.map(({key,title}) => (
              <TextField
                key={key}
                defaultValue={recordData[key]}
                label={title}
                onChange={({target:{value}}) => handleEdit(key,value)}
              />
            ))
          }
        </FormGroup>
        <FormGroup>
          <Button onClick={() => onSubmitEdit({...recordData,...data})}>Submit</Button>
          <Button onClick={() => onDialogClose()}>Cancel</Button>
        </FormGroup>
      </DialogContent>
    </Dialog>
  )
}

const Header = ({columnTitles}) => (
  <TableHead>
    <TableRow>
      {columnTitles.map(({title,id}) => <TableCell key={id}>{title}</TableCell>)}
      <TableCell>Action</TableCell>
    </TableRow>
  </TableHead>
)

const Row = ({rowData, columns, onEdit, onDelete}) => (
  <TableRow>
    {columns.map(({key}, i) => <TableCell key={i}>{rowData[key]}</TableCell>)}
    <TableCell>
      <EditButton handleClick={() => onEdit(rowData.id)} />
      <DeleteButton handleClick={() => onDelete(rowData.id)} />
    </TableCell>
  </TableRow>
)

const App = ({data,fields}) => {
  const [tableData, setTableData] = useState(data),
        [dataFields, setDataFields] = useState(fields),
        [deleteDialogOn, setDeleteDialogOn] = useState(false),
        [editDialogOn, setEditDialogOn] = useState(false),
        [recordIdToDelete, setRecordIdToDelete] = useState(),
        [recordIdToEdit, setRecordIdToEdit] = useState(),
        onEditDialogOpen = (id) => (setRecordIdToEdit(id),setEditDialogOn(true)),
        onDeleteDialogOpen = (id) => (setRecordIdToDelete(id), setDeleteDialogOn(true)),
        handleEdit = (data) => {
          setEditDialogOn(false)
          const tableDataCopy = [...tableData],
                editedItemIdx = tableDataCopy.findIndex(({id}) => id == data.id)
                tableDataCopy.splice(editedItemIdx,1,data)
          setTableData(tableDataCopy)
        },
        handleDelete = (idRecordToDelete) => {
          setDeleteDialogOn(false)
          const tableDataCopy = [...tableData]
          setTableData(tableDataCopy.filter(({id}) => id!=recordIdToDelete))
        }
  return (
    <div>
    <DeleteDialog 
      isOpen={deleteDialogOn} 
      onDialogClose={() => setDeleteDialogOn(false)}
      onConfirmDelete={handleDelete}
      recordId={recordIdToDelete}
    />
    <EditDialog 
      isOpen={editDialogOn} 
      onDialogClose={() => setEditDialogOn(false)}
      onSubmitEdit={handleEdit}
      recordData={tableData.find(({id}) => id==recordIdToEdit)||{}}
      fields={dataFields}
    />
    <TableContainer>
    <Table>
        <Header columnTitles={dataFields} /> 
      <TableBody>
        {
          tableData.map(data => (
            <Row 
              key={data.id} 
              rowData={data}
              columns={dataFields}
              onEdit={onEditDialogOpen}
              onDelete={onDeleteDialogOpen}
            />
          ))
        }
      </TableBody>
    </Table>
    </TableContainer>
    </div>
  )
}

render (
  <App data={srcData} fields={dataFields} />,
  document.getElementById('root')
)
<script src="https://cdnjs.cloudflare./ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://unpkg./@material-ui/core@latest/umd/material-ui.development.js"></script><script src="https://cdnjs.cloudflare./ajax/libs/axios/0.19.1/axios.min.js"></script><link href="https://fonts.googleapis./icon?family=Material+Icons" rel="stylesheet"><div id="root"></div>

发布评论

评论列表(0)

  1. 暂无评论