How to get the updated values from the table into a state in ag-grid - ag-grid

Basically, I have a table of items, where users can increase or decrease the quantity of the item they wish to purchase. The table has a Quantity column, which renders its own UI using the cellRenderer method to display increase/decrease buttons that change it. It also has a Total Price column that automatically calculate based on the quantity. The rowData for the table comes from an api, which is then modified and stored in a separate state variable.
In my current implementation, the values on the table change when I perform an increase/decrease, and the total price recalculates as well. However, this change does not get reflected on the state where the data is stored. Below is a sample code.
const Component = () => {
// ...
const [rowData, setRowData] = useState(modifiedApiData);
// ...
return (
<AgGrid
rowData={rowData}
columnDefs={[
{
field: 'name',
headerName: 'Name',
headerTooltip: 'Name',
//...
},
{
field: 'quantity',
headerName: 'Quantity',
headerTooltip: 'Quantity',
cellRenderer: (row: any) => {
const minimumQty = row?.data.min;
const maximumQty = row?.data.max;
const quantity = row?.data.quantity;
const handleIncrease = () => {
row?.setValue(quantity + 1);
};
const handleDecrease = () => {
row?.setValue(quantity - 1);
};
const disableDecrease = minimumQty === quantity;
const disableIncrease = maximumQty === quantity;
return (
<Box
display='flex'
justifyContent='space-between'
alignItems='center'
>
<IconButton
size='small'
aria-label='decrease quantity'
disabled={disableDecrease}
onClick={handleDecrease}
>
<MinusIcon />
</IconButton>
<Box component='span' sx={{ mx: '0.15rem' }}>
{row?.data.quantity}
</Box>
<IconButton
size='small'
aria-label='decrease quantity'
disabled={disableIncrease}
onClick={handleIncrease}
>
<PlusIcon />
</IconButton>
</Box>
);
},
},
// ...
]}
/>
)
}
Here, I'm using the setValue function provided from the cellRenderer parameter to update the quantity on the table. Whenever I log rowData on the console everytime I increase/decrease the quantity, nothing changes on the state, but the table does change.
How can I obtain the updated data from the table into the state? I need to get this updated state so that I can send the data into the backend when submitting the order. What is a better way to do this type of updating?

Related

AgGrid Custom Cell Editor does not refresh when calling refreshCells()

TLDR; the cell editor does not rerender when we invoke the api.refreshCells method.
The following code renders a simple table with 2 rows. The value of the data is irrelevant since it uses both a custom cell renderer and a custom cell editor that reaches into context to pluck a value.
When context updates we need to call refreshCells, since no change is detected to the actual cell data value.
Note when clicking increment the value of the context is incremented and the value of the cells updates accordingly. Observe the console log messages for each view renderer.
Now double click cell to enter into edit mode and then click the increment button. Note that the cell editor is not re-rendered when the increment takes place.
I can get the editor to update using events but the props (and so the context) are stale.
Presume this is a design decision, but I need to rerender the cell editor component when my context data updates. Any ideas?
https://plnkr.co/edit/3cCRnAY14nOWt3tl?open=index.jsx
const EditRenderer = (props) => {
console.log('render edit cell')
return (
<input
value={props.context.appContext.cellValue}
/>
);
};
const ViewRenderer = (props) => {
console.log('render view cell')
return <React.Fragment>{props.context.appContext.cellValue}</React.Fragment>;
};
const Grid = () => {
const [rowData] = useState([{ number: 10 }, { number: 3 }]);
const columnDefs = useMemo(
() => [
{
field: 'number',
cellEditor: EditRenderer,
cellRenderer: ViewRenderer,
editable: true,
width: 200,
},
],
[]
);
const [context, setContext] = useState({ cellValue: 1 });
const gridApiRef = useRef();
return (
<React.Fragment>
<p>Context value: {JSON.stringify(context)}</p>
<button
onClick={() => {
setContext({ cellValue: context.cellValue += 1 });
gridApiRef.current.refreshCells({force: true});
}}
>
Increment
</button>
<div style={{ width: '100%', height: '100%' }}>
<div
style={{
height: '400px',
width: '200px',
}}
className="ag-theme-alpine test-grid"
>
<AgGridReact
onGridReady={(e) => {
gridApiRef.current = e.api;
}}
context={{ appContext: context }}
columnDefs={columnDefs}
rowData={rowData}
/>
</div>
</div>
</React.Fragment>
);
};
render(<Grid />, document.querySelector('#root'));

AG Grid custom detail for master detail view not rendering 'could not find function component'

I am trying to get the ag-grid master-detail view to work with a custom detail view. The default detail view renders a grid, which is not what we need; however, I am running into problems when I try to supply a custom component for ag grid to render as per here. I have tried a couple options after reading through the docs in different places, but each of them seems to run into one issue or another, and wanted to ask if there is some ag-grid voodoo that I might be overlooking that should make this go smoothly/painlessly?
I have tried:
Option 1: using the 'detailCellRenderer' AgGrid option to directly inject a component. Like here
Option 2: using the 'components' option and setting the 'detailCellRenderer' key inside of that to a custom component.
Option 3: using both the 'components' and 'detailCellRenderer' options by setting the key value pair such that components['detailCellRenderer'] = detailCellRenderer, and passing in a string for 'detailCellRenderer' option to AgGrid, to register the component by name. Both option 2 and 3 were inspired by the docs here
Option4: trying to override the default cell renderer as described here, by setting the 'agGridDetailCellRenderer' in the 'components' option to my custom component.
I am just trying to get a basic custom cell rendering, and so I have taken the custom component from the docs here.
NOTE: I am using version 24.0.0 of ag-grid-react behind the scenes; the AGGrid component is just a simple wrapper that loads all of the enterprise version modules and some default props and such, and forwards everything to AGGridReact
Code for the various options below:
Option 1
const DetailCellRenderer = () => (
<h1 style={{ padding: '20px' }}>My Custom Detail</h1>
);
const detailCellRenderer = useMemo<any>(() => {
return DetailCellRenderer;
}, []);
const components = {
detailCellRenderer: detailCellRenderer
}
return (
<>
<Box>
<TableHeading />
<ErrorOverlay
errorMessage={ADVANCED_COMMISSION_ERROR}
isErrorMessage={!!advancedCommissionTransactionsError}
>
<Box width="100%" maxWidth="1050px" marginTop={1} className={classes.agTable}>
<Card>
<AgGrid
{...TRANSACTION_GRID_DEFAULT_PROPS}
columnDefs={ADVANCED_COMMISSION_GRID_COLS}
rowData={gridTransactionData}
onGridReady={onGridReady}
masterDetail={true}
//components={components}
detailCellRenderer={detailCellRenderer}
detailCellRendererParams={{}}
/>
Option 2
const DetailCellRenderer = () => (
<h1 style={{ padding: '20px' }}>My Custom Detail</h1>
);
const detailCellRenderer = useMemo<any>(() => {
return DetailCellRenderer;
}, []);
const components = {
detailCellRenderer: detailCellRenderer
}
return (
<>
<Box>
<ErrorOverlay
errorMessage={ADVANCED_COMMISSION_ERROR}
isErrorMessage={!!advancedCommissionTransactionsError}
>
<Box width="100%" maxWidth="1050px" marginTop={1} className={classes.agTable}>
<Card>
<AgGrid
{...TRANSACTION_GRID_DEFAULT_PROPS}
columnDefs={ADVANCED_COMMISSION_GRID_COLS}
rowData={gridTransactionData}
onGridReady={onGridReady}
masterDetail={true}
components={components}
// detailCellRenderer={detailCellRenderer}
detailCellRendererParams={{}}
/>
Option 3
const DetailCellRenderer = () => (
<h1 style={{ padding: '20px' }}>My Custom Detail</h1>
);
const detailCellRenderer = useMemo<any>(() => {
return DetailCellRenderer;
}, []);
const components = {
'detailCellRenderer': detailCellRenderer
}
return (
<>
<Box>
<TableHeading />
<ErrorOverlay
errorMessage={ADVANCED_COMMISSION_ERROR}
isErrorMessage={!!advancedCommissionTransactionsError}
>
<Box width="100%" maxWidth="1050px" marginTop={1} className={classes.agTable}>
<Card>
<AgGrid
{...TRANSACTION_GRID_DEFAULT_PROPS}
columnDefs={ADVANCED_COMMISSION_GRID_COLS}
rowData={gridTransactionData}
onGridReady={onGridReady}
masterDetail={true}
components={components}
detailCellRenderer='detailCellRenderer'
detailCellRendererParams={{}}
/>
Option 4
const DetailCellRenderer = () => (
<h1 style={{ padding: '20px' }}>My Custom Detail</h1>
);
const detailCellRenderer = useMemo<any>(() => {
return DetailCellRenderer;
}, []);
const components = {
'detailCellRenderer': detailCellRenderer
}
return (
<>
<Box>
<TableHeading />
<ErrorOverlay
errorMessage={ADVANCED_COMMISSION_ERROR}
isErrorMessage={!!advancedCommissionTransactionsError}
>
<Box width="100%" maxWidth="1050px" marginTop={1} className={classes.agTable}>
<Card>
<AgGrid
{...TRANSACTION_GRID_DEFAULT_PROPS}
columnDefs={ADVANCED_COMMISSION_GRID_COLS}
rowData={gridTransactionData}
onGridReady={onGridReady}
masterDetail={true}
components={components}
detailCellRenderer='detailCellRenderer'
detailCellRendererParams={{}}
/>
The corresponding errors for each attempt (option 1,2,3,4) are as follows:
Option 1
Option 2
Option 3
Option 4
It turns out, as per the documentation for version 24.0.0, that in order to use custom components one first needs to register it using the 'frameworkComponents' prop, like so:
const DetailCellRenderer = () => (
<h1 style={{ padding: '20px' }}>My Custom Detail</h1>
);
const detailCellRenderer = useMemo<any>(() => {
return DetailCellRenderer;
}, []);
<AgGrid
{...TRANSACTION_GRID_DEFAULT_PROPS}
columnDefs={ADVANCED_COMMISSION_GRID_COLS}
rowData={gridTransactionData}
onGridReady={onGridReady}
masterDetail={true}
detailCellRenderer={'advancedCommissionsDetailCellRenderer'}
frameworkComponents={{ advancedCommissionsDetailCellRenderer: detailCellRenderer }}
/>
This is not required in version 27, which is what the docs default to, so the example provided there is not a complete one.

React-hook-form + dynamic form: Render element upon dropdown selection

I am working in form using react-hook-form. This form use useFieldArray, it has to be dynamic.
Right now is very simple, it contains a react-select component with a few options and a textfield that get rendered depending on the option that the user select on the select component.
The problem I have is that the textfield component renders when the state updates, which is correct until I add a new group of element to the form. Since the textfield is listening to the same state it doesn't matter which select I use to render the textfield element, it gets rendered in all groups.
I am looking a way to specify which textfield should be rendered when the user change the select.
I the sandbox you can see what I have done. To reproduce the problem click on the "Add"-button and you will see two areas, each one with a select component.
When you choose "Other" in the select component a textfield appears, but not only in the area where the select was changed but in all areas.
How can I avoid that behavior?
https://codesandbox.io/s/vibrant-fast-381q0?file=/src/App.tsx
Extract:
const [isDisabled, setIsDisabled] = useState<boolean>(true);
const { control, handleSubmit, getValues } = useForm<IFormFields>({
defaultValues: {
managerialPositions: [
{
authority: 0,
chiefCategory: 0,
title: 0,
otherTitle: ""
}
]
}
});
useFieldArray implementation:
const {
fields: managerialPositionsFields,
append: managerialPositionsAppend,
remove: managerialPositionsRemove
} = useFieldArray({
name: "managerialPositions",
control
});
Here i update the state when the user select "Other title" in the select component:
const watchChange = (value?: number, i?: number) => {
let values: any = getValues();
if (values.managerialPositions[i].title === 3) {
setIsDisabled(false);
}
};
And here is where I render the button to create a new group of elements and the select component and the textfield that should be rendered if "isDisabled" is false.
{managerialPositionsFields.map((field, index) => {
return (
<Stack className="sectionContainer" key={field.id}>
<Stack horizontal horizontalAlign="space-between">
<StackItem>
<CommandBarButton
iconProps={{ iconName: "AddTo" }}
text="Add"
type="button"
onClick={() => {
managerialPositionsAppend({
authority: 0,
chiefCategory: 0,
title: 0,
otherTitle: ""
});
}}
/>
</StackItem>
</Stack>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<StackItem>
<Label className="select-label requiredIkon">Title</Label>
<Controller
control={control}
name={`managerialPositions.${index}.title`}
render={({ field: { onChange, value, ref } }) => (
<>
<Select
className="react-select-container authoritySelect"
classNamePrefix="react-select"
placeholder="Select title"
options={titelList}
id={`managerialPositions.${index}.title`}
value={
titelList.find((g) => g.value === value)
? titelList.find((g) => g.value === value)
: null
}
onChange={(val) => {
onChange(val.value);
watchChange(val.value, index);
}}
/>
{
// this input is for select validation
<input
tabIndex={-1}
autoComplete="off"
style={{ opacity: 0, height: 0 }}
value={
titelList.find((g) => g.value === value)
? titelList
.find((g) => g.value === value)
.toString()
: ""
}
required={true}
//Without this console will get an error:
onChange={() => {}}
/>
}
</>
)}
/>
</StackItem>
{!isDisabled && (
<StackItem className="">
<Controller
name={`managerialPositions.${index}.otherTitle`}
control={control}
render={({
field: { onChange, name: fieldName, value }
}) => (
<TextField
label="Other title"
name={fieldName}
onChange={(e) => {
onChange(e);
}}
value={value}
/>
)}
/>
</StackItem>
)}
</Stack>
</Stack>
);
})}

ReactJs MaterialUI - How do I test change of a select component (non-native)?

I have this simple component in my app:
import {Select, MenuItem} from '#material-ui/core';
import {useEffect, useState} from 'react';
export const CountrySelection = ({
countries,
selectedCountry,
onChange,
id,
className = ''
})=>{
const [_selectedCountry, _setSelectedCountry] = useState(null);
useEffect(()=>{
_setSelectedCountry(selectedCountry);
}, [selectedCountry]);
return(
<Select
value={(_selectedCountry && _selectedCountry.label) || ''}
onChange={onChange}
id={id}
className={'selection ' + className}
inputProps={{
'data-testid':'country-selection'
}}
>
{countries.map((c)=><MenuItem
key={c.id}
value={c.label}
>{c.label}</MenuItem>
)}
</Select>
)
}
This is my test attempt. I want to test that component keeps correct value/visual state when I change to a different option in the Select component:
afterEach(cleanup);
const setup = () => {
const countries = [
{label: "Austria", id: 0, code: 'at'},
{label: "Denmark", id: 1, code: 'dk'},
{label: "Germany", id: 2, code: 'de'}
];
const defaultCountry = countries[0];
const utils = render(
<CountrySelection
countries={countries}
selectedCountry={defaultCountry}
id="country-selection"
onChange={()=>{}}
/>
);
return {
...utils,
}
}
test('country selection has correct number of options', async()=>{
const {getAllByRole, getByText, getByTestId} = setup();
const selectEl = document.querySelector('#country-selection');
fireEvent.mouseDown(selectEl);
const options = getAllByRole('option');
expect(options.length).toBe(3);
const choice2 = getByText('Denmark');
expect(choice2.innerHTML).toBe('Denmark<span class="MuiTouchRipple-root"></span>');
const choice3 = getByText('Germany');
expect(choice3.innerHTML).toBe('Germany<span class="MuiTouchRipple-root"></span>');
// how to change the value and query it?
fireEvent.click(choice3);
const selection = getByTestId('country-selection');
expect(selection.value).toBe('Germany'); // doesn't work, value is still "Austria"
})
How do I do this with Select component? I cannot use native={true} prop.
(I am typing here just so that Stack overflow posting validation is happy with ratio of code to other text. I will type as long as it doesn't let me post my question. Sorry, humans.)
If you just want to check if a country has been selected correctly you can use userEvent to select the option like a real user would:
import { screen } from '#testing-library/react';
test('country selection has correct number of options', async()=>{
// Check that the correct number of options are displayed
const options = screen.getAllByRole('option');
expect(options.length).toBe(3);
// Select Denmark
userEvent.selectOptions(screen.getByRole('combobox'), 'dk');
// Check that Denmark is the selected country
expect((screen.getByText('Denmark') as HTMLOptionElement).selected).toBe(true);
// Select Germany
userEvent.selectOptions(screen.getByRole('combobox'), 'de');
// Check that Germany is now the selected country
expect((screen.getByText('Denmark') as HTMLOptionElement).selected).toBe(true);
})

Unable to select item when using TextField (or Select) with react-window

When using TextField component as a Select field with a large data set (1000 items), there is noticeable delay when mounting / unmounting the list.
import React from 'react';
import MenuItem from '#material-ui/core/MenuItem';
import TextField from '#material-ui/core/TextField';
const TextFieldSelect = () => {
const [value, setValue] = React.useState('');
return (
<TextField
select
onChange={newValue => setValue(newValue)}
value={value}
>
{listWith1000Items.map(item => (
<MenuItem key={index} value={index}>
`Item ${index + 1}`
</MenuItem>
)}
</TextField>
)
};
As suggested in the docs, I'm using react-window to efficiently render the large list, but as a result - I lose the functionality of being able to select the list items.
import React from 'react';
import MenuItem from '#material-ui/core/MenuItem';
import TextField from '#material-ui/core/TextField';
import { FixedSizeList } from 'react-window';
const Row = ({ index, style }) => (
<MenuItem key={index} value={index} style={style}>
`Item ${index + 1}`
</MenuItem>
)
const WindowTextFieldSelect = () => {
const [value, setValue] = React.useState('');
return (
<TextField
select
onChange={newValue => setValue(newValue)}
value={value}
>
<FixedSizedList
height={960}
width={480}
itemSize={48}
itemCount={1000}
>
{Row}
</FixedSizedList>
</TextField>
)
};
I expect that when I select an item, the value gets updated.
As recommended in this other stack overflow response, i started to use Autocomplete component and performance improves drastically.
Official Docs Autocomplete example: https://codesandbox.io/s/u09wym?file=/demo.js