I have a table that gets generated by a fetched map and builds a row for each payer within this map.
Each row contains payer name, two input fields, and a button with an onClick event handler.
On submit, the system shall take
payer id,
transaction id (gathered from props),
payer value (a custom field not related to payer object)
payer notes (again, a custom field not related to payer object)
and POST this to an endpoint.
I am looking for a way to pass the correct parameters to the onClick handler - when there are an indefinite amount of inputs generated - as my current solution only registers the last field in the map.
You will see that I use some references within the input elements - I was experimenting with them to see if I can achieve some sort of result, however, I had no luck - and, yes, they were declared in the constructor.
constructor
constructor(props) {
super(props);
this.state = {
payers: []
}
this.closeModal = this.closeModal.bind(this);
this.confirmTransaction = this.confirmTransaction.bind(this);
}
Relevant render()
render() {
const tableHeader = (
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td>Notes</td>
<td></td>
</tr>
</thead>
)
const payersList = this.state.payers.map(payer => {
return (
<React.Fragment>
<tr>
<td className="font-italic" key={payer.id}>{payer.name}</td>
<td>
<input className="form-control form-control-sm" type="text" placeholder={this.props.transaction.debit} ref={value => this.payerValue = value} />
</td>
<td>
<input className="form-control form-control-sm" type="text" ref={notes => this.payerNotes = notes} />
</td>
<td>
<button onClick={() => this.confirmTransaction(this.props.transaction.id, payer.name, this.payerValue.value, this.payerNotes.value)}>OK</button>
</td>
</tr>
</React.Fragment>
)
});
Confirm transaction handler
confirmTransaction(id, name, value, notes) {
alert(`This will set ${name} as a payer for transaction ${id} for the value of ${value} with the notes ${notes}`)
}
Example output (missing ${value} and ${notes})
well i would split this into 2 components and 1 container.
ListContainer : will hold you logic and render list
<ListComponent payers={[..payers]} onClick={this.setPayer}/> : this will render rowComponent
<RowComponent payer={{id:1}} onClick={props.onClick} /> : this will render a row.
now props.onClick is a function on ListContainer and inside RowComponent you should call this function with the object you have.
example:
https://codesandbox.io/s/peaceful-northcutt-jn769?fontsize=14
good luck :)
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function Row({ payer, onClick }) {
const [value, onChange] = useState(payer.value);
// dont forget to update value if parent changed for somereason
useEffect(() => {
if (payer.value !== value) {
onChange(payer.value);
}
}, [payer]);
return (
<li>
<label>{payer.name}</label>
<input value={value} onChange={e => onChange(e.target.value)} />
<button onClick={e => onClick({ ...payer, value })}>save</button>
</li>
);
}
function ListComponent({ payers, onClick }) {
return (
<ul>
{payers.map(payer => (
<Row key={payer.id} payer={payer} onClick={onClick} />
))}
</ul>
);
}
function ListContainer() {
const [payers, addPayer] = useState([
{ id: 1, name: "a", value: 1 },
{ id: 2, name: "b", value: 2 }
]);
function setPayer(payer) {
alert("payer is " + JSON.stringify(payer));
addPayer(payers.filter(i => i.id !== payer.id).concat(payer));
}
return (
<div className="App">
<ListComponent onClick={setPayer} payers={payers} />
<pre>{JSON.stringify(payers, null, 2)}</pre>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<ListContainer />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Related
I am trying to learn React and Typescript. I am building a little demo app that sends a request to the api https://api.zippopotam.us/us to get postcode information and display the place information. If the request is submitted with an invalid postcode I want to display an error message.
Below is my code. I have the call to the api inside the useEffect method and notice that this runs twice when the app is loaded i.e. before the user has entered a zipcode and clicked the search button so the api call is made without the zipcode and hence returns a 404, so the error code is always displayed on initial load.
I thought that the useEffect method should only get run when the zipSearch value changes i.e. when a user enters a zip and clicks enter. Although reading up on the useEffect method is seems it runs everytime the app component renders. Im also a little confused why it runs twice on initial load.
How can I get this to work the way I want it to? Any help would be much appreciated. If a moderator deletes this question, can they please let me know why? Thanks.
import React, {FormEvent, useEffect, useState} from "react";
import { useForm } from 'react-hook-form'
import "./App.css";
import axios from "axios";
import { IPlace } from "./IPlace";
export default function App2(){
const [placeFound, setPlaceFound] = useState<IPlace[]>([]);
const [zipSearch, setZipSearch] = useState("");
const [errorFound, setErrorFound] = React.useState("");
const renderPlaces = () => {
console.log("Render places runs")
if(placeFound.length !== 0){
return (<div className="table-container">
<table>
<thead>
<tr>
<th><span>State</span></th>
<th><span>Longitude</span></th>
<th><span>Latitude</span></th>
<th><span>Place Name</span></th>
</tr>
</thead>
{placeFound.map((place) =>{
return (
<tbody>
<tr>
<td>{place.state}</td>
<td>{place.longitude}</td>
<td>{place.latitude}</td>
<td>{place["place name"]}</td>
</tr>
</tbody>
)})}
</table>
</div>)
}
}
React.useEffect(() => {
console.log("useEffect is run")
const query = encodeURIComponent(zipSearch);
axios
.get(`https://api.zippopotam.us/us/${query}`,{
})
.then((response) => {
setPlaceFound(response.data.places);
setErrorFound("");
})
.catch((ex) => {
let errorFound = axios.isCancel(ex)
? 'Request Cancelled'
: ex.code === 'ECONNABORTED'
? 'A timeout has occurred'
: ex.response.status === 404
? 'Resource Not Found'
: 'An unexpected error has occurred';
setErrorFound(ex.code);
setPlaceFound([]);
});
}
},[zipSearch]);
const search=(event: FormEvent<HTMLFormElement>) =>{
console.log("Search method runs")
event.preventDefault();
const form = event.target as HTMLFormElement;
const input = form.querySelector('#zipSearchInput') as HTMLInputElement;
setZipSearch(input.value);
}
return (
<div className="App">
<div className="search-container">
<h1>Place Search using Zip Code</h1>
<form className="searchForm" onSubmit={event => search(event)}>
<div>
<label htmlFor="zipSearchInput">Zip Code</label>
<input {...register('zipSearchInput', { required: true, minLength: 5, maxLength: 5 }) }
id="zipSearchInput"
name="zipSearchInput"
type="text"
/>
</div>
{
errors.zipSearchInput && <div className="error">Zip Code is required and must be 5 digits long</div>
}
<button type="submit">Search</button>
</form>
</div>
{placeFound.length !== 0 && renderPlaces()}
{errorFound !== "" && <p className="error">{errorFound}</p>}
</div>)
}
What you should probably do is actually use the library above react-hook-form and its functions.
Then I believe that the useEffect that you are using is pretty useless in this scenario, You are going the long way for a simpler task. You can simply call the api on submit of the form and get rid of the state zipSearch the react-hook-form is taking care of that for you.
Here's the fixed version of the code below:
import React, { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import axios from "axios";
interface IPlace {
state: string;
longitude: string;
latitude: string;
"place name": string;
}
export default function App2() {
const [placeFound, setPlaceFound] = useState<IPlace[]>([]);
const [errorFound, setErrorFound] = React.useState("");
const formMethods = useForm<{ zipSearchInput: string }>();
const renderPlaces = () => {
console.log("Render places runs");
if (placeFound.length !== 0) {
return (
<div className="table-container">
<table>
<thead>
<tr>
<th>
<span>State</span>
</th>
<th>
<span>Longitude</span>
</th>
<th>
<span>Latitude</span>
</th>
<th>
<span>Place Name</span>
</th>
</tr>
</thead>
{placeFound.map((place) => {
console.log(place);
return (
<tbody key={place.latitude}>
<tr>
<td>{place.state}</td>
<td>{place.longitude}</td>
<td>{place.latitude}</td>
<td>{place["place name"]}</td>
</tr>
</tbody>
);
})}
</table>
</div>
);
}
};
const search = (values: { zipSearchInput: string }) => {
console.log(values);
const query = encodeURIComponent(values.zipSearchInput);
axios
.get(`https://api.zippopotam.us/us/${query}`, {})
.then((response) => {
setPlaceFound(response.data.places);
setErrorFound("");
})
.catch((ex) => {
let _errorFound = axios.isCancel(ex)
? "Request Cancelled"
: ex.code === "ECONNABORTED"
? "A timeout has occurred"
: ex.response.status === 404
? "Resource Not Found"
: "An unexpected error has occurred";
setErrorFound(_errorFound);
setPlaceFound([]);
});
};
return (
<div className="App">
<div className="search-container">
<h1>Place Search using Zip Code</h1>
<FormProvider {...formMethods}>
<form className="searchForm" onSubmit={formMethods.handleSubmit(search)}>
<div>
<label htmlFor="zipSearchInput">Zip Code</label>
<input
{...formMethods.register("zipSearchInput", {
required: true,
minLength: 5,
maxLength: 5
})}
id="zipSearchInput"
name="zipSearchInput"
type="text"
/>
</div>
<button type="submit">Search</button>
</form>
</FormProvider>
</div>
{placeFound.length !== 0 && renderPlaces()}
{errorFound !== "" && <p className="error">{errorFound}</p>}
</div>
);
}
Good Luck
I carry out a project which can modify the price of a product (recovered from a fake API) and then at the click of a button carries out the update by calculating the VAT of 20%. I encounter a problem I would like to have a price state and that in this state it's the value of my input namely {listProduct.price} but it doesn't work.
If you have solutions, I am interested, thank you in advance. (sorry I'm new to React I still have a bit of trouble with all these concepts)
import React, { Component } from 'react'
import '../css/ProductsDetails.css'
import {AiOutlineArrowLeft} from "react-icons/ai";
import {Link} from 'react-router-dom'
export default class ProductsDetails extends Component {
state = {
id: this.props.match.params.id,
price:
}
updatePrice = (e) => {
console.log(e);
this.setState({
price: e.target.value
})
}
render() {
const {location: {state: {listProduct}}} = this.props;
return (
<div className="products__details">
<Link to="/"><AiOutlineArrowLeft className="nav__arrow" /></Link>
<h1 className="details__title">{listProduct.title}</h1>
<div className="details__align--desk">
<div className="details__img">
<img className="product__img" src={listProduct.image} alt="Affichage du produit"/>
</div>
<div className="products__align--desk">
<h2 className="product__title">Description</h2>
<p className="product__description">{listProduct.description}</p>
<h2 className="product__title">Price</h2>
<form className="form__price">
<input className="input__price" type="text" value={listProduct.price} onChange={this.updatePrice} />
<p>Price (including VAT): {Math.round((listProduct.price + listProduct.price * 0.2)*100) /100} €</p>
<br/>
<input className="btn__update" type="submit" value="Update product" />
</form>
</div>
<div className="category__align--desk">
<h2 className="product__title">Category</h2>
<p className="product__category">{listProduct.category}</p>
</div>
</div>
</div>
)
}
}
export default class Products extends Component {
constructor(props) {
super(props);
this.state = {productsData: []};
}
componentDidMount = () => {
axios.get('https://fakestoreapi.com/products?limit=7')
.then(res => {
console.log(res.data)
this.setState ({
productsData: res.data
})
})
}
render() {
const listsProducts = this.state.productsData.map(listProduct => {
return <tbody className="products__body">
<tr>
<td> <Link to={{pathname: "/products-details/" + listProduct.id,state: {listProduct}}}>{listProduct.title}</Link></td>
<td className="products__category">{listProduct.category}</td>
<td>{listProduct.price}</td>
<td>{Math.round((listProduct.price + listProduct.price * 0.2)*100) /100}</td>
</tr>
</tbody>
})
return (
<main className="products">
<h1 className="products__title">Products management</h1>
<table cellSpacing="0">
<thead className="products__head">
<tr>
<th className="table--title">Product name</th>
<th className="table--title">Category</th>
<th className="table--title">Price</th>
<th className="table--title">Price (including VAT)</th>
</tr>
</thead>
{listsProducts}
</table>
</main>
)
}
}
Inside a react component:
1 - You declare the initial state of your component, which is, in this case, the price that the product has before the user writes something. For now, we'll set it to 0:
state = {
id: this.props.match.params.id,
price: this.props.listProduct.price ? this.props.listProduct.price : 0
}
2 - Then, in the render method, we access the price value from this.state
3 - Finally, we modify our input element so that it gets the value of the price.
<input className="input__price" type="text" value={price} onChange={this.updatePrice} />
The rest of the component was working well.
This is the result:
import React, { Component } from 'react'
import '../css/ProductsDetails.css'
import {AiOutlineArrowLeft} from "react-icons/ai";
import {Link} from 'react-router-dom'
export default class ProductsDetails extends Component {
state = {
id: this.props.match.params.id,
price: '0'
}
updatePrice = (e) => {
console.log(e);
this.setState({
price: e.target.value
})
}
render() {
const {price} = this.state
return (
<div className="products__details">
<Link to="/"><AiOutlineArrowLeft className="nav__arrow" /></Link>
<h1 className="details__title">{listProduct.title}</h1>
<div className="details__align--desk">
<div className="details__img">
<img className="product__img" src={listProduct.image} alt="Affichage du produit"/>
</div>
<div className="products__align--desk">
<h2 className="product__title">Description</h2>
<p className="product__description">{listProduct.description}</p>
<h2 className="product__title">Price</h2>
<form className="form__price">
<input className="input__price" type="text" value={price} onChange={this.updatePrice} />
<p>Price (including VAT): {Math.round((listProduct.price + listProduct.price * 0.2)*100) /100} €</p>
<br/>
<input className="btn__update" type="submit" value="Update product" />
</form>
</div>
<div className="category__align--desk">
<h2 className="product__title">Category</h2>
<p className="product__category">{listProduct.category}</p>
</div>
</div>
</div>
)
}
}
Start off with the price at 0 (not in quotes) in state, and then...
const price = this.state.price || (this.props.listProduct ? this.props.listProduct.price : 0)
<input className="input__price" type="text" value={price} onChange{this.updatePrice} />
So if the state value has been updated, that will be used, if not it will check if the price is available in props and use that, and if not it will display zero.
I have a react component called App that contains 2 components: Form and Table. And both of them are controlled component.
In the form, there is an input element and a button.
The input element has an onChange attribute; whenever the value changes it changes the value in the App's state.
The button has an onClick attribute that is provided by the App component; Whenever the button is clicked, the state's firstNames (which is an array) will be added with the state value of firstname.
The problem is when I clicked the button, it will throw an error saying that I didn't pass in an array and that it doesn't have a map method, even though in the call back, the updated state does show an array.
Below is the code:
function Form(props) {
return (
<form>
<label>
Item: <input value={props.value} onChange={props.handleChange} />
</label>
<button onClick={props.handleClick}>Submit</button>
</form>
);
}
function Table(props) {
let firstNames = props.names.map((item, index) => {
return (
<tr key={index}>
<td>{item}</td>
</tr>
);
});
return (
<table>
<thead>
<tr>
<th>First Name</th>
</tr>
</thead>
<tbody>{firstNames}</tbody>
</table>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
inputField: "",
firstNames: ["Joey", "Chloe"],
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(event) {
this.setState({ inputField: event.target.value });
}
handleClick() {
this.setState(
{
firstNames: this.state.firstNames.push(this.state.inputField),
},
console.log(this.state.firstNames)
);
}
render() {
return (
<div>
<Form
value={this.state.inputField}
handleChange={this.handleChange}
handleClick={this.handleClick}
/>
<Table names={this.state.firstNames} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
push mutates the original array, but it returns the length of the updated array, therefor, after the initial push, your firstNames inside state will be a number, which doesn't have map
You shouldn't mutate state variables, you should create a new array instead when adding a new name, for example, like this:
this.setState({
firstNames: [...this.state.firstNames, this.state.inputField]
})
The full sample code would then look something like this:
function Form(props) {
return (
<form onSubmit={props.handleClick}>
<label>
Item: <input value={props.value} onChange={props.handleChange} />
</label>
<button>Submit</button>
</form>
);
}
function Table(props) {
let firstNames = props.names.map((item, index) => {
return (
<tr key={index}>
<td>{item}</td>
</tr>
);
});
return (
<table>
<thead>
<tr>
<th>First Name</th>
</tr>
</thead>
<tbody>{firstNames}</tbody>
</table>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
inputField: "",
firstNames: ["Joey", "Chloe"],
};
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleChange(event) {
this.setState({ inputField: event.target.value });
}
handleClick( e ) {
event.preventDefault();
this.setState(
{
firstNames: [...this.state.firstNames, this.state.inputField],
inputField: ''
}, () => console.log(this.state.firstNames) );
}
render() {
return (
<div>
<Form
value={this.state.inputField}
handleChange={this.handleChange}
handleClick={this.handleClick}
/>
<Table names={this.state.firstNames} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Following 2 things I still updated in your code:
Added the type="button" so that a submit doesn't happen
<button type="button" onClick={props.handleClick}>Submit</button>
Changed the callback of the setState which you used for logging
this.setState({
firstNames: [...this.state.firstNames, this.state.inputField],
inputField: ''
}, () => console.log(this.state.firstNames) );
When you did it in your original way, the console.log would be trigger before you could be sure that the setState call has actually happened.
Another note perhaps could be that using index for keys can lead to problems afterwards (be it sorting or removing items), a key should be unique. In this code, it's merely a hint
I have A react component which renders a list of items that have been called from an API and set to setOfAllBooks state. Any time I search for the item, setOfAllBooks state is filtered through by the search ternm and the results are held in searchedBooks state. The results of searchedBooks are then passed to Table component and rendered in a list. At this point it works correctly, but when I search for another item it gets clustered in the Table. What I want to do is anytime I search a new Item after I have searched for a previos term I want the list-items in the Table component to be cleared to make way for the new items that have been searched.
import React, { Component } from 'react';
import './Home.css'
import axios from 'axios';
import Autosuggest from 'react-autosuggest';
var books = []
const getSuggestions = value => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
return inputLength === 0 ? [] : books.filter(book =>
book.title.toLowerCase().slice(0, inputLength) === inputValue);
};
const getSuggestionValue = suggestion => suggestion.title;
const renderSuggestion = suggestion => (
<div>
{suggestion.title}
</div>
);
const Table = ({ data }) => (
<table class="table table-hover">
<thead>
<tr class="table-primary">
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">ISBN</th>
<th scope="col">No. Of Copies</th>
</tr>
</thead>
<tbody>
{data.map(row =>
<TableRow row={row} />
)}
</tbody>
</table>
)
const TableRow = ({ row }) => (
<tr class="table-light">
<th scope="row" key={row.title}>{row.title}</th>
<td key={row.author}>{row.author}</td>
<td key={row.isbn}>{row.isbn}</td>
<td key={row.isbn}>24</td>
</tr>
)
class Home extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
suggestions: [],
setOfAllBooks: [],
searchedBooks: []
};
this.searchBook = this.searchBook.bind(this);
}
componentDidMount(){
axios.get('/api/book/viewAll')
.then(res => {
this.setState({ setOfAllBooks: res.data });
books = this.state.setOfAllBooks;
console.log(this.state.setOfAllBooks)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value)
});
};
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
}
searchBook(event){
event.preventDefault();
this.setState({value: this.state.value});
this.state.searchedBooks = this.state.setOfAllBooks.filter(book => book.title == this.state.value);
this.setState({searchedBook: []})
console.log(this.state.searchedBook);
}
render() {
const { value, suggestions } = this.state;
const inputProps = {
placeholder: 'Enter the name of the book',
value,
onChange: this.onChange
}
return (
<div class="form-group col-lg-4">
<label for="exampleInputEmail1">Email address</label>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
id="searchFor"
/>
<div className=" form-group">
<label htmlFor="searchFor"> </label>
<button class="form-control btn btn-success" type="submit" onClick={this.searchBook}>Search</button>
</div>
<Table data={this.state.searchedBooks} />
</div>
)
}
}
export default Home;
The results
The Error
You need to add the key prop to the TableRow component as <TableRow key={row.title} row={row} />. Remove the key where you have right now.
.... A good rule of thumb is that elements inside the map() call need keys.
... keys used within arrays should be unique among their siblings. . Doc.
So, it seems title what you used for key will still throw warnings, as they are not uniqe. If you have ID attribute in the row object use that. Adding key to TableRow will remove the first warning, but other warning still be there until title doesn't have the uniq values across all the data.
I have a component called Cells which renders with data that is gotten from a flux store. My problem is that I want to render this data to a specific row but because of the way I am rendering the rows (they are dynamic so you can add them to the table), I am struggling to give the rows identifiers which the cell component can render into. I hope that makes sense!
Here is the code:
Cells Component:
import React from 'react';
export default class Cells extends React.Component {
render() {
return (
<td>{this.props.value}</td>
);
}
}
Table Component:
import React from 'react';
import TableHeader from './TableHeader.jsx';
import Cells from './Cells.jsx';
import RowForm from './RowForm.jsx';
import {createRow} from '../../actions/DALIActions';
import AppStore from '../../stores/AppStore';
export default class Table extends React.Component {
state = {rows: [], cellValues: [], isNew: false, isEditing: false};
updateState = () => this.setState({cellValues: AppStore.getCellValues()});
componentWillMount() {
AppStore.addChangeListener(this.updateState);
}
handleAddRowClickEvent = () => {
let rows = this.state.rows;
rows.push({isNew: true});
this.setState({rows: rows});
};
handleEdit = (row) => {
this.setState({isEditing: true});
};
editStop = () => {
this.setState({isEditing: false});
};
handleSubmit = (access_token, id, dataEntriesArray) => {
createRow(access_token, id, dataEntriesArray);
};
componentWillUnmount() {
AppStore.removeChangeListener(this.updateState);
}
render() {
let {rows, cellValues, isNew, isEditing} = this.state;
let headerArray = AppStore.getTable().columns;
let cells = this.state.cellValues.map((value, index) => {
return (
<Cells key={index} value={value.contents} />
);
});
return (
<div>
<div className="row" id="table-row">
<table className="table table-striped">
<thead>
<TableHeader />
</thead>
<tbody>
//////////// This is where the render of the data would happen/////////////
{rows.map((row, index) => this.state.isEditing ?
<RowForm formKey={index} key={index} editStop={this.editStop} handleSubmit={this.handleSubmit} /> :
<tr key={index}>
{this.state.cellValues ? cells : null}
<td>
<button className="btn btn-primary" onClick={this.handleEdit.bind(this, row)}><i className="fa fa-pencil"></i>Edit</button>
</td>
</tr>
)}
///////////////End/////////////////
</tbody>
</table>
</div>
<div className="row">
<div className="col-xs-12 de-button">
<button type="button" className="btn btn-success" onClick={this.handleAddRowClickEvent}>Add Row</button>
</div>
</div>
</div>
);
}
}
I know this isn't probably the best way to achieve what I want (any tips on that would be appreciated as well), but its what I have to work with at the moment!
Any help would be much appreciated, especially examples!
Thanks for you time!
Instead of having one object (rows) that contains row headers, and another object that contains all cells for all rows (cellvalues), I would advise you to put the cell data inside the individual row data in some way, so that your data structure would look something like this:
rows = [
{ rowID: 100, cells: [
{ cellID: 101, value: 'data' },
{ cellID: 102, value: 'data' }
]
},
{ rowID: 200, cells: [
{ cellID: 201, value: 'data' },
{ cellID: 202, value: 'data' }
]
}
]
That way, you pass the cellValues per row, and that allows you to have different cells per row.
Make a separate component for <Row>, which renders:
return
<tr>
{this.props.cells.map( cell => {
return <Cell key={cell.cellID} value={cell.value} />
})}
</tr>
And change the <tr> bit inside your main render to:
<Row key={row.keyID} cells={row.cells}/>
Finally: it is a bad idea to use the index for keys, as in key={i}. Use a key that uniquely identifies the content of the cell/ row.
UPDATE: A typical situation is that cells or rows are first created in front-end, and only get their database ID after posting to database.
Options to still get unique IDs for the keys are:
upon creation of the cell or row in react, give the cell/row a unique ID in the form of a timestamp. After getting back the response from the database, replace the timestamp with the official database key. (this is when you do optimistic updates: show new rows temporarily, until the database gives you the official rows).
do not display the row/cell, until you get response back from server with official IDs
create a unique key by hashing the content of all cell/row contents (if you are pretty sure that cell/row contents are not likely to be identical)
Hope this helps.
You need to pass row to cells render method :
{rows.map((row, index) => this.state.isEditing ?
<RowForm formKey={index} key={index} editStop={this.editStop} handleSubmit={this.handleSubmit} /> :
<tr key={index}>
{(row.cellValues || []).map((value, index) => {
return (
<Cells key={index} value={value.contents} />
)})
}
<td>
<button className="btn btn-primary" onClick={this.handleEdit.bind(this, row)}><i className="fa fa-pencil"></i>Edit</button>
</td>
</tr>
)}
Hope this help!