React Complex Form Editing key value pair - javascript

I am building a form to edit a key/value pair object, see picture:
This is the object that I get from the backend that I stored in the container component.
items: [{
name: "Product Name",
sku: "00001",
attributes: {
width: "value",
height: "value",
length: "value",
color: "value"
}
},
{
name: "Product Name 2",
sku: "00002",
attributes: {
description: "Hello World",
weight: "250",
quantity: "500",
hello: "World"
}
}]
I then pass the data into a child component via props. This is the code for the form:
class EditForm extends Component {
render() {
let editForm = this.props.items && this.props.items.map((item, idx) => {
return(
<tr key={item.sku}>
<td className="edit-table">
<input
value={item.name}
name={item.name}
onChange={(e)=> this.props.onChange(e, idx)}
/>
</td>
<td className="edit-table">
<ul className="item-attributes">
{Object.keys(item.attributes).map((key) => {
return (<li key={key}>
<label>{key}</label>
<input
value={item.attributes[key]}
onChange={(e) => this.props.onChange(e, idx) }
/>
</li>)
})}
</ul>
</td>
</tr>
)
})
return(
<table className="editcontainer-table">
<thead>
<tr>
<th>SKU</th>
<th>Attributes</th>
</tr>
</thead>
<tbody>
{editForm}
</tbody>
</table>
);
}
}
Now, this is where I'm stuck, I'm trying to figure out how the onChange function would work for me to edit the objects in the state and send back to the server for updating.

Give the item name input a name="name" attribute
Give the attribute inputs names per the attribute: name={key}
You can now identify what's being edited in onChange - make the appropriate state change based on the index and input name being changed.
e.g. (using import update from 'immutability-helper') onChange might look like this:
onChange = (e, index) => {
let {name, value} = e.target
let updateSpec
if (name === 'name') {
updateSpec = {
[index]: {
name: {$set: value}
}
}
}
else {
updateSpec = {
[index]: {
attributes: {
[name]: {$set: value}
}
}
}
}
this.setState({items: update(this.state.items, updateSpec)})
}
Here's an example app which shows this solution in action:
Live version
Full example source

I would change the onChange function in your Container to be the following:
onChange = (e, sku, field) => {
const updatedItems = this.state.items.map(e => {
if (e.sku !== sku) return e;
if (field === 'name') {
e.name = e.target.value;
} else {
// has to be attributes
e.attributes[field] = e.target.value;
}
return e;
});
this.setState({
items: updatedItems,
})
}
Added sku parameter to identify the item in items array and the field you want to change. Since sku is not going to change, we can use this as the item identifier. This way, you can change the product name and its respective attributes. For full working code, see the code example below.
class Container extends React.Component {
constructor() {
super();
this.state = {
items: [{
name: "Product Name",
sku: "00001",
attributes: {
width: "value",
height: "value",
length: "value",
color: "value"
}
},
{
name: "Product Name 2",
sku: "00002",
attributes: {
description: "Hello World",
weight: "250",
quantity: "500",
hello: "World"
}
}],
}
}
onChange = (e, sku, field) => {
console.log(e.target.value);
const updatedItems = this.state.items.map(item => {
if (item.sku !== sku) return item;
if (field === 'name') {
item.name = e.target.value;
} else {
// has to be attributes
item.attributes[field] = e.target.value;
}
return item;
});
this.setState({
items: updatedItems,
})
}
render() {
// log new state here
console.log(this.state.items);
return <EditForm onChange={this.onChange} items={this.state.items} />
}
}
class EditForm extends React.Component {
render() {
let editForm = this.props.items && this.props.items.map((item, idx) => {
return(
<tr key={item.sku}>
<td className="edit-table">
<input
value={item.name}
name={item.name}
onChange={(e)=> this.props.onChange(e, item.sku, 'name')}
/>
</td>
<td className="edit-table">
<ul className="item-attributes">
{Object.keys(item.attributes).map((key) => {
return (<li key={key}>
<label>{key}</label>
<input
value={item.attributes[key]}
onChange={(e) => this.props.onChange(e, item.sku, key) }
/>
</li>)
})}
</ul>
</td>
</tr>
)
})
return(
<table className="editcontainer-table">
<thead>
<tr>
<th>SKU</th>
<th>Attributes</th>
</tr>
</thead>
<tbody>
{editForm}
</tbody>
</table>
);
}
}
ReactDOM.render(<Container />, document.getElementById('app'));
<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="app"></div>

Related

Trying to sort data inside a react table: Reactjs

I am trying to sort the table data based on the field selected from the dropdown. It should alternatively sort between ascending or descending whenever the same field is clicked. I am maintaining a sort object that decides which field is selected and what is the sort order. Then it is sent to lodash orderBy with the field and the order. It does not work
This is what I have tried. Can some one tell me what I am doing wrong. Help is really appreciated.
https://codesandbox.io/s/simple-react-class-component-1n3f9?file=/src/index.js:0-3000
import React from "react";
import ReactDOM from "react-dom";
import "semantic-ui-css/semantic.min.css";
import { Dropdown } from "semantic-ui-react";
import moment from "moment";
import orderby from "lodash.orderby";
const options = [
{ key: 1, text: "Name", value: "name", icon: "sort" },
{ key: 2, text: "Time", value: "time", icon: "sort" },
{ key: 3, text: "Type", value: "type", icon: "sort" }
];
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
original: [],
sortObject: { field: "", order: "" }
};
}
componentDidMount() {
let list = [
{
name: "namev1",
time: 1583295463213,
type: 14
},
{
name: "namea2",
time: 1582885423296,
type: 15
},
{
name: "namea3",
time: 1581295463213,
type: 16
}
];
this.setState({ list, original: list });
}
handleSearch = e => {
let searchInput = e.target.value;
let filteredData = this.state.original.filter(value => {
return (
value.name.toLowerCase().includes(searchInput.toLowerCase()) ||
value.type.toString().includes(searchInput.toString())
);
});
this.setState({ list: filteredData });
};
formSortObject = fieldName => {
let { sortObject } = this.state;
if (!sortObject.field || sortObject.field !== fieldName) {
Object.assign(sortObject, {
field: fieldName,
order: "asc"
});
return sortObject;
} else if (sortObject.field === fieldName) {
Object.assign(sortObject, {
...sortObject,
order: sortObject.order === "desc" ? "asc" : "desc"
});
return sortObject;
}
};
handleSort = (e, data) => {
let dropdDownValue = data.value;
let currentField = this.formSortObject(dropdDownValue);
let result = orderby(
this.state.list,
currentField.field,
currentField.order
);
this.setState({ list: result });
};
render() {
return (
<>
Search: <input type="text" onChange={this.handleSearch} />
<Dropdown text="Sort By" options={options} onChange={this.handleSort} />
<h1>List</h1>
<table>
<tbody>
{this.state.list.map((item, index) => (
<tr key={index}>
<td>
<p>{index + 1}</p>
</td>
<td>
<p>{item.name}</p>
</td>
<td>
<p>{moment().diff(item.time, "days")}</p>
</td>
<td>
<p>{item.type}</p>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}
}
Your code isnt's working as you spected because you are calling the handleSort function only when the value of the select change (see the onChange of your <Dropdown />).
What you need is the function executed when an option is clicked.
I searched for the documentation of the library you are using and I came to the conclusion that what you need is this.
<Dropdown text='Sort By'>
<Dropdown.Menu>
{options.map(item=>
<Dropdown.Item text={item.text} value={item.value} icon={item.icon} key={item.key} onClick={this.handleSort}/>
)}
</Dropdown.Menu>
</Dropdown>
I tried it in your codesandbox and it works perfectly!
I hope it helps!

Unable to access the table header data

I'm developing an app using React. However, facing some issue with reflecting the {column.label} at column header field. The code works with any plain text but not for inputs in {}.
Below is the code for reference.
filename: tableHeader.jsx:
import React, { Component } from "react";
class TableHeader extends Component {
raiseSort = (path) => {
const sortColumn = { ...this.props.sortColumn };
if (sortColumn.path === path)
sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
else {
sortColumn.path = path;
sortColumn.order = "asc";
}
this.props.onSort(sortColumn);
};
render() {
return (
<thead>
<tr>
{this.props.columns.map((column) => (
<th
key={column.path || column.key}
onClick={() => this.raiseSort(column.path)}
>
{column.label}
</th>
))}
</tr>
</thead>
);
}
}
export default TableHeader;
filename: moviesTable.jsx:
import React, { Component } from "react";
import Like from "./common/like";
import TableHeader from "./common/tableHeader";
class MoviesTable extends Component {
columns = [
{ path: "title", lable: "Title" },
{ path: "genre.name", lable: "Genre" },
{ path: "numberInStuck", lable: "Stock" },
{ path: "dailyRentalRate", lable: "Rate" },
{ key: "Like" },
{ key: "Delete" },
];
render() {
const { movies, onDelete, onLike, onSort, sortColumn } = this.props;
return (
<table className="table">
<TableHeader
columns={this.columns}
sortColumn={sortColumn}
onSort={onSort}
/>
<tbody>
{movies.map((movie) => (
<tr key={movie._id}>
<td> {movie.title} </td>
<td> {movie.genre.name} </td>
<td> {movie.numberInStock} </td>
<td> {movie.dailyRentalRate} </td>
<td>
<Like liked={movie.liked} onClick={() => onLike(movie)} />
</td>
<td>
<button
onClick={() => onDelete(movie)}
className="btn btn-danger btn-sm"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
}
export default MoviesTable;
fileName: movie.jsx:
import React, { Component } from "react";
import { getMovies } from "../services/fakeMovieService";
import { getGenres } from "../services/fakeGenreService";
import Pagination from "./common/pagination";
import { paginate } from "../utils/paginate";
import ListGorup from "./common/listGroup";
import MoviesTable from "./moviesTable";
import _ from "lodash";
class Movies extends Component {
state = {
movies: [],
genres: [],
pageSize: 4,
currentPage: 1,
sortColumn: { path: "title", order: "asc" },
};
componentDidMount() {
const genres = [{ _id: "", name: "All Genres" }, ...getGenres()];
this.setState({ movies: getMovies(), genres });
}
handleDelete = (movie) => {
const movies = this.state.movies.filter((m) => m._id !== movie._id);
this.setState({ movies });
};
handleLike = (movie) => {
const movies = [...this.state.movies];
const index = movies.indexOf(movie);
movies[index] = { ...movie };
movies[index].liked = !movies[index].liked;
this.setState({ movies });
};
handlePageChange = (page) => {
this.setState({ currentPage: page });
};
handleGenreSelect = (genre) => {
this.setState({ selectedGenre: genre, currentPage: 1 });
};
handleSort = (sortColumn) => {
this.setState({ sortColumn });
};
render() {
const { length: count } = this.state.movies;
const {
pageSize,
currentPage,
movies: allMovies,
selectedGenre,
sortColumn,
} = this.state;
if (count === 0) return <p>No movies in the DB..</p>;
const filtered =
selectedGenre && selectedGenre._id
? allMovies.filter((m) => m.genre._id === selectedGenre._id)
: allMovies;
const sorted = _.orderBy(filtered, [sortColumn.path], [sortColumn.order]);
const movies = paginate(sorted, currentPage, pageSize);
return (
<div>
<div className="row">
<div className="col-3">
<ListGorup
items={this.state.genres}
onItemSelect={this.handleGenreSelect}
selectedItem={this.state.selectedGenre}
/>
</div>
<div className="col">
<p>Showing {filtered.length} movies in the DB. </p>
<MoviesTable
movies={movies}
onLike={this.handleLike}
onDelete={this.handleDelete}
onSort={this.handleSort}
sortColumn={sortColumn}
/>
<Pagination
itemsCount={filtered.length}
onPageChange={this.handlePageChange}
pageSize={pageSize}
currentPage={currentPage}
/>
</div>
</div>
</div>
);
}
}
export default Movies;
The below code from file1(tableHeader.jsx) is responsible to give the column name. Not sure, why it's not coming.
<thead>
<tr>
{this.props.columns.map((column) => (
<th
key={column.path || column.key}
onClick={() => this.raiseSort(column.path)}
>
{column.label}
</th>
))}
</tr>
</thead>
Looking for to hearing from you.
Your columns list only contains objects with the key lable (misspelled). It should be label:
columns = [
{ path: "title", /* not lable ! */ label: "Title" },
{ path: "genre.name", label: "Genre" },
{ path: "numberInStuck", label: "Stock" },
{ path: "dailyRentalRate", label: "Rate" },
{ key: "Like" },
{ key: "Delete" },
];

Adding clicked items to new array in React

I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!

Pass a parameter to a prop that is actually a component?

My scenario is that I have a table that is generated based on data. In one of the columns I would like to pass in a 'remove' button/component, which is meant to remove the row that it is in.
My issue is that the 'remove' button component needs to be given the row so it can determine which data to remove.
If you look in Table.js, you can see where I render the prop as a component '{col.component}' - But how can I also pass values to the components action?Examples below.
App.js
import React, { Component } from 'react';
import Table from './Table';
import Table from './RemoveButton';
class App extends Component {
//Data is the array of objects to be placed into the table
let data = [
{
name: 'Sabrina',
age: '6',
sex: 'Female',
breed: 'Staffordshire'
},
{
name: 'Max',
age: '2',
sex: 'Male',
breed: 'Boxer'
}
]
removeRow = name => {
//Remove object from data that contains name
}
render() {
//Columns defines table headings and properties to be placed into the body
let columns = [
{
heading: 'Name',
property: 'name'
},
{
heading: 'Age',
property: 'age'
},
{
heading: 'Sex',
property: 'sex'
},
{
heading: 'Breed',
property: 'breed'
},
{
heading: '',
component: <RemoveButton action=removeRow()/>
}
]
return (
<>
<Table
columns={columns}
data={data}
propertyAsKey='name' //The data property to be used as a unique key
/>
</>
);
}
}
export default App;
RemoveButton.js
import React from 'react';
const RemoveButton = action => {
return(
<button onClick={action}>Remove Row</button>
)
}
export default RemoveButton;
Table.js
const Table = ({ columns, data, propertyAsKey }) =>
<table className='table'>
<thead>
<tr>{columns.map(col => <th key={`header-${col.heading}`}>{col.heading}</th>)}</tr>
</thead>
<tbody>
{data.map(item =>
<tr key={`${item[propertyAsKey]}-row`}>
{columns.map(col => {
if(col.component){
return(<td> key={`remove-${col.property}`}>{col.component}</td>)
} else {
return(<td key={`${item[propertyAsKey]}-${col.property}`}>{item[col.property]}</td>)
}
})}
</tr>
)}
</tbody>
</table>
Instead of passing down a component in the column, you could pass down the removeRow function to the Table component as a regular prop, and have another value on the remove column to indicate when you should render the remove button for that column, and pass the item name when you invoke it.
class App extends React.Component {
state = {
data: [
{
name: "Sabrina",
age: "6",
sex: "Female",
breed: "Staffordshire"
},
{
name: "Max",
age: "2",
sex: "Male",
breed: "Boxer"
}
]
};
removeRow = name => {
this.setState(({ data }) => ({
data: data.filter(el => el.name !== name)
}));
};
render() {
let columns = [
{
heading: "Name",
property: "name"
},
{
heading: "Age",
property: "age"
},
{
heading: "Sex",
property: "sex"
},
{
heading: "Breed",
property: "breed"
},
{
heading: "",
removeCol: true
}
];
return (
<Table
columns={columns}
data={this.state.data}
removeRow={this.removeRow}
propertyAsKey="name"
/>
);
}
}
const Table = ({ columns, data, removeRow, propertyAsKey }) => (
<table className="table">
<thead>
<tr>
{columns.map(col => (
<th key={`header-${col.heading}`}>{col.heading}</th>
))}
</tr>
</thead>
<tbody>
{data.map(item => (
<tr key={`${item[propertyAsKey]}-row`}>
{columns.map(col => {
if (col.removeCol) {
return (
<td key={`remove-${col.property}`}>
<button onClick={() => removeRow(item.name)}>
Remove Row
</button>
</td>
);
} else {
return (
<td key={`${item[propertyAsKey]}-${col.property}`}>
{item[col.property]}
</td>
);
}
})}
</tr>
))}
</tbody>
</table>
);
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>

dynamic setState to add new dynamic property in react

I have this scenario where I have to add multiple and dynamic property to an array of object. Says it's lang = ['en', 'fr', 'more'], how can I produce a object structure like this
Below is my failed attempt:
class App extends React.Component {
state = {
lang: ["en", "fr"],
items: [
{
id: 1,
value: {
en: "abc",
fr: "hello"
}
}
]
};
onChange = (e, i) => {
this.setState({
items: this.state.items.map(o => ({
...o,
value: {
[this.state.lang[i]]: e.target.value //need fix
}
}))
});
};
render() {
return (
<div className="App">
{this.state.lang.map((o, index) => (
<div>
<input
onChange={e => this.onChange(e, index)}
placeholder={o}
type="text"
/>
<br />
</div>
))}
<br />
<pre>{JSON.stringify(this.state, null, 2)}</pre>
</div>
);
}
}
https://codesandbox.io/s/p746jn313q
If I understood what you're trying to do correctly, you just needed to spread the value object inside of your map:
onChange = (e, i) => {
this.setState({
items: this.state.items.map(o => ({
...o,
value: {
...o.value, // <- this
[this.state.lang[i]]: e.target.value
}
}))
});
};
I've also edited the codesandbox: https://codesandbox.io/s/13vo5rrjwj

Categories

Resources