Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ Features include:
- [Usage](#usage)
- [Props overview](#props-overview)
- [Update functions](#update-functions)
- [OnChange function](#onchange-function)
- [Copy function](#copy-function)
- [Filter functions](#filter-functions)
- [Examples](#examples)
- [Examples](#examples-1)
- [Search/Filtering](#searchfiltering)
- [Themes \& Styles](#themes--styles)
- [Fragments](#fragments)
Expand Down Expand Up @@ -94,6 +95,7 @@ The only *required* value is `data`.
| `onEdit` | `UpdateFunction` | | A function to run whenever a value is **edited**. |
| `onDelete` | `UpdateFunction` | | A function to run whenever a value is **deleted**. |
| `onAdd` | `UpdateFunction` | | A function to run whenever a new property is **added**. |
| `onChange` | `OnChangeFunction` | | A function to modify/constrain user input as they type -- see [OnChange functions](#onchange-function). |
| `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. |
| `indent` | `number` | `3` | Specify the amount of indentation for each level of nesting in the displayed data. |
| `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). |
Expand Down Expand Up @@ -140,6 +142,32 @@ The function will receive the following object as a parameter:

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.

### OnChange function

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.

The input object is similar to the Update function input, but with no `newData` field (since this operation occurs before the data is updated).

#### Examples

- Restrict "age" inputs to positive values up to 100:
```js
// in <JsonEditor /> props
onChange = ({ newValue, name }) => {
if (name === "age" && newValue < 0) return 0;
if (name === "age" && newValue > 100) return 100;
return newValue
}
```
- Only allow alphabetical or whitespace input for "name" field (including no line breaks):
```js
onChange = ({ newValue, name }) => {
if (name === 'name' && typeof newValue === "string")
return newValue.replace(/[^a-zA-Z\s]|\n|\r/gm, '');
return newValue;
}
```

### Copy function

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:
Expand Down Expand Up @@ -531,7 +559,7 @@ A few helper functions, components and types that might be useful in your own im
- `Theme`: a full [Theme](#themes--styles) object
- `ThemeInput`: input type for the `theme` prop
- `JsonEditorProps`: all input props for the Json Editor component
- [`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)
- [`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)
- `TranslateFunction`: function that takes a [localisation](#localisation) key and returns a translated string
- `IconReplacements`: input type for the `icons` prop
- `CollectionNodeProps`: all props passed internally to "collection" nodes (i.e. objects/arrays)
Expand Down
1 change: 1 addition & 0 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ function App() {
stringTruncate={90}
customNodeDefinitions={demoData[selectedData]?.customNodeDefinitions}
customText={demoData[selectedData]?.customTextDefinitions}
onChange={demoData[selectedData]?.onChange ?? undefined}
/>
</Box>
<VStack w="100%" align="flex-end" gap={4}>
Expand Down
24 changes: 21 additions & 3 deletions demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CollectionKey,
DataType,
DefaultValueFunction,
OnChangeFunction,
SearchFilterFunction,
ThemeStyles,
UpdateFunction,
Expand Down Expand Up @@ -49,6 +50,7 @@ interface DemoData {
name: CollectionKey
path: CollectionKey[]
}) => any
onChange?: OnChangeFunction
defaultValue?: unknown | DefaultValueFunction
customNodeDefinitions?: CustomNodeDefinition[]
customTextDefinitions?: CustomTextDefinitions
Expand Down Expand Up @@ -140,9 +142,11 @@ export const demoData: Record<string, DemoData> = {
<Text>
You'll note that the <span className="code">id</span> field is not editable, which would
be important if this saved back to a database. An additional{' '}
<span className="code">restrictEdit</span> function as been included which targets the{' '}
<span className="code">id</span> field specifically. You also can't add additional fields
to the main "Person" objects.
<Link href="https://github.com/CarlosNZ/json-edit-react#filter-functions" isExternal>
<span className="code">restrictEdit</span> function
</Link>{' '}
has been included which targets the <span className="code">id</span> field specifically.
You also can't add additional fields to the main "Person" objects.
</Text>
<Text>
Also, notice that when you add a new item in the top level array, a correctly structured{' '}
Expand All @@ -159,6 +163,14 @@ export const demoData: Record<string, DemoData> = {
</Link>
.
</Text>
<Text>
Finally, an{' '}
<Link href="https://github.com/CarlosNZ/json-edit-react#onchange-function" isExternal>
<span className="code">onChange</span> function
</Link>{' '}
has been added to restrict user input in the <span className="code">name</span> field to
alphabetical characters only (with no line breaks too).
</Text>
</Flex>
),
restrictEdit: ({ key, level }) => key === 'id' || level === 0 || level === 1,
Expand Down Expand Up @@ -202,6 +214,12 @@ export const demoData: Record<string, DemoData> = {
}
return 'New Value'
},
onChange: ({ newValue, name }) => {
if (name === 'name') return (newValue as string).replace(/[^a-zA-Z\s]|\n|\r/gm, '')
if (['username', 'email', 'phone', 'website'].includes(name as string))
return (newValue as string).replace(/\n|\r/gm, '')
return newValue
},
data: data.jsonPlaceholder,
},
vsCode: {
Expand Down
2 changes: 2 additions & 0 deletions src/CollectionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
const value = JSON5.parse(stringifiedValue)
setIsEditing(false)
setError(null)
if (JSON.stringify(value) === JSON.stringify(data)) return
onEdit(value, path).then((error) => {
if (error) showError(error)
})
Expand All @@ -160,6 +161,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({

const handleEditKey = (newKey: string) => {
setIsEditingKey(false)
if (name === newKey) return
if (!parentData) return
const parentPath = path.slice(0, -1)
if (!newKey) return
Expand Down
12 changes: 8 additions & 4 deletions src/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type CollectionData,
type JsonEditorProps,
type FilterFunction,
type OnChangeFunction,
type InternalUpdateFunction,
type NodeData,
type SearchFilterFunction,
} from './types'
Expand All @@ -26,6 +26,7 @@ const Editor: React.FC<JsonEditorProps> = ({
onEdit: srcEdit = onUpdate,
onDelete: srcDelete = onUpdate,
onAdd: srcAdd = onUpdate,
onChange,
enableClipboard = true,
theme = 'default',
icons,
Expand Down Expand Up @@ -89,13 +90,15 @@ const Editor: React.FC<JsonEditorProps> = ({
fullData: data,
}

const onEdit: OnChangeFunction = async (value, path) => {
const onEdit: InternalUpdateFunction = async (value, path) => {
const { currentData, newData, currentValue, newValue } = updateDataObject(
data,
path,
value,
'update'
)
if (currentValue === newValue) return

setData(newData)

const result = await srcEdit({
Expand All @@ -112,7 +115,7 @@ const Editor: React.FC<JsonEditorProps> = ({
}
}

const onDelete: OnChangeFunction = async (value, path) => {
const onDelete: InternalUpdateFunction = async (value, path) => {
const { currentData, newData, currentValue, newValue } = updateDataObject(
data,
path,
Expand All @@ -135,7 +138,7 @@ const Editor: React.FC<JsonEditorProps> = ({
}
}

const onAdd: OnChangeFunction = async (value, path) => {
const onAdd: InternalUpdateFunction = async (value, path) => {
const { currentData, newData, currentValue, newValue } = updateDataObject(
data,
path,
Expand Down Expand Up @@ -169,6 +172,7 @@ const Editor: React.FC<JsonEditorProps> = ({
onEdit,
onDelete,
onAdd,
onChange,
showCollectionCount,
collapseFilter,
restrictEditFilter,
Expand Down
29 changes: 25 additions & 4 deletions src/ValueNodeWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo } from 'react'
import React, { useEffect, useState, useMemo, useCallback } from 'react'
import {
StringValue,
NumberValue,
Expand Down Expand Up @@ -32,6 +32,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
nodeData,
onEdit,
onDelete,
onChange,
enableClipboard,
restrictEditFilter,
restrictDeleteFilter,
Expand Down Expand Up @@ -59,6 +60,25 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
const customNodeData = getCustomNode(customNodeDefinitions, nodeData)
const [dataType, setDataType] = useState<DataType | string>(getDataType(data, customNodeData))

const updateValue = useCallback(
(newValue: ValueData) => {
if (!onChange) {
setValue(newValue)
return
}

const modifiedValue = onChange({
currentData: nodeData.fullData,
newValue,
currentValue: value as ValueData,
name,
path,
})
setValue(modifiedValue)
},
[onChange]
)

useEffect(() => {
setValue(typeof data === 'function' ? INVALID_FUNCTION_STRING : data)
setDataType(getDataType(data, customNodeData))
Expand Down Expand Up @@ -113,7 +133,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
// that won't match the custom node condition any more
customNodeData?.CustomNode ? translate('DEFAULT_STRING', nodeData) : undefined
)
setValue(newValue as ValueData | CollectionData)
updateValue(newValue as ValueData)
onEdit(newValue, path)
setDataType(type)
}
Expand Down Expand Up @@ -151,6 +171,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {

const handleEditKey = (newKey: string) => {
setIsEditingKey(false)
if (name === newKey) return
if (!parentData) return
const parentPath = path.slice(0, -1)
if (!newKey) return
Expand Down Expand Up @@ -186,7 +207,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
const inputProps = {
value,
parentData,
setValue,
setValue: updateValue,
isEditing,
setIsEditing: canEdit ? () => setIsEditing(true) : () => {},
handleEdit,
Expand All @@ -204,7 +225,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
{...props}
value={value}
customNodeProps={customNodeProps}
setValue={setValue}
setValue={updateValue}
handleEdit={handleEdit}
handleCancel={handleCancel}
handleKeyPress={(e: React.KeyboardEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions src/ValueNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ export const NumberValue: React.FC<InputProps & { value: number }> = ({
break
case 'ArrowUp':
e.preventDefault()
setValue((prev) => Number(prev) + 1)
setValue(Number(value) + 1)
break
case 'ArrowDown':
e.preventDefault()
setValue((prev) => Number(prev) - 1)
setValue(Number(value) - 1)
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import JsonEditor from './JsonEditor'
import {
type JsonEditorProps,
type UpdateFunction,
type OnChangeFunction,
type CopyFunction,
type FilterFunction,
type SearchFilterFunction,
Expand Down Expand Up @@ -34,6 +35,7 @@ export {
type ThemeInput,
type JsonEditorProps,
type UpdateFunction,
type OnChangeFunction,
type CopyFunction,
type FilterFunction,
type SearchFilterFunction,
Expand Down
Loading