简体   繁体   中英

Editable React table: pass data between components

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 component 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.org/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.org/docs/refs-and-the-dom.html but this seems to work if my tr were to be a component 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 compose 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?

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 recommend

  • to break your table component into smaller pieces (like <Header /> , <Row /> , etc) within common parent <App /> and a separate <EditDialog /> / <DeleteDialog /> components to edit/delete row data) - smaller components are easier to maintain and troubleshoot
  • store your table data (preferably, with unique record id 's) within parent ( <Table /> ) component and pass data entries that correspond to table rows as params to <Row /> components
  • pass abstract onEdit() and onDelete() event handlers as props to <Row /> components 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 components, 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.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.1/axios.min.js"></script><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><div id="root"></div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM