I have a dynamic form in Ant Design where I can:
set the price and the quantity in some inputs.
create new rows
I was able to access all the values from the rows after submiting with this piece of code:
const onFinish = values => {
console.log("Received values of form:", values);
};
But I would like to access each value from the inputs, get the total (price * quantity) and set it the total input.
I saw a solution in basic React from this question Calculating quantity and price total in react. That answer shows that you need use state and use the onChange event and work from there, but I don't really know how to translate this answer because I don't understand how to access the state from each row in Ant Design.
You can see my code right here: https://codesandbox.io/s/stupefied-hawking-pcr7k?file=/index.js:260-348
Any help would be appreciated
You can calculate derived values by taking advantage of the onValuesChange event handler on the Form component, and the form instance methods provided by the Form.useForm hook.
Please see sandbox example:
https://codesandbox.io/s/antdesign-calculating-a-derived-value-using-dynamic-form-hgyzh?file=/index.js:229-1020
const ItemForm = () => {
const [form] = Form.useForm()
const onFinish = values => {
console.log('Received values of form:', values)
}
const handleTotal = (_, values) => {
const rowsCopy = [...values.rows]
values.rows.forEach((fieldGroup, index) => {
if (fieldGroup && fieldGroup.quantity && fieldGroup.price) {
fieldGroup.total = fieldGroup.quantity * fieldGroup.price
rowsCopy.splice(index, 1, fieldGroup)
console.log('fieldGroup', fieldGroup)
console.log('rowsCopy', rowsCopy)
form.setFieldsValue({ rows: rowsCopy })
}
})
}
return (
<Form
name="dynamic_form_nest_item"
form={form}
onFinish={onFinish}
onValuesChange={handleTotal}
autoComplete="off"
size="small"
>
Related
I am new to ReactJS and pairing it with Material UI is really causing me some roadblocks. I have created a reusable search filter component for my data tables and it worked exactly the way I wanted, but now I want to add a button to clear the field and show the unfiltered results, as well as return the InputSearch component back to its default state so it will display the label inside the field again, not up in the field’s border as these Material UI TextFields do then they are focused or have a current value. This is where I am hitting my roadblock. I have tried multiple solutions I found online, like using the inputRef/useCallback method to change the values, but it didn’t seem to work…or maybe I misunderstood and did it wrong. I was also recommended to put my search values to state. As happens with state my searches are now always one render behind (I.E. , results matching ‘US’ for ‘USA’ , ‘USA’ for ‘USAF’, etc…). Then when I run the handleFilterReset function to set the filter values back to an empty string, nothing happens. I just want my search filter to work instantly (like it did before I moved the value to state [commented out]) and be able to be cleared, resetting the table back to its default display.
Can someone please help me figure this out? Suggestions are appreciated, but code snippets are much more helpful since I am really new to React and especially Material UI.
dataTable.js
const [inputValue, setInputValue] = useState('')
const [searchFn, setSearchFn,] = useState({ fn: items => { return items; } });
// Searching Data
const handleSearch = e => {
setInputValue(e.target.value) // value displayed in input field
let query = (e.target.value).toString().toLowerCase();
setSearchFn({
fn: items => {
if (query === "")
return items;
else
return items.filter(x =>
(x.tankName !== null && x.tankName.toLowerCase().includes(query)) ||
(x.dimensions !== null && x.dimensions.toLowerCase().includes(query))
)
}
})
}
// Clearing Filters
const handleFilterReset = () => {
setInputValue('');
setSearchFn({fn: items => {return items;}})
};
// Search and filter Inputs
<div>
<InputSearch
value={inputValue}
onChange={handleSearch}
/>
<Button
text="Reset"
onClick={handleFilterReset}
/>
</div>
InputSearch.js
export default function InputSearch(props) {
const { inputRef, name, value, error=null, onChange, ...other } = props;
return (
<TextField
label="Search..."
name={name}
value={value}
onChange={onChange}
{...other}
{...(error && {error:true, helperText:error})}
>
</TextField>
)
}
You need to pass the value to InputSearch
Heres an example:
https://codesandbox.io/s/morning-brook-durbvd?file=/demo.tsx
React has a pretty good introduction on its site.
https://reactjs.org/docs/components-and-props.html
The code has been updated with a solution to this issue. I created a display value for the input that I passed to state, which was set to a blank string when the reset is pressed as well as passing an unfiltered data set.
screeen record of the issue: https://streamable.com/ofn42v
it is working fine in local but once deployed to production(vercel), it is not working. i have tried sooo many different things like having a separate state in cart, useEffect with totalQuantity in dependency array and nothing seems to work. Ideally when the totalQuantity inside the context is updated, the components using it should rerender as mentioned in react doc which is happening from n to 2 except for 1. can someone please help :(
my code for the cart icon in nav bar:
function Cart(props) {
const { enableCart, totalQuantity } = useContext(AppContext);
return (
<>
{enableCart ? (
<Link href="/cart" passHref>
<a aria-label="Shopping cart" title="Shopping cart">
<Badge count={totalQuantity} offset={[0, 5]}>
<ShoppingCartIcon className="w-7 h-7" />
</Badge>
</a>
</Link>
) : null}
</>
);
}
Update quantity - code in appContext:
import { useCookies } from "react-cookie";
export const AppProvider = (props) => {
const [cartItems, updateCart] = useState([]);
const [totalQuantity, setTotalQuantity] = useState(0);
const [cookies, setCookie] = useCookies(["cart"]);
const cookieCart = cookies.cart;
useEffect(() => {
cartOperations();
}, []);
const calculateAmountQuantity = (items) => {
let totalCount = 0;
let totalPrice = 0;
items.forEach((item) => {
totalCount += item.quantity;
totalPrice += item.price * item.quantity;
setTotalAmount(totalPrice);
setTotalQuantity(totalCount);
});
};
const cartOperations = async (items) => {
if (items !== undefined) {
updateCart([...items]);
calculateAmountQuantity(items);
} else if (cookieCart !== undefined) {
updateCart([...cookieCart]);
calculateAmountQuantity(cookieCart);
} else {
updateCart([]);
setTotalAmount(0);
setTotalQuantity(0);
}
};
const addItem = (item) => {
let items = cartItems;
let existingItem;
if (items) existingItem = items.find((i) => i.id === item.id);
if (!existingItem) {
items = [
...(items || []),
Object.assign({}, item, {
quantity: 1,
}),
];
updateCart([...items]);
setTotalAmount(totalAmount + item.price * 1);
setTotalQuantity(totalQuantity + 1);
} else {
const index = items.findIndex((i) => i.id === item.id);
items[index] = Object.assign({}, item, {
quantity: existingItem.quantity + 1,
});
updateCart([...items]);
setTotalAmount(totalAmount + existingItem.price);
setTotalQuantity(totalQuantity + 1);
}
saveCartToCookie(items);
saveCartToStrapi(items);
};
i am storing the cart content in cookie.
code for AppContext is here in github, full nav bar code
Live url: https://sunfabb.com
Goto Products, add few items to cart, then try removing one by one from the cart page. (i have enabled react profiler in prod as well)
EDIT: This issue is completely specific to antd library. I was able to debug further based on the below 2 answers and there is nothing wrong with react context or re-render. i tried using a custom badge for cart and it is working perfectly fine. Yet to fix the antd issue though. I can go with custom one, but antd's badge is better with some animations.
As pointed out by #hackape, when setting the value of state to something that depends on the previous value of that state, you should pass a function to the setState instead of a value.
So instead of setTotalQuantity(totalQuantity + 1);, you should say setTotalQuantity(previousQuantity => previousQuantity + 1);.
This is the safe way of doing that, so for example if we are trying to do it twice simultaneously, they both get taken into account, instead of both using the same initial totalQuantity.
Other thing that I would think about changing is that you are setting those quantities and amounts in multiple places, and relying on the previous value. So if it goes out of sync once, it's out of sync also on the next action, and so on.
You could use the useEffect hook for this. Every time the cartItems change, calculate those values again, and do that based only on the new cartItems array, not on the old values.
Something like this for example:
useEffect(() => {
setTotalAmount(cartItems.reduce((total, currentItem) => total + (currentItem.price * currentItem.quantity), 0));
setTotalQuantity(cartItems.reduce((total, currentItem) => total + currentItem.quantity, 0));
}, [cartItems]);
Or if you prefer calling it like you do now, I would still replace the value with the reduce from my example, so it get's calculated based on the whole cart instead of previous value.
A shopping cart is usually something that contains less than 100 entries, so there is really no need to worry about the performance.
From looking at the renders and from seeing that after a refresh the cart shows as empty as should be, it's probably a lifecycle issue.
I'd suggest creating another useEffect hook that listens to totalQuantity or totalAmount (logically the bigger of the two though by the state values it looks either should be fine) and in the hook call change the cart icon based on the updated sum
EDIT:
misread your inter-component imports, because Cart (from components/index/nav.js) should listen for changes from the context.provider you would use a context.consumer on Cart with the totalQuantity value (not just with importing the variable from the context as that rides on the application rendering from other reasons)
see example in consumer docs and in this thread, and check this GitHub issues page for other's detailed journey while encountering this issue more directly
I need to dynamically generate multiple divs with a single input-box in it, so the user can add a number.
The user can add by clicking a button, any number of divs with input-box to put a number in it.
After the user end with the entry of the data, must click a button to process the data.
I've find out how to use React to iterate through an existing array, but not about how to iterate through a new DOM tree that was created dynamically by the user, and then generate an array with the values inside all the input-boxes.
After processing, different values will be displayed (max value, min value, average, return results from equations, etc)
Without seeing your code it's hard to help, but you probably want to use controlled inputs rather than uncontrolled ones, and so you'd have an array of the current values for the inputs as state information.
For instance, in a functional component using hooks:
const { useState } = React;
function Example() {
// The values we use on the inputs
const [values, setValues] = useState([]);
// Update the value at the given index
const updateValue = (value, index) => {
setValues(values =>
Object.assign([], values, {[index]: value})
);
};
// Add an input
const addInput = () => {
setValues(values => [...values, ""]);
};
// Get the sum (just an example; and one of the very few places I'll use `reduce`)
const sum = values.reduce((sum, value) => sum + Number(value), 0);
// Render as many inputs as we have values, along with the
// button to add an input and the sum
return (
<div>
<div>
{values.map((value, index) =>
<div key={index}>
<input type="text" value={value} onChange={evt => updateValue(evt.target.value, index)} />
</div>
)}
</div>
<div>Sum: {sum}</div>
<input type="button" value="Add Input" onClick={addInput} />
</div>
);
}
ReactDOM.render(<Example/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
I think you could just create one div, and an array to store the values, then create a function that everytime the user choose to add a new value, it saves the current on that array and clean the input. So, when the user select to process the data it takes that array and do what you need.
I am currently working on an online store that filters products based on certain criteria such as size, stock, gender, etc.
While I have been able to make it work to a certain extent. My program currently filters by size, gender, sorts by price etc. However, I cannot get it to filter by brand. For some reason once I click on the brand, I am able to filter the function once, however, once I click on another brand the filter for that particular brand does not run.
Here is the link to the code sandbox:
https://codesandbox.io/s/mystifying-roentgen-7mp0t
I am currently stuck with filtering by brand and I have tried to compared my filtered result to the state of the item clicked, by checking if the brand is included in the item and by using localeCompare().
Here is the link to the code sandbox:
https://codesandbox.io/s/mystifying-roentgen-7mp0t
createCheckboxes = () => available_sizes.map(this.createCheckbox);
handleFormSubmit = event => {
//4) this button updates the filters on the sizes, which I think I need to fix to update the brands, the price and the gender
event.preventDefault();
//5) right here I am storing the selected checkboxes which is what I was doing before by pushing the checkboxes
const selectedSizes = [...this.selectedCheckboxes];
const shallowCopy = [...this.state.filteredProducts];
let filteredProducts = shallowCopy.filter(product =>
selectedSizes.every(size =>
product.stock.some(s => s.stock > 0 && s.size === size)
)
);
let filteredGender = filteredProducts.filter(product => {
return product.gender.some((item, idx, arr) => {
return item[this.selectedGender] === false ? null : product;
});
});
//***this is the function that is not currently running***//
let filteredData = filteredGender.filter(product => {
//console.log(product.brand.includes(this.state.activeBrand))
//console.log(product.brand = this.state.brand)
return product.brand.includes(this.state.activeBrand)
});
let sortedPrice = filteredData.sort((a, b) => {
return this.state.sortBy === "min"
? a.price - b.price
: b.price - a.price;
});
this.setState({
filteredProducts: sortedPrice
});
};
I am expecting to be able to filter by brand wit this function, once an item is clicked.
Here is the link to the code sandbox:
https://codesandbox.io/s/mystifying-roentgen-7mp0t
There are 2 errors in your application:
1) the first one is reported by #user753642 in comment to your question, remove this line from index.js, because it sets your brand of all products to "":
console.log(product.brand = this.state.brand)
2) you are filtering filteredProducts and no the all products. While after first filtering on brand the filterdProducts does not have any item of other brands, it returns an empty collection after filtering on another brand. Change line in handleFormSubmit in index.js, from:
const shallowCopy = [...this.state.filteredProducts];
to:
const shallowCopy = [...this.state.products];
I have a React Native form that allows me to add an Input UI in the form, by clicking a button with this function. This allow me to generate it on the fly. The code for that is this.
addClick() {
this.setState(prevState => ({ values: [...prevState.values, ""] }));
console.log(this.values[0].name);
}
That part works well, but I'm having a problem extracting the data from the dynamic inputs, and add it to an array. So far I have tried this
setVal = value => {
const values = this.state.values[0];
if (values[0].name === "" || values[0].description === "") return;
[...this.state.values, value];
this.setState(values);
console.log(values);
};
How do I organize my states properly so that I can add as many inputs I need, and when I'm finished, I can update the state, and access the new data in my list component?
How do I update my state to the new Array? at the moment, this.state only shows the initial state set at the top.
I'm missing a few things
Please take a look at the full code sandbox HERE so you can see:
See...your created isssue is not so obvious we need to see where you call setVal() function but....
i think you will be more comfortable if you render your <input/> s directly from your state array, not from const x = [] variant. because it seems like you want a dynamic view and in such a cases you will need to bind your loop quantity from state. so:
this.state = {
x: [0,1,2,3,4]
}
and inside your render :
{this.state.x.map(x => {
return (
<TextInput
placeholder={`name${x}`}
value={values[x.toString()]}
handleBlur={() => handleBlur(x.toString())}
onChangeText={handleChange(x.toString())}
style={styles.input}
/>
);
})}