From 8bc7ecc8274f84197d4af17bad69600c2bda4f4b Mon Sep 17 00:00:00 2001 From: MBelniak Date: Wed, 22 Jan 2025 17:07:06 +0100 Subject: [PATCH 1/2] Call callback on cell edit completion in tree table, document issue with updating cells --- components/doc/common/apidoc/index.json | 7 + components/doc/datatable/edit/celleditdoc.js | 140 +++++++++++------- components/doc/treetable/editdoc.js | 64 ++++---- components/lib/datatable/BodyCell.js | 34 ++--- components/lib/treetable/TreeTableBodyCell.js | 82 ++++++++-- 5 files changed, 207 insertions(+), 120 deletions(-) diff --git a/components/doc/common/apidoc/index.json b/components/doc/common/apidoc/index.json index 3a6424de0f..7fd813216c 100644 --- a/components/doc/common/apidoc/index.json +++ b/components/doc/common/apidoc/index.json @@ -12702,6 +12702,13 @@ "readonly": false, "type": "any", "description": "New value of the element." + }, + { + "name": "props", + "optional": false, + "readonly": true, + "type": "any", + "description": "All props received by cell component." } ] }, diff --git a/components/doc/datatable/edit/celleditdoc.js b/components/doc/datatable/edit/celleditdoc.js index 77b2d6cf76..5368c0af4f 100644 --- a/components/doc/datatable/edit/celleditdoc.js +++ b/components/doc/datatable/edit/celleditdoc.js @@ -38,28 +38,31 @@ export function CellEditDoc(props) { }; const onCellEditComplete = (e) => { - let { rowData, newValue, field, originalEvent: event } = e; - - switch (field) { - case 'quantity': - case 'price': - if (isPositiveInteger(newValue)) { - rowData[field] = newValue; - } else { - event.preventDefault(); - } - - break; - - default: - if (newValue.trim().length > 0) { - rowData[field] = newValue; - } else { - event.preventDefault(); - } - - break; - } + setProducts((products) => { + let { rowData, newValue, field, originalEvent: event } = e; + let newProducts = JSON.parse(JSON.stringify(products)); + + switch (field) { + case 'quantity': + case 'price': + if (isPositiveInteger(newValue)) { + newProducts.find((row) => row.id === rowData.id)[field] = newValue; + } else { + event.preventDefault(); + } + + break; + + default: + if (newValue.trim().length > 0) { + newProducts.find((row) => row.id === rowData.id)[field] = newValue; + } else { + event.preventDefault(); + } + + break; + } + }); }; const cellEditor = (options) => { @@ -71,11 +74,11 @@ export function CellEditDoc(props) { }; const textEditor = (options) => { - return options.editorCallback(e.target.value)} onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(e.target.value)} />; }; const priceEditor = (options) => { - return options.editorCallback(e.value)} mode="currency" currency="USD" locale="en-US" onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(e.value)} mode="currency" currency="USD" locale="en-US" />; }; const priceBodyTemplate = (rowData) => { @@ -130,20 +133,31 @@ export default function CellEditingDemo() { }; const onCellEditComplete = (e) => { - let { rowData, newValue, field, originalEvent: event } = e; - - switch (field) { - case 'quantity': - case 'price': - if (isPositiveInteger(newValue)) rowData[field] = newValue; - else event.preventDefault(); - break; - - default: - if (newValue.trim().length > 0) rowData[field] = newValue; - else event.preventDefault(); - break; - } + setProducts((products) => { + let { rowData, newValue, field, originalEvent: event } = e; + let newProducts = JSON.parse(JSON.stringify(products)); + + switch (field) { + case 'quantity': + case 'price': + if (isPositiveInteger(newValue)) { + newProducts.find((row) => row.id === rowData.id)[field] = newValue; + } else { + event.preventDefault(); + } + + break; + + default: + if (newValue.trim().length > 0) { + newProducts.find((row) => row.id === rowData.id)[field] = newValue; + } else { + event.preventDefault(); + } + + break; + } + }); }; const cellEditor = (options) => { @@ -152,11 +166,11 @@ export default function CellEditingDemo() { }; const textEditor = (options) => { - return options.editorCallback(e.target.value)} onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(e.target.value)} />; }; const priceEditor = (options) => { - return options.editorCallback(e.value)} mode="currency" currency="USD" locale="en-US" onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(e.value)} mode="currency" currency="USD" locale="en-US" />; }; const priceBodyTemplate = (rowData) => { @@ -230,20 +244,31 @@ export default function CellEditingDemo() { }; const onCellEditComplete = (e: ColumnEvent) => { - let { rowData, newValue, field, originalEvent: event } = e; - - switch (field) { - case 'quantity': - case 'price': - if (isPositiveInteger(newValue)) rowData[field] = newValue; - else event.preventDefault(); - break; - - default: - if (newValue.trim().length > 0) rowData[field] = newValue; - else event.preventDefault(); - break; - } + setProducts((products) => { + let { rowData, newValue, field, originalEvent: event } = e; + let newProducts = JSON.parse(JSON.stringify(products)); + + switch (field) { + case 'quantity': + case 'price': + if (isPositiveInteger(newValue)) { + newProducts.find((row) => row.id === rowData.id)[field] = newValue; + } else { + event.preventDefault(); + } + + break; + + default: + if (newValue.trim().length > 0) { + newProducts.find((row) => row.id === rowData.id)[field] = newValue; + } else { + event.preventDefault(); + } + + break; + } + }); }; const cellEditor = (options: ColumnEditorOptions) => { @@ -252,11 +277,11 @@ export default function CellEditingDemo() { }; const textEditor = (options: ColumnEditorOptions) => { - return ) => options.editorCallback(e.target.value)} onKeyDown={(e) => e.stopPropagation()} />; + return ) => options.editorCallback(e.target.value)} />; }; const priceEditor = (options: ColumnEditorOptions) => { - return options.editorCallback(e.value)} mode="currency" currency="USD" locale="en-US" onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(e.value)} mode="currency" currency="USD" locale="en-US" />; }; const priceBodyTemplate = (rowData: Product) => { @@ -295,7 +320,8 @@ export default function CellEditingDemo() { <>

- Cell editing is enabled by setting editMode as cell, defining input elements with editor property of a Column and implementing onCellEditComplete to update the state. + Cell editing is enabled by setting editMode as cell, defining input elements with editor property of a Column and implementing onCellEditComplete to update the state. When updating the state from + React's useState hook, use a callback format to always work on the up-to-date state.

diff --git a/components/doc/treetable/editdoc.js b/components/doc/treetable/editdoc.js index 447107604c..794d89ffbe 100644 --- a/components/doc/treetable/editdoc.js +++ b/components/doc/treetable/editdoc.js @@ -13,13 +13,15 @@ export function EditDoc(props) { NodeService.getTreeTableNodes().then((data) => setNodes(data)); }, []); // eslint-disable-line react-hooks/exhaustive-deps - const onEditorValueChange = (options, value) => { - let newNodes = JSON.parse(JSON.stringify(nodes)); - let editedNode = findNodeByKey(newNodes, options.node.key); + const onEditComplete = (event) => { + setNodes((nodes) => { + let newNodes = JSON.parse(JSON.stringify(nodes)); + let editedNode = findNodeByKey(newNodes, event.props.node.key); - editedNode.data[options.field] = value; + editedNode.data[event.field] = event.newValue; - setNodes(newNodes); + return newNodes; + }); }; const findNodeByKey = (nodes, key) => { @@ -37,7 +39,7 @@ export function EditDoc(props) { }; const inputTextEditor = (options) => { - return onEditorValueChange(options, e.target.value)} onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(event.target.value)} />; }; const sizeEditor = (options) => { @@ -49,8 +51,7 @@ export function EditDoc(props) { }; const requiredValidator = (e) => { - let props = e.columnProps; - let value = props.node.data[props.field]; + let value = e.newValue; return value && value.length > 0; }; @@ -77,13 +78,14 @@ export default function EditDemo() { NodeService.getTreeTableNodes().then((data) => setNodes(data)); }, []); - const onEditorValueChange = (options, value) => { - let newNodes = JSON.parse(JSON.stringify(nodes)); - let editedNode = findNodeByKey(newNodes, options.node.key); - - editedNode.data[options.field] = value; + const onEditComplete = (event) => { + setNodes((nodes) => { + let newNodes = JSON.parse(JSON.stringify(nodes)); + let editedNode = findNodeByKey(newNodes, event.props.node.key); - setNodes(newNodes); + editedNode.data[event.field] = event.newValue; + return newNodes; + }); }; const findNodeByKey = (nodes, key) => { @@ -101,7 +103,7 @@ export default function EditDemo() { }; const inputTextEditor = (options) => { - return onEditorValueChange(options, e.target.value)} onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(event.target.value)} />; }; const sizeEditor = (options) => { @@ -123,8 +125,8 @@ export default function EditDemo() {
- - + +
); @@ -145,13 +147,14 @@ export default function EditDemo() { NodeService.getTreeTableNodes().then((data) => setNodes(data)); }, []); - const onEditorValueChange = (options: ColumnEditorOptions, value: string) => { - let newNodes = JSON.parse(JSON.stringify(nodes)); - let editedNode = findNodeByKey(newNodes, options.node.key); + const onEditComplete = (event) => { + setNodes((nodes) => { + let newNodes = JSON.parse(JSON.stringify(nodes)); + let editedNode = findNodeByKey(newNodes, event.props.node.key); - editedNode.data[options.field] = value; - - setNodes(newNodes); + editedNode.data[event.field] = event.newValue; + return newNodes; + }); }; const findNodeByKey = (nodes: TreeNode[], key: string) => { @@ -169,7 +172,7 @@ export default function EditDemo() { }; const inputTextEditor = (options: ColumnEditorOptions) => { - return ) => onEditorValueChange(options, e.target.value)} onKeyDown={(e) => e.stopPropagation()} />; + return options.editorCallback(event.target.value)} />; }; const sizeEditor = (options: ColumnEditorOptions) => { @@ -181,7 +184,7 @@ export default function EditDemo() { }; const requiredValidator = (e: ColumnEvent) => { - let props = e.columnProps; + let props = e.props; let value = props.node.data[props.field]; return value && value.length > 0; @@ -191,8 +194,8 @@ export default function EditDemo() {
- - + +
); @@ -267,14 +270,15 @@ export default function EditDemo() { <>

- Incell editing is enabled by defining input elements with editor property of a Column. + Incell editing is enabled by defining input elements with editor property of a Column and implementing onCellEditComplete to update the state. When updating the state from React's useState hook, use a callback format + to always work on the up-to-date state.

- - + +
diff --git a/components/lib/datatable/BodyCell.js b/components/lib/datatable/BodyCell.js index c0f4973af6..e654cfdfcf 100644 --- a/components/lib/datatable/BodyCell.js +++ b/components/lib/datatable/BodyCell.js @@ -66,13 +66,11 @@ export const BodyCell = React.memo((props) => { const [bindDocumentClickListener, unbindDocumentClickListener] = useEventListener({ type: 'click', listener: (e) => { - setTimeout(() => { - if (!selfClick.current && isOutsideClicked(e.target)) { - // #2666 for overlay components and outside is clicked + if (!selfClick.current && isOutsideClicked(e.target)) { + // #2666 for overlay components and outside is clicked - switchCellToViewMode(e, true); - } - }, 0); + switchCellToViewMode(e, true); + } selfClick.current = false; }, @@ -179,7 +177,7 @@ export const BodyCell = React.memo((props) => { let valid = true; - if ((!submit || cellEditValidateOnClose()) && cellEditValidator) { + if ((submit || cellEditValidateOnClose()) && cellEditValidator) { valid = cellEditValidator(params); } @@ -193,7 +191,7 @@ export const BodyCell = React.memo((props) => { event.preventDefault(); } - setEditingRowDataState(newRowData); + setEditingRowDataState(valid && submit ? newRowData : props.rowData); }; const findNextSelectableCell = (cell) => { @@ -367,17 +365,19 @@ export const BodyCell = React.memo((props) => { }; const onKeyDown = (event) => { - if (props.editMode !== 'row') { - if (event.code === 'Enter' || event.code === 'NumpadEnter' || event.code === 'Tab') { - switchCellToViewMode(event, true); - } + if (editingState) { + event.stopPropagation(); - if (event.code === 'Escape') { - switchCellToViewMode(event, false); - } - } + if (props.editMode !== 'row') { + if (event.code === 'Enter' || event.code === 'NumpadEnter' || event.code === 'Tab') { + switchCellToViewMode(event, true); + } - if (props.allowCellSelection) { + if (event.code === 'Escape') { + switchCellToViewMode(event, false); + } + } + } else if (props.allowCellSelection) { const { target, currentTarget: cell } = event; switch (event.code) { diff --git a/components/lib/treetable/TreeTableBodyCell.js b/components/lib/treetable/TreeTableBodyCell.js index 1f07244390..bd803b05f0 100644 --- a/components/lib/treetable/TreeTableBodyCell.js +++ b/components/lib/treetable/TreeTableBodyCell.js @@ -6,6 +6,8 @@ import { DomHandler, ObjectUtils, classNames } from '../utils/Utils'; export const TreeTableBodyCell = (props) => { const [editingState, setEditingState] = React.useState(false); + const [editingRowDataState, setEditingRowDataState] = React.useState(props.rowData); + const editingRowDataStateRef = React.useRef(null); const elementRef = React.useRef(null); const keyHelperRef = React.useRef(null); const selfClick = React.useRef(false); @@ -25,7 +27,8 @@ export const TreeTableBodyCell = (props) => { parent: props.metaData, hostName: props.hostName, state: { - editing: editingState + editing: editingState, + editingRowData: editingRowDataState }, context: { index: props.index, @@ -71,9 +74,11 @@ export const TreeTableBodyCell = (props) => { const [bindDocumentClickListener, unbindDocumentClickListener] = useEventListener({ type: 'click', listener: (e) => { - if (!selfClick.current && isOutsideClicked(e.target)) { - switchCellToViewMode(e); - } + setTimeout(() => { + if (!selfClick.current && isOutsideClicked(e.target)) { + switchCellToViewMode(e, true); + } + }, 0); selfClick.current = false; }, @@ -100,6 +105,8 @@ export const TreeTableBodyCell = (props) => { } setEditingState(true); + setEditingRowDataState(props.rowData); + editingRowDataStateRef.current = props.rowData; const onCellEditInit = getColumnProp('onCellEditInit'); @@ -127,8 +134,16 @@ export const TreeTableBodyCell = (props) => { }; const onKeyDown = (event) => { - if (event.which === 13 || event.which === 9) { - switchCellToViewMode(event); + if (editingState) { + event.stopPropagation(); + + if (event.which === 13 || event.which === 9) { + // Enter or Tab + switchCellToViewMode(event, true); + } else if (event.which === 27) { + // Escape + switchCellToViewMode(event, false); + } } }; @@ -142,7 +157,9 @@ export const TreeTableBodyCell = (props) => { setEditingState(false); unbindDocumentClickListener(); OverlayService.off('overlay-click', overlayEventListener.current); + editingRowDataStateRef.current = null; overlayEventListener.current = null; + selfClick.current = false; }, 1); }; @@ -150,19 +167,44 @@ export const TreeTableBodyCell = (props) => { onClick(event); }; - const switchCellToViewMode = (event) => { - if (props.cellEditValidator) { - let valid = props.cellEditValidator({ - originalEvent: event, - columnProps: props - }); + const switchCellToViewMode = (event, submit) => { + const onCellEditComplete = getColumnProp('onCellEditComplete'); + const onCellEditCancel = getColumnProp('onCellEditCancel'); + const cellEditValidator = getColumnProp('cellEditValidator'); + const newRowData = { ...editingRowDataStateRef.current }; + const newValue = resolveFieldData(newRowData); + const params = { ...getCellCallbackParams(event), newRowData, newValue }; + + if (!submit && onCellEditCancel) { + onCellEditCancel(params); + } + + let valid = true; + + if (submit && cellEditValidator) { + valid = cellEditValidator(params); + } - if (valid) { - closeCell(); + if (valid) { + if (submit && onCellEditComplete) { + onCellEditComplete(params); } - } else { + closeCell(); + } else { + event.preventDefault(); } + + setEditingRowDataState(newRowData); + }; + + const editorCallback = (val) => { + let editingRowData = { ...editingRowDataState }; + + ObjectUtils.mutateFieldData(editingRowData, field, val); + setEditingRowDataState(editingRowData); + + editingRowDataStateRef.current = editingRowData; }; const isSelected = () => { @@ -206,7 +248,15 @@ export const TreeTableBodyCell = (props) => { if (editingState) { if (columnEditor) { - content = ObjectUtils.getJSXElement(columnEditor, { node: props.node, rowData: props.rowData, value: ObjectUtils.resolveFieldData(props.node.data, props.field), field: props.field, rowIndex: props.rowIndex, props }); + content = ObjectUtils.getJSXElement(columnEditor, { + node: props.node, + rowData: editingRowDataState, + value: ObjectUtils.resolveFieldData(editingRowDataState, props.field), + field: props.field, + rowIndex: props.rowIndex, + props, + editorCallback + }); } else { throw new Error('Editor is not found on column.'); } From 778460fd3e584b8aeb24f3164870147ef368cf1f Mon Sep 17 00:00:00 2001 From: MBelniak Date: Wed, 22 Jan 2025 17:19:43 +0100 Subject: [PATCH 2/2] Return new state --- components/doc/datatable/edit/celleditdoc.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/doc/datatable/edit/celleditdoc.js b/components/doc/datatable/edit/celleditdoc.js index 5368c0af4f..f0d983132e 100644 --- a/components/doc/datatable/edit/celleditdoc.js +++ b/components/doc/datatable/edit/celleditdoc.js @@ -62,6 +62,8 @@ export function CellEditDoc(props) { break; } + + return newProducts; }); }; @@ -157,6 +159,8 @@ export default function CellEditingDemo() { break; } + + return newProducts; }); }; @@ -268,6 +272,8 @@ export default function CellEditingDemo() { break; } + + return newProducts; }); };