How to set value to state for recursive rendered component? - javascript

I need to make a component. This should be a editable tree component. I tried a lot, but i can't handle issue. I don't use any libraries such as redux. I made a component it travel all json object recusively. Now, At onChange event, it passes param (path of key) according to this path I wrote a function called 'updateObjectValueByPath' updates that value. Now I need to save this json object to a state. But I can't save latest version, when I switch input field 'datas' object reset its value. Here is my code, maybe I can't explain myself clearly. Hope code can help. Thanks for all..
render() {
let json = this.state.json;
const data = this.props.data;
const path = this.props.path;
if (Array.isArray(data)) {
return data.map((element, index) => {
let currentPath = `${path}.${index}`;
return (
<div key={index}>
<CCol sm={12}>
<CInputGroup>
<CCol sm={3}>
<CInputGroupPrepend>
<CListGroup accent>
<CListGroupItem key="id" accent="success">
{this.props.properties}
{index + 1} :
</CListGroupItem>
</CListGroup>
</CInputGroupPrepend>
</CCol>
<CCol sm={5}>
{index === 0 ? (
<CButton color="warning" onClick={() => this.addItem(path)}>
Add
</CButton>
) : null}
</CCol>
<CCol sm={4}>
<CButton
color="danger"
onClick={() => this.removeItem(path, index)}
>
Remove
</CButton>
</CCol>
</CInputGroup>
</CCol>
<CCol>
<JsonTree
path={currentPath}
data={element}
arrayIndex={index}
key={currentPath}
jsonContent={this.state.json}
></JsonTree>
</CCol>
</div>
);
});
} else if (data instanceof Object) {
// console.log(this.props.jsonContent);
return Object.keys(data).map((key) => {
let currentPath = path !== "" ? `${path}.${key}` : key;
return (
<JsonTree
data={data[key]}
path={currentPath}
key={currentPath}
properties={key}
jsonContent={this.state.json}
/>
);
});
} else {
// console.log(this.props.jsonContent);
return (
<div>
{console.log(this.state.json)}
{/* {this.setState({ json: this.state.den })} */}
<CCol sm={12} key={path}>
<CInputGroup>
<CCol sm={3}>
<CInputGroupPrepend>
<CListGroup accent>
<CListGroupItem key="id" accent="primary">
{this.props.properties}:
</CListGroupItem>
</CListGroup>
</CInputGroupPrepend>
</CCol>
<CCol sm={9}>
{/* number kabul edecek value olarak */}
{typeof data === "number" ? (
<CInput
id={path}
name={path}
defaultValue={data}
type="number"
style={{ fontWeight: "bold" }}
// disabled={!this.props.isEditable}
onChange={(event) => this.handleChange(event, path, json)}
/>
) : typeof data === "string" ? (
<CInput
id={path}
name={path}
type="text"
defaultValue={data}
style={{ fontWeight: "bold" }}
// disabled={!this.props.isEditable}
onChange={(event) => this.handleChange(event, path, json)}
/>
) : null}
</CCol>
</CInputGroup>
</CCol>
</div>
);
}
}
handleChange = (event, path, datas) => {
const seperatedPath = path.split(".");
let arrayPath = seperatedPath.map((v) => (isFinite(v) ? +v : v));
let value = event.target.value;
let newData = this.updateObjectValueByPath(arrayPath, datas, value);
console.log(newData);
this.setState({ json: newData });
};
updateObjectValueByPath = (arrayPath, datas, value) => {
const key = arrayPath[0];
if (Array.isArray(datas) && datas !== null) {
if (arrayPath.length > 1) {
return datas.map((el, index) => {
if (index === key) {
return this.updateObjectValueByPath(
arrayPath.slice(1, arrayPath.length),
el,
value
);
}
return el;
}, []);
}
return [...datas, value];
}
if (typeof datas === "object" && !Array.isArray(datas) && datas !== null) {
if (arrayPath.length > 1) {
return {
...datas,
[key]: this.updateObjectValueByPath(
arrayPath.slice(1, arrayPath.length),
datas[key],
value
),
};
}
return { ...datas, [key]: value };
}
return datas;
};

I solved that issue, for solution:
I wrote a callback function which call that class. And this callback prop also should be written in recursive field like that;
<JsonTree
data={data[key]}
path={currentPath}
key={currentPath}
properties={key}
jsonContent={this.state.json}
callback={this.props.callback}
/>
Then, when onchange event triggered, call this callback and sent newData as param.
let newData = this.updateObjectValueByPath({ arrayPath, datas, value });
console.log(newData);
this.props.callback(newData);
By doing this, we are traversing the new object as a parameter through the parent.

Related

How do I calculate the price upon entering the quantity but also show its quantity when the add and minus button was clicked?

So what I wanted to do was to also calculate the unit price according to what number was entered. I was already able to update the quantity of the product and then calculate its unit price and also its total amount.
My problem now is how can I also allow the user to enter the quantity of the product. And also still retain the functionality to increase and decrease the product.
I have recreated this in codesandbox: https://codesandbox.io/s/add-to-cart-sampled-2-with-table-efqrhd?file=/src/Cart.js
const Cart = ({ cartItems, handleCartClearance, handleRemove, handleAdd }) => {
const [inputQty, setInputQty] = useState(0);
console.log(cartItems, "items");
const totalAmount = cartItems.reduce(
(price, item) => price + item.quantity * item.price,
0
);
const handleSubmit = (e) => {
e.preventDefault();
console.log(cartItems, "order");
};
console.log(
"test",
cartItems.reduce((prev, item) => {
if (!prev[item.id]) prev[item.id] = { ...item, nest: [] };
prev[item.id].nest.push({ ...item });
return prev;
}, {})
);
return (
<div>
<form onSubmit={handleSubmit}>
Order page
{cartItems.length >= 1 && (
<Button onClick={handleCartClearance}>Clear Orders</Button>
)}
{cartItems.length === 0 && <div>No Items in the cart</div>}
<div>
{Object.entries(
cartItems.reduce((prev, item) => {
if (!prev[item.id]) prev[item.id] = { ...item, nest: [] };
prev[item.id].nest.push(item);
return prev;
}, {})
).map(([id, obj], idx) => (
<TableContainer key={id + obj.color} component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">
Product Name: {obj.name + " " + obj.size}
</TableCell>
<TableCell colspan={3}></TableCell>
</TableRow>
<TableRow>
<TableCell>Color</TableCell>
<TableCell>Qty</TableCell>
<TableCell></TableCell>
<TableCell>Unit Price</TableCell>
</TableRow>
</TableHead>
<TableBody>
{obj.nest.map((nest, idx) => (
<TableRow key={idx}>
<TableCell>{nest.color}</TableCell>
<TableCell>
<input
style={{ width: "1rem" }}
value={nest.quantity}
/>
</TableCell>
<TableCell align="right">
<IconButton
onClick={(e) =>
handleAdd(
nest.id,
nest.prodName,
nest.price,
nest.size,
nest.cat,
nest.color
)
}
>
<AddIcon color="success" />
</IconButton>
<IconButton onClick={() => handleRemove(nest)}>
<RemoveIcon />
</IconButton>
</TableCell>
<TableCell>
{Number(nest.quantity) * Number(nest.price)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
))}
<div>
<b>Total Amount :{totalAmount}</b>
</div>
{cartItems.length >= 1 && <Button type="submit">Save Order</Button>}
</div>
</form>
</div>
);
};
export default Cart;
Additional Answer from the answer below to only accept whole numbers:
<input
style={{ width: "1rem" }}
value={nest.quantity}
onChange={(e) => {
if (e.target.value === "0") {
return handleRemove(nest);
}
const re = /^[0-9\b]+$/;
if (
e.target.value === "" ||
re.test(e.target.value)
) {
handleAdd(
nest.id,
nest.prodName,
nest.price,
nest.size,
nest.cat,
nest.color,
e.target.value
);
}
}}
/>
Make an addition to your handleAdd function to pass in another quan argument. Then add an onChange to your input to call handleAdd and pass in the optional quan value as e.target.value.
const handleAdd = (id, name, price, size, cat, color, quan = null) => {
console.log("add", id, name, price, size, cat, color, quan);
const productExist = cartItems.find(
(item) => item.id === id && item.color === color
);
if (productExist) {
setCartItems(
cartItems.map((item) =>
item.id === id && item.color === color
? { ...productExist, quantity: quan === "" || quan ? quan: +productExist.quantity + 1 }
: item
)
);
} else {
setCartItems([
...cartItems,
{ id, name, price, size, cat, color, quantity: 1 }
]);
}
};
<input
style={{ width: "1rem" }}
value={nest.quantity}
onChange={(e) => {
if (e.target.value === "0") {
return handleRemove(nest)
}
handleAdd(
nest.id,
nest.prodName,
nest.price,
nest.size,
nest.cat,
nest.color,
e.target.value
)
}
}
/>
Notice I added some error handling for a 0 value being passed from the input. There may be additional steps for you to figure out how to handle negative values, decimal values, etc.
One "gotcha" when working with inputs: the value coming back from it is a string not an integer. To keep things consistent as integers, I added the + operator to the front on any quantity math I need to perform. Here is a link to Unary Operator so you can get familiar with the usage of +.

I have this error that say "Cannot update a components while rendering a different component" when I run react.js code

I have the error "Warning: Cannot update a component (App) while rendering a different component (Car). To locate the bad setState() call inside Car" when I run my code. If I take out the function removeFromCart(p), the code is good, but if this function is in App, the code is broken.
function App() {
const [product, setProduct] = useState({
product: data.products,
cartItem:[],
size: "",
sort: ""
})
const removeFromCart = (p) => {
//Copy the recent products in cart
let cartItems = product.cartItem;
//Take out the product you choose in cart
setProduct({
...product,
cartItem: cartItems.filter(x=>x._id!==p._id)
});
}
//Add item to the product.cartItem
const addToCart = (p) => {
//Copy what does the cart has
const cartItems = product.cartItem;
let inCart = false;
//To check the item you click is in the cart or not
cartItems.forEach((item) => {
if (item._id === p._id) {
//If in the cart, the item count +1
item.count++;
//inCart become true to tell next statement the item you click already in in the cart,
//WE don't need to push a need item to the cart.
inCart = true;
}
})
//If the item you click is not in the cart,
//Pushing this to the cart, and set this item's count in 1.
if (!inCart) {
cartItems.push({...p, count:1})
}
//To renew the cartItem in the product state.
setProduct({...product, cartItem:cartItems});
}
//To show the items which has the size you choose
const sizeProducts = (e) => {
//value is the size you choose
let { value } = e.target
//s is the sort parameter you choose
let s = product.sort;
//If you didn't choose any size, show all product.
if (value === "") {
setProduct({ size:"", sort:s, product: data.products });
} else {
//If you choose any size, show the product that have the size you choose,
//and sort it will the sort parameter yuou choose
setProduct({
size: value,
sort:s,
product: data.products.filter(p =>
p.availableSizes.indexOf(value) >= 0
).sort((a, b) =>
s === "lowest" ? ((a.price > b.price) ? 1 : -1) :
s === "highest" ? ((a.price < b.price) ? 1 : -1) :
((a._id > b._id) ? 1 : -1)
)
});
}
}
const sortProducts = (e) => {
//The value is the sort parameter you choose
let { value } = e.target;
//The curr is the product after yiu choose the parameter in the size
let curr = product.product;
//Sort product
setProduct({
sort: value,
product: curr.sort((a, b) =>
value === "lowest" ? ((a.price > b.price) ? 1 : -1) :
value === "highest" ? ((a.price < b.price) ? 1 : -1) :
((a._id > b._id) ? 1 : -1)
)
});
}
return (
<div >
<Grid xs={ 12 }>
<Navigation />
</Grid>
<Grid container>
<Grid xs={0} sm={2}></Grid>
<Grid container xs={12} sm={8}>
<Grid item xs={12}>
<Filter count={product.product.length}
size={product.size}
sort={product.sort}
sizeProducts={sizeProducts}
sortProducts={ sortProducts}
></Filter>
</Grid>
<Product
products={product.product}
addToCart={ addToCart }
></Product>
</Grid>
<Grid xs={0} sm={2}></Grid>
</Grid>
<div className="cartSection">
<Car cartItems={product.cartItem}
removeFromCart={ removeFromCart } />
</div>
</div>
);
}
Car component:
export default class Car extends Component {
render() {
const { cartItems } = this.props;
return (
<Grid container className="cart">
<Grid container xs={12}>
<Grid xs={12} sm={6} md={3} >
<div>
{cartItems.length === 0 ?
(<div>Cart Empty</div>) :
(<div>You have {cartItems.length} in cart</div>)
}
{cartItems.map(item => (
<div key={ item._id } className="cartItem">
<img src={item.image} alt="" />
<div className="info">
<div>
{ item.title }
</div>
<div>
<span>{item.count} X ${item.price}</span>
<Button
variant="contained"
color="primary"
onClick={ this.props.removeFromCart(item) }
>Remove</Button>
</div>
</div>
</div>
))}
</div>
</Grid>
</Grid>
</Grid>
)
}
}
Error:
index.js:1 Warning: Cannot update a component (`App`) while rendering a different component (`Car`). To locate the bad setState() call inside `Car`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
at Car
at div
at div
at App
doing
onClick={ this.props.removeFromCart(item) }
will call this.props.removeFromCart immediately, causing the App component to update while rendering Car ( because it's updating its state ), replace it with :
// in the Car component
onClick={ () => this.props.removeFromCart(item) }
see the snippet below for the difference , the first one will call greet even before you click on the button :
const App = () => {
const greet = (msg) => { console.log('hello, ', msg); }
return <div>
<button onClick={greet('world')}>click me</button>
<button onClick={() => greet('world 2')}>click me</button>
</div>
}
ReactDOM.render(<App />, document.querySelector('#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>

Error: Maximum update depth exceeded when using react input mask on formik

I am trying to bind react-input-mask with formik. However, I get an error on the first key stroke. Here is how I am trying to use it. If i remove react-input-mask then the code works as a normal textfield but if i use input mask then only i get 'maximum updated depth' error. What might be the reason?
const InputMaskField = ({
label,
field,
form,
prepend,
append,
innerAppend,
...rest
}) => {
const [active, setActive] = React.useState(false)
const hasError =
(get(form.touched, `${field.name}`) || get(form, 'submitCount') > 0) &&
get(form.errors, `${field.name}`)
return (
<InputWrapper>
<Label isActive={active}>{label}</Label>
<InputMask {...rest}>
{(inputProps) => {
return (
<>
<TextInputWrapper isActive={active}>
{prepend && <InputAddonItem prepend>{prepend}</InputAddonItem>}
<InputAddonField
isActive={active}
hasError={hasError}
prepend={!!prepend}
append={!!append}
{...field}
{...inputProps}
onBlur={(e) => {
setActive(false)
field.onBlur(e)
}}
onFocus={() => setActive(true)}
/>
{append && (
<InputAddonItem
isActive={active}
hasError={hasError}
{...field}
{...rest}
>
{append}
</InputAddonItem>
)}
{innerAppend && <InnerAppend>{innerAppend}</InnerAppend>}
</TextInputWrapper>
</>
)
}}
</InputMask>
{hasError && <Error>{hasError}</Error>}
</InputWrapper>
)
}

why would react not render conditionally

So i have an issue while trying to render this list, item.cars, item.yachts and item.villas are numbers, what could be the reason for this not showing absolutely anything?
const renderItems = (locations) => {
<div className={styles.list}>
{locations.map((item) => {
if (item.cars != 0 || item.yachts != 0 || item.villas != 0)
return (
<DestinationItem
id={item.id}
image={Image[item.id]}
name={item.name}
subtitle={item.description}
description={DESTINATIONS_DESCRIPTION[item.id]}
/>
);
return null;
})}
</div>;
};
this is an item in locations object
cars: 0
description: "(Miami, Palm Beach, Broward)"
id: 3
name: "South Florida"
photo: "http://l.amazonaws.com/areaphotos/Audi_R8_a.jpg"
villas: 69
yachts: 53
__proto__: Object
this is the whole component code:
const Destinations = locations => {
const renderItems = (locations) => {
<div className={styles.list}>
{locations.map(
(item) =>
(item.cars != 0 || item.yachts != 0 || item.villas != 0) && (
<DestinationItem
id={item.id}
key={item.id}
image={Image[item.id]}
name={item.name}
subtitle={item.description}
description={DESTINATIONS_DESCRIPTION[item.id]}
/>
)
)}
</div>;
console.log(locations, 'data');
return (
<div className={styles.mainContainer}>
<div className={styles.title}>
<h1>Pick Your Destination</h1>
</div>
{renderItems(locations)}
</div>
);
};
};
export default Destinations;
Ok, so I'm not fully sure, but I try not to use if statements in the DOM. Try this style of conditional instead:
<div className={styles.list}>
{locations.map((item) =>
(item.cars != 0 || item.yachts != 0 || item.villas != 0) && (
<DestinationItem
id={item.id}
key={item.id}
image={Image[item.id]}
name={item.name}
subtitle={item.description}
description={DESTINATIONS_DESCRIPTION[item.id]}
/>)
})
</div>;
The && operator acts as your if statement. And this way, you don't even need to include {}s in your map function. Also, don't forget to add a unique key to any mapped elements!
The Destinations component takes locations as a prop, correct? In that case, you'll want to either destructure the props object, like this:
const Destinations = ({ locations }) => {
or this
const Destinations = props => {
const { locations } = props
or just use the props object and access the fields separately:
const Destinations = props => {
// ...
return (
// ...
{renderItems(props.locations)}
// ...
)
}
Additionally, the renderItems function has curly braces around it, so you need to use return to return a value from it. It's currently returning undefined, as there is no return.
const renderItems = (locations) => {
return (
<div className={styles.list}>
{locations.map((item) => {
if (item.cars !== 0 || item.yachts !== 0 || item.villas !== 0)
return (
<DestinationItem
id={item.id}
image={Image[item.id]}
name={item.name}
subtitle={item.description}
description={DESTINATIONS_DESCRIPTION[item.id]}
/>
);
return null;
})}
</div>
);
};
Or you can use the concise form of an arrow function, and replace the curly braces with parentheses:
const renderItems = (locations) => (
<div className={styles.list}>
{locations.map((item) => {
if (item.cars !== 0 || item.yachts !== 0 || item.villas !== 0)
return (
<DestinationItem
id={item.id}
image={Image[item.id]}
name={item.name}
subtitle={item.description}
description={DESTINATIONS_DESCRIPTION[item.id]}
/>
);
return null;
})}
</div>
);
As a side-note, you'll almost always want to use strict equality operators (=== and !==) instead of abstract equality operators (== and !=). The latter one has its share of pitfalls, e.g. '1' == 1 is true, as unexpected things like [[]] == 0!

Input not rerendering inside SimpleForm

i was using AOR 1.2.2 for site settings panel.
Tricky part was that those settings can have different types: string,int,bool, array of string, int etc
I managed to do this by connecting record from store and using this code:
const SettingsEdit = (props) => {
return (
<Edit actions={<SettingsEditActions {...props} />} title=
{<SettingsTitle />} {...props}>
<SimpleForm toolbar={<EditToolbar />}>
<TextField source="description" />
<DisabledInput elStyle={{ width: '100%' }} label="Default
value" source="defaultValue" />
{renderCountryValue(props)}
</SimpleForm>
</Edit>
);
};
const renderCountryValue = (prop) => {
const record = prop.record;
if (record) {
if (record.multilang) {
// countryValue will be a dict with locale keys
// TODO Multilang fields temporary disabled in restClient
return null;
}
// countryValue will be single value or array
if (record.schema.type === 'array') {
// countryValue will be single array
if (record.schema.items.type === 'string') {
return <LongTextInput format={v => v.join()} parse={v => v.split(',')} label="Value" source="countryValue" />;
}
if (record.schema.items.type === 'integer') {
return <LongTextInput format={v => v.join()} parse={v => v.split(',')} validate={validateIntegerArray} label="Value" source="countryValue" />;
}
}
// countryValue will be single value
if (record.schema.type === 'string') {
return <TextInput label="Value" source="countryValue" />;
}
if (record.schema.type === 'integer') {
return <NumberInput label="Value" source="countryValue" />;
}
if (record.schema.type === 'boolean') {
return <BooleanInput label="Value" source="countryValue" />;
}
return <LongTextInput label="Value" source="countryValue" />;
}
return <TextInput label="Value" source="countryValue" />;
};
It was working well untill i tried updating AOR to 1.3.1 then it stopped.
What i noticed is that in first render there is no record so it renders default TextInput but on second render when there is record it doesn't rerender this Input into correct type like NumberInput or etc.
I tried to debug it and programm come to place when it should render other Input but nothing happen on screen.
Any ideas or workarounds?
Have faced some issues myself when depending on record in props. I usually then set manual checks for the value and return null when no record is found in props.

Categories