Skip to content

Commit a8e3ef4

Browse files
authored
#51 onChange prop (#55)
1 parent 91d4689 commit a8e3ef4

9 files changed

+117
-23
lines changed

README.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ Features include:
2222
- [Usage](#usage)
2323
- [Props overview](#props-overview)
2424
- [Update functions](#update-functions)
25+
- [OnChange function](#onchange-function)
2526
- [Copy function](#copy-function)
2627
- [Filter functions](#filter-functions)
27-
- [Examples](#examples)
28+
- [Examples](#examples-1)
2829
- [Search/Filtering](#searchfiltering)
2930
- [Themes \& Styles](#themes--styles)
3031
- [Fragments](#fragments)
@@ -94,6 +95,7 @@ The only *required* value is `data`.
9495
| `onEdit` | `UpdateFunction` | | A function to run whenever a value is **edited**. |
9596
| `onDelete` | `UpdateFunction` | | A function to run whenever a value is **deleted**. |
9697
| `onAdd` | `UpdateFunction` | | A function to run whenever a new property is **added**. |
98+
| `onChange` | `OnChangeFunction` | | A function to modify/constrain user input as they type -- see [OnChange functions](#onchange-function). |
9799
| `enableClipboard` | `boolean\|CopyFunction` | `true` | Whether or not to enable the "Copy to clipboard" button in the UI. If a function is provided, `true` is assumed and this function will be run whenever an item is copied. |
98100
| `indent` | `number` | `3` | Specify the amount of indentation for each level of nesting in the displayed data. |
99101
| `collapse` | `boolean\|number\|FilterFunction` | `false` | Defines which nodes of the JSON tree will be displayed "opened" in the UI on load. If `boolean`, it'll be either all or none. A `number` specifies a nesting depth after which nodes will be closed. For more fine control a function can be provided — see [Filter functions](#filter-functions). |
@@ -140,6 +142,32 @@ The function will receive the following object as a parameter:
140142

141143
The function needn't return anything, but if it returns `false`, it will be considered an error, in which case an error message will displayed in the UI and the internal data state won't actually be updated. If the return value is a `string`, this will be the error message displayed (i.e. you can define your own error messages for updates). On error, the displayed data will revert to its previous value.
142144

145+
### OnChange function
146+
147+
Similar to the Update functions, the `onChange` function is executed as the user input changes. You can use this to restrict or constrain user input -- e.g. limiting numbers to positive values, or preventing line breaks in strings. The function *must* return a value in order to update the user input field, so if no changes are to made, just return it unmodified.
148+
149+
The input object is similar to the Update function input, but with no `newData` field (since this operation occurs before the data is updated).
150+
151+
#### Examples
152+
153+
- Restrict "age" inputs to positive values up to 100:
154+
```js
155+
// in <JsonEditor /> props
156+
onChange = ({ newValue, name }) => {
157+
if (name === "age" && newValue < 0) return 0;
158+
if (name === "age" && newValue > 100) return 100;
159+
return newValue
160+
}
161+
```
162+
- Only allow alphabetical or whitespace input for "name" field (including no line breaks):
163+
```js
164+
onChange = ({ newValue, name }) => {
165+
if (name === 'name' && typeof newValue === "string")
166+
return newValue.replace(/[^a-zA-Z\s]|\n|\r/gm, '');
167+
return newValue;
168+
}
169+
```
170+
143171
### Copy function
144172

145173
A similar callback is executed whenever an item is copied to the clipboard (if passed to the `enableClipboard` prop), but with a different input parameter:
@@ -531,7 +559,7 @@ A few helper functions, components and types that might be useful in your own im
531559
- `Theme`: a full [Theme](#themes--styles) object
532560
- `ThemeInput`: input type for the `theme` prop
533561
- `JsonEditorProps`: all input props for the Json Editor component
534-
- [`UpdateFunction`](#update-functions), [`FilterFunction`](#filter-functions), [`CopyFunction`](#copy-function), [`SearchFilterFunction`](#searchfiltering), [`CompareFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [`LocalisedString`](#localisation), [`CustomNodeDefinition`](#custom-nodes), [`CustomTextDefinitions`](#custom-text)
562+
- [`UpdateFunction`](#update-functions), [`OnChangeFunction`](#onchange-function), [`FilterFunction`](#filter-functions), [`CopyFunction`](#copy-function), [`SearchFilterFunction`](#searchfiltering), [`CompareFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [`LocalisedString`](#localisation), [`CustomNodeDefinition`](#custom-nodes), [`CustomTextDefinitions`](#custom-text)
535563
- `TranslateFunction`: function that takes a [localisation](#localisation) key and returns a translated string
536564
- `IconReplacements`: input type for the `icons` prop
537565
- `CollectionNodeProps`: all props passed internally to "collection" nodes (i.e. objects/arrays)

demo/src/App.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ function App() {
292292
stringTruncate={90}
293293
customNodeDefinitions={demoData[selectedData]?.customNodeDefinitions}
294294
customText={demoData[selectedData]?.customTextDefinitions}
295+
onChange={demoData[selectedData]?.onChange ?? undefined}
295296
/>
296297
</Box>
297298
<VStack w="100%" align="flex-end" gap={4}>

demo/src/demoData/dataDefinitions.tsx

+21-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
CollectionKey,
1515
DataType,
1616
DefaultValueFunction,
17+
OnChangeFunction,
1718
SearchFilterFunction,
1819
ThemeStyles,
1920
UpdateFunction,
@@ -49,6 +50,7 @@ interface DemoData {
4950
name: CollectionKey
5051
path: CollectionKey[]
5152
}) => any
53+
onChange?: OnChangeFunction
5254
defaultValue?: unknown | DefaultValueFunction
5355
customNodeDefinitions?: CustomNodeDefinition[]
5456
customTextDefinitions?: CustomTextDefinitions
@@ -140,9 +142,11 @@ export const demoData: Record<string, DemoData> = {
140142
<Text>
141143
You'll note that the <span className="code">id</span> field is not editable, which would
142144
be important if this saved back to a database. An additional{' '}
143-
<span className="code">restrictEdit</span> function as been included which targets the{' '}
144-
<span className="code">id</span> field specifically. You also can't add additional fields
145-
to the main "Person" objects.
145+
<Link href="https://github.com/CarlosNZ/json-edit-react#filter-functions" isExternal>
146+
<span className="code">restrictEdit</span> function
147+
</Link>{' '}
148+
has been included which targets the <span className="code">id</span> field specifically.
149+
You also can't add additional fields to the main "Person" objects.
146150
</Text>
147151
<Text>
148152
Also, notice that when you add a new item in the top level array, a correctly structured{' '}
@@ -159,6 +163,14 @@ export const demoData: Record<string, DemoData> = {
159163
</Link>
160164
.
161165
</Text>
166+
<Text>
167+
Finally, an{' '}
168+
<Link href="https://github.com/CarlosNZ/json-edit-react#onchange-function" isExternal>
169+
<span className="code">onChange</span> function
170+
</Link>{' '}
171+
has been added to restrict user input in the <span className="code">name</span> field to
172+
alphabetical characters only (with no line breaks too).
173+
</Text>
162174
</Flex>
163175
),
164176
restrictEdit: ({ key, level }) => key === 'id' || level === 0 || level === 1,
@@ -202,6 +214,12 @@ export const demoData: Record<string, DemoData> = {
202214
}
203215
return 'New Value'
204216
},
217+
onChange: ({ newValue, name }) => {
218+
if (name === 'name') return (newValue as string).replace(/[^a-zA-Z\s]|\n|\r/gm, '')
219+
if (['username', 'email', 'phone', 'website'].includes(name as string))
220+
return (newValue as string).replace(/\n|\r/gm, '')
221+
return newValue
222+
},
205223
data: data.jsonPlaceholder,
206224
},
207225
vsCode: {

src/CollectionNode.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
148148
const value = JSON5.parse(stringifiedValue)
149149
setIsEditing(false)
150150
setError(null)
151+
if (JSON.stringify(value) === JSON.stringify(data)) return
151152
onEdit(value, path).then((error) => {
152153
if (error) showError(error)
153154
})
@@ -160,6 +161,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
160161

161162
const handleEditKey = (newKey: string) => {
162163
setIsEditingKey(false)
164+
if (name === newKey) return
163165
if (!parentData) return
164166
const parentPath = path.slice(0, -1)
165167
if (!newKey) return

src/JsonEditor.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type CollectionData,
88
type JsonEditorProps,
99
type FilterFunction,
10-
type OnChangeFunction,
10+
type InternalUpdateFunction,
1111
type NodeData,
1212
type SearchFilterFunction,
1313
} from './types'
@@ -26,6 +26,7 @@ const Editor: React.FC<JsonEditorProps> = ({
2626
onEdit: srcEdit = onUpdate,
2727
onDelete: srcDelete = onUpdate,
2828
onAdd: srcAdd = onUpdate,
29+
onChange,
2930
enableClipboard = true,
3031
theme = 'default',
3132
icons,
@@ -89,13 +90,15 @@ const Editor: React.FC<JsonEditorProps> = ({
8990
fullData: data,
9091
}
9192

92-
const onEdit: OnChangeFunction = async (value, path) => {
93+
const onEdit: InternalUpdateFunction = async (value, path) => {
9394
const { currentData, newData, currentValue, newValue } = updateDataObject(
9495
data,
9596
path,
9697
value,
9798
'update'
9899
)
100+
if (currentValue === newValue) return
101+
99102
setData(newData)
100103

101104
const result = await srcEdit({
@@ -112,7 +115,7 @@ const Editor: React.FC<JsonEditorProps> = ({
112115
}
113116
}
114117

115-
const onDelete: OnChangeFunction = async (value, path) => {
118+
const onDelete: InternalUpdateFunction = async (value, path) => {
116119
const { currentData, newData, currentValue, newValue } = updateDataObject(
117120
data,
118121
path,
@@ -135,7 +138,7 @@ const Editor: React.FC<JsonEditorProps> = ({
135138
}
136139
}
137140

138-
const onAdd: OnChangeFunction = async (value, path) => {
141+
const onAdd: InternalUpdateFunction = async (value, path) => {
139142
const { currentData, newData, currentValue, newValue } = updateDataObject(
140143
data,
141144
path,
@@ -169,6 +172,7 @@ const Editor: React.FC<JsonEditorProps> = ({
169172
onEdit,
170173
onDelete,
171174
onAdd,
175+
onChange,
172176
showCollectionCount,
173177
collapseFilter,
174178
restrictEditFilter,

src/ValueNodeWrapper.tsx

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useMemo } from 'react'
1+
import React, { useEffect, useState, useMemo, useCallback } from 'react'
22
import {
33
StringValue,
44
NumberValue,
@@ -32,6 +32,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
3232
nodeData,
3333
onEdit,
3434
onDelete,
35+
onChange,
3536
enableClipboard,
3637
restrictEditFilter,
3738
restrictDeleteFilter,
@@ -59,6 +60,25 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
5960
const customNodeData = getCustomNode(customNodeDefinitions, nodeData)
6061
const [dataType, setDataType] = useState<DataType | string>(getDataType(data, customNodeData))
6162

63+
const updateValue = useCallback(
64+
(newValue: ValueData) => {
65+
if (!onChange) {
66+
setValue(newValue)
67+
return
68+
}
69+
70+
const modifiedValue = onChange({
71+
currentData: nodeData.fullData,
72+
newValue,
73+
currentValue: value as ValueData,
74+
name,
75+
path,
76+
})
77+
setValue(modifiedValue)
78+
},
79+
[onChange]
80+
)
81+
6282
useEffect(() => {
6383
setValue(typeof data === 'function' ? INVALID_FUNCTION_STRING : data)
6484
setDataType(getDataType(data, customNodeData))
@@ -113,7 +133,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
113133
// that won't match the custom node condition any more
114134
customNodeData?.CustomNode ? translate('DEFAULT_STRING', nodeData) : undefined
115135
)
116-
setValue(newValue as ValueData | CollectionData)
136+
updateValue(newValue as ValueData)
117137
onEdit(newValue, path)
118138
setDataType(type)
119139
}
@@ -151,6 +171,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
151171

152172
const handleEditKey = (newKey: string) => {
153173
setIsEditingKey(false)
174+
if (name === newKey) return
154175
if (!parentData) return
155176
const parentPath = path.slice(0, -1)
156177
if (!newKey) return
@@ -186,7 +207,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
186207
const inputProps = {
187208
value,
188209
parentData,
189-
setValue,
210+
setValue: updateValue,
190211
isEditing,
191212
setIsEditing: canEdit ? () => setIsEditing(true) : () => {},
192213
handleEdit,
@@ -204,7 +225,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
204225
{...props}
205226
value={value}
206227
customNodeProps={customNodeProps}
207-
setValue={setValue}
228+
setValue={updateValue}
208229
handleEdit={handleEdit}
209230
handleCancel={handleCancel}
210231
handleKeyPress={(e: React.KeyboardEvent) => {

src/ValueNodes.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ export const NumberValue: React.FC<InputProps & { value: number }> = ({
7777
break
7878
case 'ArrowUp':
7979
e.preventDefault()
80-
setValue((prev) => Number(prev) + 1)
80+
setValue(Number(value) + 1)
8181
break
8282
case 'ArrowDown':
8383
e.preventDefault()
84-
setValue((prev) => Number(prev) - 1)
84+
setValue(Number(value) - 1)
8585
}
8686
}
8787

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import JsonEditor from './JsonEditor'
22
import {
33
type JsonEditorProps,
44
type UpdateFunction,
5+
type OnChangeFunction,
56
type CopyFunction,
67
type FilterFunction,
78
type SearchFilterFunction,
@@ -34,6 +35,7 @@ export {
3435
type ThemeInput,
3536
type JsonEditorProps,
3637
type UpdateFunction,
38+
type OnChangeFunction,
3739
type CopyFunction,
3840
type FilterFunction,
3941
type SearchFilterFunction,

0 commit comments

Comments
 (0)