I want to use react-select as a custom cell editor for ag-grid
Here is the code sandbox link
You can download the source here
npm install
npm start
I've removed all the css so it looks so plain, but it still does the job.
Here is my package.json
{
"name": "Test",
"version": "1.5.0",
"private": true,
"dependencies": {
"react": "16.8.1",
"react-dom": "16.8.1",
"react-select": "^2.4.1",
"react-scripts": "2.1.5",
"ag-grid-community": "20.1.0",
"ag-grid-react": "20.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"deploy": "npm run build",
"lint:check": "eslint . --ext=js,jsx; exit 0",
"lint:fix": "eslint . --ext=js,jsx --fix; exit 0",
"install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install && npm start"
},
"optionalDependencies": {
"@types/googlemaps": "3.30.16",
"@types/markerclustererplus": "2.1.33",
"ajv": "6.9.1",
"prettier": "1.16.4"
},
"devDependencies": {
"eslint-config-prettier": "4.0.0",
"eslint-plugin-prettier": "3.0.1"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import Grid from './app/AgGrid'
const colDefs = [
{
headerName : "Test",
field : "test",
editor : "manyToOne",
editable : true,
suppressKeyboardEvent : function suppressEnter(params) {
let KEY_ENTER = 13;
let KEY_LEFT = 37;
let KEY_UP = 38;
let KEY_RIGHT = 39;
let KEY_DOWN = 40;
var event = params.event;
var key = event.which;
var editingKeys = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_ENTER];
var suppress = params.editing && editingKeys.indexOf(key) >= 0;
console.log("suppress "+suppress);
return suppress;
},
cellEditor : "reactSelectCellEditor",
},
]
ReactDOM.render(
<Grid
readOnly={false}
field={null}
columnDefs={colDefs}
editable={true}
editingMode={null}
rowData={[]}
/>
,
document.getElementById("root")
);
AgGrid.js
import React, { Component } from 'react'
import { AgGridReact } from 'ag-grid-react'
import 'ag-grid-community/dist/styles/ag-grid.css'
import ReactSelectCellEditor from './ReactSelectCellEditor'
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
columnDefs : props.columnDefs,
rowData : [],
isFormOpen : false,
editedData : {
data : [],
rowIndex : null,
columns : []
},
frameworkComponents:{
'reactSelectCellEditor': ReactSelectCellEditor,
},
}
}
updateRowData = (newData,index) => {
var unsavedData = this.state.rowData.map((row,i)=>{
if (i === index) {
return newData
}
else{
return row
}
})
this.setState({rowData:unsavedData})
}
onGridReady = params => {
}
createData = () => {
let fields = this.state.columnDefs.map(column=>{
return column.field
})
let newData = {}
fields.map(field=>{
if (field !== "id") {
newData[field] = ""
}
})
newData["delete"] = false
this.setState({rowData:[...this.state.rowData,newData]})
}
toggleModal = () => {
this.setState({isFormOpen:!this.state.isFormOpen})
}
setEditedData = (data) => {
console.log("editedData",data)
this.setState({editedData:data})
}
render() {
var defaultColDef = {
editable:this.state.editable,
sortable:false
}
var initColDefs = this.state.columnDefs.map(colDef=>{
return {
...colDef,
cellEditorParams:{
...colDef.cellEditorParams,
}
}
})
var renderedColumnDefs = []
if (this.props.editingMode === "inline") {
renderedColumnDefs = [
...initColDefs
]
}
else if (this.props.readOnly) {
renderedColumnDefs = [...initColDefs]
}
else{
defaultColDef = {
editable:false,
sortable:false
}
renderedColumnDefs = [
...initColDefs,
]
}
var renderedRowData = []
this.state.rowData.map(row=>{
if (row.delete !== true) {
renderedRowData.push(row)
}
})
return (
<React.Fragment>
<AgGridReact
defaultColDef = {defaultColDef}
columnDefs = {renderedColumnDefs}
rowData = {renderedRowData}
onGridReady = {this.onGridReady}
onCellValueChanged = {this.handleChange}
frameworkComponents = {this.state.frameworkComponents}
singleClickEdit = {true}
stopEditingWhenGridLosesFocus = {true}
reactNext={true}
/>
<div className="addButton" style={{padding:'5px', border: '1px solid black'}} onClick={()=>this.createData()}>Add</div>
</React.Fragment>
);
}
}
export default Grid
And ReactSelectCellEditor.js
import React, { Component } from 'react'
import CreatableSelect from "react-select/lib/Creatable"
import { components } from 'react-select'
import ReactDOM from 'react-dom'
const colourOptions = [
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' },
{ value: 'yellow', label: 'Yellow' }
]
class ReactSelectCellEditor extends Component {
constructor(props) {
super(props);
this.state = {
value : null
}
this.handleChange = this.handleChange.bind(this)
}
componentDidMount() {
}
handleCreateOption = (inputValue:any) => {
}
handleChange(selected){
this.setState({
value : selected.value
})
}
afterGuiAttached(param) {
let self = this;
this.Ref.addEventListener('keydown', function (event) {
// let key = event.which || event.keyCode
// if (key === 37 || key === 38 || key === 39 || key === 40 || key === 13) {
// event.stopPropagation();
// console.log("onKeyDown "+key);
// }
});
this.SelectRef.focus()
}
formatCreate = (inputValue) => {
return (<p> Add: {inputValue}</p>);
};
isPopup(){
return true
}
getValue() {
return this.state.value
}
render() {
return (
<div ref = { ref => { this.Ref = ref }} style={{width: '200px'}}>
<CreatableSelect
onChange = {this.handleChange}
options = {colourOptions}
formatCreateLabel = {this.formatCreate}
createOptionPosition = {"first"}
ref = { ref => { this.SelectRef = ref }}
/>
</div>
);
}
}
export default ReactSelectCellEditor
As you can see, a simple bare bone combined implementation between ag-grid and popped up (not in cell) react-select.
It works fine, except the default navigation key event of ag-grid hinders react-select from working flawlessly.
Now I've read about the solutions to this here in ag-grid documentation of keyboard navigation while editing
Here is the thing:
Firstly, I have tried solution 1 which is to stop event propagation of my custom cell editor. It works, but strangely although I stop the event bubbling in the container of react-select, not the react-select itself, as you can see from the commented out script in ReactSelectCellEditor.js, somehow React Select Key Event also stops executing. I don't know why react select event which is a child element of the div stops executing when I stop the event propagation in the parent element. Isn't the event supposed to bubble up from child to parent not the other way around? So first solution is a no go.
As for the second solution the one that is strange is ag-grid itself. As the documentation says, ag-grid allows us to intercept the key event by defining a function for suppressKeyboardEvent from the column definition. It works if the editing mode is not popup. But in my case, I use the popup mode, and in popup mode I find the suppressKeyboardEvent isn't triggered while editing, it isn't even called at all.
Now I'm in a deadlock. In the first solution the react-select behaves weirdly and in the second one it is the ag-grid that behaves weirdly. How do I solve this?
After searching through the internet it looks like React Select uses SyntheticKeyboardEvent
which, if I'm not wrong, executes AFTER events have already gone through a first capture/bubbling cycle across the DOM tree. So if I stop the propagation SyntheticKeyboardEvent
won't be triggered. But if I don't stop the propagation ag-grid which uses the native event listener will trigger its default navigation function and kill the react-select component. Now is this it for me? A dead end?
Let me warn you this solution is a hack and an anti pattern. As you have stated react select uses SyntheticKeyboardEvent and it waits until the native event goes through the cycle across the DOM tree. But there is a way for you to call react select functionalities by modifying the code which will allow you to access the methods of react select component from other component which is why this is an anti pattern solution.
Remove suppressKeyboardEvent
from colDef
Make a copy of CreatableSelect
component from the node_modules/react-select/src/Creatable.js
Add these lines to the copied component to get access through onRef
componentDidMount() {
this.props.onRef(this)
}
componentWillUnmount() {
this.props.onRef(undefined)
}
Pass the control methods of react select by defining these in the copy
focusOption(param) {
this.select.focusOption(param);
}
selectOption(param) {
this.select.selectOption(param);
}
Then when you render the component add onRef = { ref => { this.SelectRef = ref }}
Now you will be able to call these to simulate the control before you stop the propagation
this.SelectRef.focusOption('up');
this.SelectRef.focusOption('down');
You can gain access to react select's states by writing a method like this
focusedOption() {
return this.select.state.focusedOption;
}
Complete the rest of the simulation by referencing to node_modules/react-select/src/Select.js line 1142
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With