Im trying to display a bunch of objects in table format. If the object has a child, i'd like it displayed in the next row before continuing down the array to the next object.
In my example below I successfully render a table showing all top level or parent objects:
const renderCategories = (categories) => {
let myCategories = [];
for(let category of categories){
myCategories.push(
<tr key={category.name}>
<td><input type="checkbox"></input></td>
<td>{category.name}</td>
</tr>
);
}
return myCategories;
}
return(
<div>
<table>
<thead>
<tr>
<th><input type="checkbox"></input></th>
<th>Name</th>
</tr>
</thead>
<tbody>
{renderCategories(category.categories)}
</tbody>
</table>
</div>
)
};
In my original version i wasn't using a table but instead using lists and so achieved what i was looking for by doing something like:
const renderCategories = (categories) => {
let myCategories = [];
for(let category of categories){
myCategories.push(
<li key={category.name}>
{category.name}
{category.children.length > 0 ?
{<ul>renderCategories(category.children)}</ul>) : null}
</li>
);
}
return myCategories;
}
This solution obviously wont work for my new use case as I cant render table rows inside table rows. This is driving me crazy as no alternative seems to work. I could achieve my desired design using divs instead of a table. However I want to use a table, or at least know how to do it by using a table.
Anyone able to point me in the right direction? thanks
Use Array.flatMap() and array spread to return a flattened array of <tr> elements.
Note: I've also added the indentation using level and empty <td> elements. You remove the Array.from() line, and the level if not needed.
const renderCategories = (categories, level = 0) =>
categories.flatMap(({ name, children }) => {
const cat = (
<tr key={name}>
<td><input type="checkbox"></input></td>
{Array.from({ length: level }, (_, i) => <td key={i}></td>)}
<td>{name}</td>
</tr>
);
return children.length ?
[cat, ...renderCategories(children, level + 1)]
:
cat;
});
const Demo = ({ categories }) => (
<div>
<table>
<thead>
<tr>
<th><input type="checkbox"></input></th>
<th>Name</th>
</tr>
</thead>
<tbody>
{renderCategories(categories)}
</tbody>
</table>
</div>
);
const categories = [{ name: 'A', children: [{ name: 'A1', children: [] }, { name: 'A2', children: [] }] }, { name: 'B', children: [{ name: 'B1', children: [{ name: 'B1-1', children: [] }, { name: 'B1-2', children: [] }] }, { name: 'B2', children: [] }] }]
ReactDOM.render(
<Demo categories={categories} />,
root
)
th, td {
border: 1px solid black;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Related
I am creating a dynamic table using react based on objects.
The problem is that I am trying to render a new row for every three TD, but instead it is rendering all TDs in the same table row (tr).
How can I separate the TDs into az new TR after a group of 3 TDs have been filled?
Here is my code:
Component rendering table:
`export class Roster extends React.Component{
render(){
return(
<div id="tab">
<table >
<tbody>
<tr>
<td colSpan={3}>
<h4>Goalkeepers</h4>
</td>
</tr>
</tbody>
<Goalies/>
</div>
)
}
}`
Component looping through object:
`class Goalies extends React.Component{
getTable() {
let td_array = [];
let goal_object = {};
Object.keys(goalies).forEach(function(key) {
goal_object = (goalies[key]);
});
for(const checker in goal_object){
td_array.push(<td key={checker}>{goal_object[checker].p_name} <br/> {goal_object[checker].city} <br/> {goal_object[checker].number}</td>);
}
return(
<tr>
{td_array}
</tr>
)
}
render() {
return (<tbody>{this.getTable()}</tbody>)
}
}`
Objects:
export const def = {
1:{
p_name:"p1",
number: 2,
city: "city1"
},
2:{
p_name:"p2",
number: 5,
city: "city2"
},
3:{
p_name:"p3",
number: 3,
city: "city3"
},
4:{
p_name:"p4",
number: 7,
city: "city4"
},
5:{
p_name:"p5",
number: 15,
city: "city5"
},
6:{
p_name:"p6",
number: 21,
city: "city6"
}
}
I want to get:
td1
td2
td3
td4
td5
td6
instead, I am getting:
td1
td2
td3
td4
td5
td6
I tried using conditional statements, pushing into a new array...
In order to achieve the layout you're after, the correct HTML markup would look like this:
<table>
<thead>
<tr>
<th colspan="3">
<h4>Goalkeepers</h4>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>td1</td>
<td>td2</td>
<td>td3</td>
</tr>
<tr>
<td>td4</td>
<td>td5</td>
<td>td6</td>
</tr>
</tbody>
</table>
First, you'll want to group your items into arrays of n (3 in your case). I'd use a generator function:
function* toChunks(arr, n) {
for (let i = 0; i < arr.length; i += n) {
yield arr.slice(i, i + n)
}
}
const rows = [...toChunks(items, 3)]
As an alternative, you could also use:
const chunk = (arr, n) =>
[...Array.from({ length: Math.ceil(arr.length / n) }).keys()].map((key) =>
arr.slice(key * n, (key + 1) * n)
)
const rows = chunk(items, 3)
Given the above rows, your component would look like this:
const Goalies = ({ rows }) => (
<tbody>
{rows.map((row, rk) => (
<tr key={rk}>
{row.map((cell, ck) => (
<td key={ck}>{cell.number}</td>
))}
</tr>
))}
</tbody>
)
Demo here
However, by looking at your data I think you shouldn't be using a <table> here at all. The layout you're after could easier be achieved with:
const Cells = ({ items }) => (
<div className="myWrapper">
{items.map((item, key) => (
<div key={key}>{item.number}</div>
))}
</div>
)
along with any of the following CSS models:
.myWrapper {
display: flex;
flex-wrap: wrap;
}
.myWrapper > * {
width: calc(100%/3)
}
/* or alternatively: */
.myWrapper {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr))
}
I have a Table component which I want to render custom component based on my data (which is an array of objects) also I need my component (I mean Table component) to have two props, one is the data object and another one is an array of objects which each object has two properties: a title and a function that render different component based on the data key. for example if the key in data object is fullName, I need to render a paragraph tag or if key is avatar, I need to return an image tag and so on.. let me show in code:
const Table = () => {
const data= [
{
id: 1,
avatar: 'blah blah blah',
fullName: 'Arlan Pond',
email: 'apond0#nytimes.com',
country: 'Brazil',
registerDate: '1/11/2021',
status: 'active',
},
];
const cols = [
{
title: 'ID',
componentToRender(rowsArr) { //this is how I defined my method.
rowsArr.map((el, index) => {
return <td>{el.id}</td>;
});
},
},
{
title: 'Avatar',
componentToRender(rowsArr) { //this is how I defined my method.
rowsArr.map((el, index) => {
return <td><span>{el.avatar}</span></td>;
});
},
},
];
return (
<div className='table-responsive' style={{width: '95%'}}>
<table className='table table-borderless'>
<thead>
<tr>
//here I need to show my headers...
{cols.map((el, index) => {
return <th key={index}>{el.title}</th>;
})}
</tr>
</thead>
<tbody>
//here I need to fill my table with components.
<tr className='table-row'>
{cols.map((el, index) => {
return el.componentToRender(data);
})}
</tr>
</tbody>
</table>
</div>
);
};
and the problem is table show only headers but data cells are empty. how can I achieve this?
This is one of the objects that you have defined.
{
title: 'ID',
componentToRender(rowsArr) { //this is how I defined my method.
rowsArr.map((el, index) => {
return <td>{el.id}</td>;
});
},
}
if you look closely , you will see that you are not returning anything from the function. The return keyword that you have used, goes back to the map method. So to fix the problem, you will need to change the code like this:
{
title: 'ID',
componentToRender(rowsArr) { //this is how I defined my method.
return rowsArr.map((el, index) => {
return el.id;
});
},
}
and tbody logic needs to change as well:
<tbody>
<tr className='table-row'>
{cols.map((el, index) => {
return <td key={index}>{el.componentToRender(rows)}</td>;
})}
</tr>
</tbody>
I refactored the component rendering. Instead of looping trough data in every component render, I created a data.map inside <tbody>, which creates a <tr> for every data entry (every object inside data array) and then renders the needed component based on the title.
Beware! title in cols should have the same naming case as the one in data. (for example both should be Avatar or avatar)
import "./styles.css";
const Table = () => {
const data= [
{
ID: 1,
Avatar: 'blah blah blah', // Capitalized like the Avatar in cols
fullName: 'Arlan Pond',
email: 'apond0#nytimes.com',
country: 'Brazil',
registerDate: '1/11/2021',
status: 'active',
},
{
ID: 2,
Avatar: 'blah blah blah2',
fullName: 'Arlan Pond',
email: 'apond0#nytimes.com',
country: 'Brazil',
registerDate: '1/11/2021',
status: 'active',
},
];
const cols = [
// refactored the components
{
title: 'ID',
component(content) { return <td>{content}</td> }
},
{
title: 'Avatar',
component(content) { return <td><span>{content}</span></td> }
},
];
return (
<div className='table-responsive' style={{width: '95%'}}>
<table className='table table-borderless'>
<thead>
<tr>
{cols.map((el, index) => {
return <th key={index}>{el.title}</th>;
})}
</tr>
</thead>
<tbody>
{/* completely changed here */}
{data.map((entry, entryindex) => {
return <tr className='table-row'>
{cols.map((el, index) => {
return el.component(data[entryindex][el.title])
})}
</tr>
})}
</tbody>
</table>
</div>
);
};
export default function App() {
return (
<div className="App">
<Table />
</div>
);
}
See it live in Codesandbox
I created the table I mentioned below using React js. When I click on the button below the table, I want to add a new row to the table. I have listed the react code I wrote below. how can I do that?
My React Code
const PPP13 = (props) => {
return (
<Jumbotron>
<p className="btn-group">13- List all owners of 20% or more of the equity of the Applicant</p>
<Table striped bordered hover>
<thead>
<tr>
<th>Owner Name</th>
<th>Title</th>
<th>Ownership %</th>
<th>TIN (EIN, SSN)</th>
<th>Address</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<FormControl aria-label="DDD"/>
</td>
<td>
<FormControl aria-label="DDD"/>
</td>
<td>
<FormControl aria-label="DDD"/>
</td>
<td>
<FormControl aria-label="DDD"/>
</td>
<td>
<FormControl aria-label="DDD"/>
</td>
</tr>
</tbody>
</Table>
<Button className="btn-group" name="add" value="No">
Add more owners
</Button>
</Jumbotron>
)
}
Here is what you can do. Lets say you have a Main component which will get all details.
class Products extends React.Component {
constructor(props) {
super(props);
// this.state.products = [];
this.state = {};
this.state.filterText = "";
this.state.products = [
{
id: 1,
category: 'Sporting Goods',
price: '49.99',
qty: 12,
name: 'football'
}, {
id: 2,
category: 'Sporting Goods',
price: '9.99',
qty: 15,
name: 'baseball'
}, {
id: 3,
category: 'Sporting Goods',
price: '29.99',
qty: 14,
name: 'basketball'
}, {
id: 4,
category: 'Electronics',
price: '99.99',
qty: 34,
name: 'iPod Touch'
}, {
id: 5,
category: 'Electronics',
price: '399.99',
qty: 12,
name: 'iPhone 5'
}, {
id: 6,
category: 'Electronics',
price: '199.99',
qty: 23,
name: 'nexus 7'
}
];
}
handleAddEvent(evt) {
var id = (+ new Date() + Math.floor(Math.random() * 999999)).toString(36);
var product = {
id: id,
name: "empty row",
price: "mpty row",
category: "mpty row",
qty: 0
}
this.state.products.push(product);
this.setState(this.state.products);
}
handleProductTable(evt) {
var item = {
id: evt.target.id,
name: evt.target.name,
value: evt.target.value
};
var products = this.state.products.slice();
var newProducts = products.map(function(product) {
for (var key in product) {
if (key == item.name && product.id == item.id) {
product[key] = item.value;
}
}
return product;
});
this.setState({products:newProducts});
};
render() {
return (
<div>
<ProductTable onProductTableUpdate={this.handleProductTable.bind(this)} onRowAdd={this.handleAddEvent.bind(this)} products={this.state.products} />
</div>
);
}
}
This contains the code for adding row.Then for the table do something like this.
class ProductTable extends React.Component {
render() {
var onProductTableUpdate = this.props.onProductTableUpdate;
var product = this.props.products.map(function(product) {
return (<ProductRow onProductTableUpdate={onProductTableUpdate} product={product} key={product.id}/>)
});
return (
<div>
<button type="button" onClick={this.props.onRowAdd} className="btn btn-success pull-right">Add</button>
<table className="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>price</th>
<th>quantity</th>
<th>category</th>
</tr>
</thead>
<tbody>
{product}
</tbody>
</table>
</div>
);
}
}
Now for the row Comoponent:
class ProductRow extends React.Component {
render() {
return (
<tr className="eachRow">
<td>
{this.props.product.id}
</td>
<td>
{this.props.product.price}
</td>
<td>
{this.props.product.qty}
</td>
<td>
{this.props.product.category}
</td>
</tr>
);
}
}
Working Example:
https://jsfiddle.net/mrAhmedkhan/nvgozjhy/
Ok here's my plan:
First we create a state to hold all the data for the table. I've used an object instead of an array as it's much easier to do the change handling. With arrays you always end up doing all this awkward splicing. You can always parse the object out into an array when you're ready to use it elsewhere.
Then we render out each row of the table by mapping over the entries in our table state. Note we also write the change handler inside the map, meaning we can easily use the rowId (tableData key) to set our new state when a change comes in.
Finally we plop in a button to add more rows. This has a click handler associated with it (handleAddRowClick) which counts the number of rows we have and uses this to generate a new rowId. We use the new rowId to expand the tableData state to include a new defaultRow. I defined defaultRow outside of the function, this prevents it from being redeclared on every render.
import React, { useState } from 'react'
import { Table, Input, Button } from 'reactstrap'
const defautRow = { colA: '', colB: '' }
const IncreasableTable = props => {
const [tableData, setTableData] = useState({
row1: { colA: '', colB: '' }
})
const handleAddRowClick = () => {
const extantRowsCount = Object.keys(tableData).length
setTableData(s => ({
...s,
[`row${extantRowsCount}`]: defautRow
}))
}
return (
<>
<Table>
{
Object.entries(tableData).map(([rowId, data]) => {
const handleChange = ({ target: { name, value } }) => {
setTableData(s => ({
...s,
[rowId]: {
...s[rowId],
[name]: value
}
}))
}
return (
<tr key={rowId}>
<td>
<Input name="colA" value={data.colA} onChange={handleChange}/>
<Input name="colB" value={data.colB} onChange={handleChange}/>
</td>
</tr>
)
})
}
</Table>
<Button onClick={handleAddRowClick}>Click me to add more rows</Button>
</>
)
}
export default IncreasableTable
I'd like to sort table items (alphabetical) by clicking on table header. I've tried to do it myself, but it works really strange, only clicking on second header (priority) works... And when I click on first and third header, it sorts table items in order how they were put in there.
I use orderBy from lodash.
Here is my code, the slice of the full class.
const header = [
{name: "Task Name", id: "taskName"},
{name: "Priority", id: "priority"},
{name: "Done", id: "done"},
];
<TableHead>
<TableRow>
{header.map((el, i) => (
<TableCell key={i}>
<div
style={{
display: 'flex',
alignItems: 'center'
}}
onClick={() => this.props.handleSort(el.id)}
>
{el.name}
{
this.props.columnToSort === el.id
? (this.props.sortDirection === 'asc'
? <UpArrow/>
: <DownArrow/>
)
: null
}
</div>
</TableCell>
))}
<TableCell/>
</TableRow>
</TableHead>
And logics in different class, "connected" by props.
const invertDirection = {
asc: "desc",
desc: "asc",
};
class...
state = {
columnToSort: '',
sortDirection: 'desc',
};
handleSort = (columnName) => {
this.setState({
columnToSort: columnName,
sortDirection:
this.state.columnToSort === columnName
? invertDirection[this.state.sortDirection]
: 'asc',
});
};
props
tableData={orderBy(
this.state.tableData,
this.state.columnToSort,
this.state.sortDirection
)}
handleSort = {this.handleSort}
columnToSort = {this.state.columnToSort}
sortDirection = {this.state.sortDirection}
I know it may be hard to read, because I've got many components, but pasted only things I use to do a sort.
Can you tell me why when clicking on second table header priority, sorting works, and when clicking on other headers it don't?
If you have any better ideas for sorting, please let me know.
I'm hopping i understand your goal here, you are trying to sort the data via a click on the table's headers and toggle it to sort it in ascending or descending manner.
If this is correct i would take a simpler approach.
Sorting by dynamic key
You can create a Th component of your own that will take an onClick prop and an id prop where the id is the name of the object's key.
When the Th is clicked it will invoke the handler and will pass the id (the object's key) to the handler.
This way you can sort on the key that got passed by the child.
Ascending Or Descending
We only have 2 options for sorting - Ascending or Descending. This means we can use a Boolean instead of a string (that will simplify our logic a bit).
So after each click on a given Th we will set a new Boolean object in our state when the key being the id of the Th and we will flip it's value.
This way we can conditionally sort by the given key either in an ascending or descending way.
Here is a small running example:
const data = [
{ name: 'John', age: 32 },
{ name: 'Mike', age: 27 },
{ name: 'Jane', age: 31 },
{ name: 'Criss', age: 25 },
{ name: 'Tom', age: 18 },
]
class Th extends React.Component {
handleClick = () => {
const { onClick, id } = this.props;
onClick(id);
}
render() {
const { value } = this.props;
return (
<th onClick={this.handleClick}>{value}</th>
)
}
}
class App extends React.Component {
state = {
users: data
}
handleSort = (id) => {
this.setState(prev => {
return {
[id]: !prev[id],
users: prev.users.sort((a, b) => prev[id] ? a[id] < b[id] : a[id] > b[id] )
}
});
}
render() {
const { users } = this.state;
return (
<table>
<thead>
<tr>
<Th onClick={this.handleSort} id="name" value="Name" />
<Th onClick={this.handleSort} id="age" value="Age" />
</tr>
</thead>
<tbody>
{
users.map(user => (
<tr>
<td>{user.name}</td>
<td>{user.age}</td>
</tr>
))
}
</tbody>
</table>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I have a table where the row elements are all populated by child components. There is a checkbox in each of these child components. Now I want to get all checked checkboxes at once. I could use prefs emit as two way binding and update an array or object on the parent but I am wondering if there is better way for this.
Here a short example for the template part:
<table>
<thead>
<tr>
<th> Check </th>
<th> Title </th>
</tr>
</thead>
<list-tbody v-for="element in elements" :element="element"> </list-tbody>
</table>
and this is the child component
<tbody>
<tr>
<td>
<input type="checkbox">
</td>
<td> {{element.title}} </td>
</tr>
</tbody>
As mentioned in the comments you could handle this in two ways:
Use Vuex and mutate the elements array from the child component.
Emit an event on each selection click event to the parent and the parent will update the elements array.
You prefer the second because you're not using Vuex and that's OK.
Once you're having the data "linked" between child component and parent you can use a filter method to only show the selected elements.
Please have a look at the demo below or the fiddle from my comment.
const listTbodyVuex = {
props: ['element'],
template: `
<tbody>
<tr>
<td>
<input type="checkbox" #click="selected">
</td>
<td> {{element.title}} </td>
</tr>
</tbody>
`,
methods: {
...Vuex.mapMutations(['changeSelection']),
selected(evt) {
//console.log('clicked', evt.target.checked, this.changeSelection)
// changeSelection mutation could be also called with-out mapping
// this.$store.commit('changeSelection', ...);
this.changeSelection({
id: this.element.id, selected: evt.target.checked
});
}
}
}
const listTbodyEvents = {
props: ['element'],
template: `
<tbody>
<tr>
<td>
<input type="checkbox" #click="selected">
</td>
<td> {{element.title}} </td>
</tr>
</tbody>
`,
methods: {
selected(evt) {
console.log('clicked', evt.target.checked)
this.$emit('selected', {
element: this.element,
newSelection: evt.target.checked
})
}
}
}
const store = new Vuex.Store({
state: {
elements: [
{
id: 0,
title: 'first',
selected: false
},
{
id: 1,
title: 'second',
selected: false
},
{
id: 2,
title: 'third',
selected: false
}
]
},
mutations: {
changeSelection(state, {id, selected}) {
let element = state.elements
.filter((element) => element.id === id)[0];
element.selected = selected;
//console.log('update element', JSON.parse(JSON.stringify(element)));
Vue.set(state.elements, element.id, element);
}
}
})
new Vue({
el: '#app',
store,
data() {
return {
elements: [
{
id: 0,
title: 'first',
selected: false
},
{
id: 1,
title: 'second',
selected: false
},
{
id: 2,
title: 'third',
selected: false
}
]
}
},
computed: {
...Vuex.mapState({
vuexElements: (state) => state.elements
})
},
components: {
listTbodyEvents,
listTbodyVuex
},
methods: {
updateElement(data) {
let element = this.elements
.filter((element) => element.id === data.element.id)[0];
element.selected = data.newSelection;
// console.log('update', element)
},
filterSelected(data) {
// console.log('filter', data.filter((item) => console.log(item.selected)))
return data.filter((item) => item.selected);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.0/vuex.js"></script>
<div id="app">
<h1>Example with vuex</h1>
<table>
<thead>
<tr>
<th> Check </th>
<th> Title </th>
</tr>
</thead>
<list-tbody-vuex v-for="element in elements" :element="element" :key="element.id"> </list-tbody-vuex>
</table>
<pre>only selected: {{filterSelected(vuexElements)}}</pre>
<pre>{{vuexElements}}</pre>
<hr/>
<h1>Example with events</h1>
<table>
<thead>
<tr>
<th> Check </th>
<th> Title </th>
</tr>
</thead>
<list-tbody-events v-for="element in elements" :element="element" :key="element.id" #selected="updateElement"> </list-tbody-events>
</table>
<pre>only selected: {{filterSelected(elements)}}</pre>
<pre>{{elements}}</pre>
</div>
You should really stick to emitting values to maintain separation of your components. That being said, you can do the following if you really wanted to grab the data all at once:
First, you'll need a data attribute in your child component that your checkbox uses with v-model. For the sake of this example, let's just call it checkbox_value. Once you've done that, you can do something like the following in your parent component method:
var checkbox_values = [];
this.$children.forEach(function(child) {
//you can check the child type here if you have other non-checkbox children
checkbox_values.push(child.$data.checkbox_value);
});
Of course, I wouldn't encourage you to do something like this, but you'll have to make that judgement call.
Note: The above will return values only. You could push object key/value pairs instead!