ag-grid react how to return value from external popup component - reactjs

We're trying to use ag-grid to display and edit different types of data, for example a color. We have a react component that displays a set of colors for the user to choose from, with OK and CANCEL buttons that close the dialog (it's a MUI Dialog component) and calls a callback function prop with the selected color (if OK clicked).
We are able to get this dialog to display from the ag-grid cell, but can't figure out how to get the color returned to the cell. Currently the color dialog is defined as
const ColorChooser = ({initialColor, callback}) => {
const [color, setColor] = useState(initialColor);
const [open, setOpen] = useState(true);
and the OK button handler is simply
const okHandler = () => {
if (callback) {
callback(color);
}
setOpen(false);
}
where the open state opens / closes the MUI Dialog.
In another component we are able to display the dialog from the ag-grid based on the 'type' of the data in the grid cell:
//
// Return the appropriate editor based on the cell contents
const selectEditor = (params) => {
if (params.data.dataType === 'string') {
return (
{
component: 'agTextCellEditor',
params: params,
}
)
}
else if (params.data.dataType === 'color') {
return (
{
component: ColorChooser,
params: params,
popup: true,
popupPosition: 'under'
}
)
}
}
const [columnDefs] = useState([
{ field: 'property', lockPosition: 'left', resizable: true, sortable: true, flex: 1 },
{ field: 'value', editable: true, flex: 1, cellEditorSelector: selectEditor, cellRendererSelector: selectRenderer }
]);
and this is where we're stumped. The color dialog displays but we can't figure out how to get the selection back to the cell when the OK button is clicked.
In case it's not obvious I'm fairly new to react and ag-grid, apologies if this is too elementary. Thanks!

Related

AGGrid React Custom Header Component - Updating state

I've taken over a React project and I'm fairly new to the library. I'm attempting to implement AG-Grid and what I think is a relatively simple custom header that renders the column name and an icon. When the icon is clicked, I need the icon to change and update the state of the custom header components parent.
My issue is that the click event works once and updates the state in the parent, but the state doesn't propagate back down to the custom header component and therefore the icon doesn't switch. Not sure if this is an AG Grid issue or a React issue.
My Custom Header Component
const WeightHeaderComponent = (params) => {
const handleClick = () => {
params.weightModeCallback()
}
return (
<span onClick={handleClick} style={{cursor: 'pointer'}}>
Weight<i className={`${params.weightModeIsPercent ? 'fa fa-percent cn-blue' : 'fa fa-tally cn-blue'}`} style={{marginLeft: '3px'}}></i>
</span>
);
};
The AG Grid column def using the custom header component:
const [columnDefs] = useState([
...
{
headerName: 'Weight',
field: 'weight',
type: 'numericColumn',
headerTooltip:
'The weight of an assignment category determines the impact an assignment will have over your overall grade.',
width: 100,
defaultMinWidth: 100,
headerComponentParams: { weightModeIsPercent: weightModeIsPercent, weightModeCallback: updateWeightMode },
headerComponent: WeightHeaderComponent
},
...
])
The relevant state hook:
const [weightModeIsPercent, setWeightModeIsPercent] = useState(true);
And finally my callback function in the parent
function updateWeightMode () {
setWeightModeIsPercent(!weightModeIsPercent);
}
After an hour of so of doc reading and head-desk beating I figured it out. I was attempting to do this in a very roundabout way when AG Grid offers a nice little api which happens to have some functions for exactly this type of thing: updating your columnDefs.
Specifically: setColumnDefs(newDefs).
My code accepts an updated parameter with which I update the columnDefs for the column I want to update.
function updateWeightMode (updatedWeightModeBool) {
const newDefs = columnDefs.map((def) => {
if (def.field === 'weight') {
return {
...def,
headerComponentParams: {
weightModeIsPercent: updatedWeightModeBool,
weightModeCallback: updateWeightMode
}
};
}
return def;
});
weightsGridRef.current.api.setColumnDefs(newDefs);
}
So, this wasn't really an issue with state (though I'm still updating the parents state for other uses).
Try using
api.refreshHeader();

How to show Dropdown Menu when Right-Clicking on Table Row?

How do I open a dropdown menu when a row is right-clicked in a table?
I've tried using Table's onRow onContextMenu: event => { setVisible(true); } function with dropdown outside the table <Dropdown overlay={menu} visible={visible}><Table>...</Table></Dropdown>, but the dropdown menu opens at the bottom of the table, not where the mouse is.
I've found a working demo sandbox but its with ant design 3.0.0 and doesn't work with the latest version.
First, add visible state in your component:
const [visible, setVisible] = useState(false);
Then pass the visible state into Dropdown component that use contextMenu as trigger and your Menu component as the overlay:
<Dropdown overlay={menu} visible={visible} trigger={["contextMenu"]}>
<Table ... />
</Dropdown>
Add states that will keep position and value of our clicked cell:
const [posX, setPosX] = useState(0);
const [posY, setPosY] = useState(0);
const [value, setValue] = useState("");
Next, use onRow attribute where we pass a Function that return onContextMenu and onClick Function at your Table component.
We will receive an event at onContextMenu by which we get the clicked position and use onClick to hide the menu
<Table
onRow={(_record, _rowIndex) => {
return {
onContextMenu: (event) => {
event.preventDefault();
// grab and keep the clicked position
const { clientX, clientY, target } = event;
setPosX(clientX);
setPosY(clientY);
// grab the clicked cell value
setValue(target.innerHTML);
// show the overlay Menu
setVisible(true);
},
// hide the overlay Menu on click
onClick: () => setVisible(false)
};
}}
...
/>
And finally, the overlay Menu positioning. Here we use inline style to set the position. Since Dropdown component default positioning is bottomLeft so the position will be at:
left: 0 // left side
top: n // where n is the bottom of the table which is equal to the height of your table
This is why your dropdown menu opens at the bottom of the table.
To fix this, we need to set the top value as:
// the formula
topPosition = clientY - tableHeight - menuHeight;
// in our example we get:
topPosition = clientY - 220 - 72
= clientY - 292;
So the clicked position becomes:
left: clientX,
top: clientY - 292
and at the menu we set by using inline style as follow:
const menu = (
<Menu
style={{ top: posY - 292, left: posX }}
onClick={onClick}
items={[
{
key: "1",
label: "Action 1"
},
{
key: "2",
label: "Action 2"
}
]}
/>
);
This is a minimal example:
Additional Info
If you need to access the row value from the menu, you can get the information from record props at onRow event handler and grab the value at onContextMenu event handler. So, when you do a right click at each row, the record value will be set as rowRecord state that could be used inside Menu component that is rendered inside a useMemo hook later. Since the menu now have been memoized, now the onClick method have to use useCallback hook too.
const [rowRecord, setRowRecord] = useState();
...
const onClick = useCallback(
(_item) => {
console.log(value);
setVisible(false);
},
[value]
);
...
const menu = useMemo(() => {
console.log(rowRecord);
return (
<Menu
style={{ top: posY - 292, left: posX }}
onClick={onClick}
items={[
{
key: "1",
label: rowRecord?.name
},
{
key: "2",
label: rowRecord?.email
}
]}
/>
);
}, [onClick, posX, posY, rowRecord]);
...
<Table
onRow={(record, _rowIndex) => {
return {
onContextMenu: (event) => {
event.preventDefault();
...
setRowRecord(record);
},
...
};
}}
...
/>
Actually, the rowRecord state could be used inside our component immediately after we set the record value by using setRowRecord setter. But again, it depends on your own preference.

How to disable the button once clicked in antd table?

Getting the table values through props. The claim update function is working fine, but the record still there. once I refresh or select another tab only after that the claimed record will be removed.
{
title: 'Action',
dataIndex: 'action',
render: (text, record) =>
<Button class="claimBom-btn" onClick={(e) => this.handleClaim(e,text, record)} ><Icon type="plus-circle" />Claim</Button>
}
This the button call for all record in the table
Just pass along the button state in the datasource.
When it is clicked mutate the datasource.
const [datasource, setDatasource] = useState([
{
disabled: false,
// others properties
}
]);
// On button click find the data and change disabled property
const onClick = (id) => {
const source = datasource.find(source => source.id === id);
source.disabled = true;
setDatasource(datasource);
}
{
title: 'Action',
dataIndex: 'action',
render: (text, record) =>
<Button disabled={text.disabled} class="claimBom-btn" onClick={(e) => this.handleClaim(e,text, record)} ><Icon type="plus-circle" />Claim</Button>
}
You can simply pass the disabled prop to the button accordingly. When calling this.handleClaim, set a state of which button should be disabled while handling, and pass that as a disabled prop to the button.

Doing validation on certain columns in React-Data-Grid

I want to implement validation on react-data-grid table, in which only cells which i defined in the column itself will be validated, and validation only occurs if the state is set to true.
So i have the following codes here
const [ validateMode, setValidateMode ] = useState(false);
const errorBackground = { backgroundColor: 'some error color in here' }
const DescriptionFormatter = props => {
const { value, row } = props;
let template;
if (!validateMode){
template = value;
}
if (validateMode){
let validationStyling = Boolean(value.length) ? {} : errorBackground;
template = <div style={{ ...validationStyling }}>value</div>
}
return template;
}
const tableCols = [
{
key: 'id',
name: 'No'
},
{
key: 'name',
name: 'Name',
editable: !disabled,
editor: <AutoComplete options={nameList} />
},
{
key: 'description',
name: 'Description',
editable: !disabled,
formatter: DescriptionFormatter
}
];
When button is pressed, it will trigger the validateMode state to true, and when that happen, i want the description column to validate the cell and return error background cell if value is empty, otherwise default background if there's value in it (simple checking if value is empty or not).
I can't seem to get the validateMode console log working when i press the button. Its only showing on page init, or when the cell changes (like when inserting value to it).I cannot get the validation working in here, unless if i do it wrongly.
Please help me on this.
Can't believe im answering my own question lol
Anyway i found the answer already. Validation cannot be done at the columns definition. It has to be done on the cell level of the datagrid. You can pass states to the cell and do your validation there, and change the CSS of the cell by using conditional className to show if validation is true or false.
Snippets of the codes to make it work
import ReactDataGrid, { Row, Cell } from "react-data-grid";
/*
Validator is a state passed from the parent page that indicates whether page is on validation or not, in case
if dev want the table to validate when a trigger hits the page (like submit button?)
*/
const { validator } = props;
const CustomCell = props => {
let valueValidation = false;
if (validator){
/* Insert custom validation in here */
return ( <Cell {...props} className={ validator && valueValidation ? '' : classes.error } /> );
}
/* If not in validation mode, display normal rows */
if (!validator){
return ( <Cell {...props} /> );
}
}
const CustomRow = props => {
return ( <Row {...props} cellRenderer={cellProps => <CustomCell {...cellProps} />} /> );
}
return (
<ReactDataGrid
rowRenderer={CustomRow}
/>
);

How can I display a Persona component in a CommandBar in React Fabric-UI?

I am trying to display a Persona component on the far right of my CommandBar component, which I use as a header for my application.
Here's a code snippet
const getFarItems = () => {
return [
{
key: 'profile',
text: <Persona text="Kat Larrson" />,
onClick: () => console.log('Sort')
}
]
}
const FabricHeader: React.SFC<props> = () => {
return (
<div>
<CommandBar
items={getItems()}
farItems={getFarItems()}
ariaLabel={'Use left and right arrow keys to navigate between commands'}
/>
</div>
);
}
This throws a type error because the text prop expects a string and not a component. Any help would be appreciated!
Under the ICommandBarItemProps there is a property called commandBarButtonAs that the docs state:
Method to override the render of the individual command bar button.
Note, is not used when rendered in overflow
And its default component is CommandBarButton which is basically a Button
Basically there are two ways to do this.
Keep using Button, and apply your own renderer. Basically the IButtonProps you can add onRenderChildren which would allow you to add any Component such as Persona to render. This example would show you how it is done https://codepen.io/micahgodbolt/pen/qMoYQo
const farItems = [{
// Set to null if you have submenus that want to hide the down arrow.
onRenderMenuIcon: () => null,
// Any renderer
onRenderChildren: () => ,
key: 'persona',
name: 'Persona',
iconOnly: true,
}]
Or add your own crazy component not dependent on CommandBarButton but that means you need to handle everything like focus, accessibility yourself. This example would show you how it is done https://codepen.io/mohamedhmansour/pen/GPNKwM
const farItems = [
{
key: 'persona',
name: 'Persona',
commandBarButtonAs: PersonaCommandBarComponent
}
];
function PersonaCommandBarComponent() {
return (
);
}

Resources