diff --git a/docs/pages/apis.mdx b/docs/pages/apis.mdx
index 6084974b..e597beb8 100644
--- a/docs/pages/apis.mdx
+++ b/docs/pages/apis.mdx
@@ -10,12 +10,16 @@
| `sx` | `SxProps` | - | [The `sx` prop](https://mui.com/system/getting-started/the-sx-prop/) lets you style elements inline, using values from the theme. |
| `indentWidth` | `number` | 3 | Indent width for nested objects |
| `keyRenderer` | `{when: (props) => boolean}` | - | Customize a key, if `keyRenderer.when` returns `true`. |
-| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) |
+| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) |
+| `enableAdd` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable add feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
+| `enableDelete` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable delete feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
+| `enableClipboard` | `boolean` | `false` | Whether enable clipboard feature. |
+| `editable` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable edit feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
| `onChange` | `(path, oldVal, newVal) => void` | - | Callback when value changed. |
| `onCopy` | `(path, value) => void` | - | Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. |
| `onSelect` | `(path, value) => void` | - | Callback when value selected. |
-| `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. |
-| `editable` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable edit feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
+| `onAdd` | `(path) => void` | - | Callback when the add button is clicked. This is the function which implements the add feature. Please see the [DEMO](/full) for more details. |
+| `onDelete` | `(path) => void` | - | Callback when the delete button is clicked. This is the function which implements the delete feature. Please see the [DEMO](/full) for more details. |
| `defaultInspectDepth` | `number` | 5 | Default inspect depth for nested objects.
_\* If the number is set too large, it could result in performance issues._ |
| `defaultInspectControl` | `(path, currentValue) => boolean` | - | Whether expand or collapse a field by default. Using this will override `defaultInspectDepth`. |
| `maxDisplayLength` | `number` | 30 | Hide items after reaching the count.
`Array` and `Object` will be affected.
_\* If the number is set too large, it could result in performance issues._ |
diff --git a/docs/pages/full/index.tsx b/docs/pages/full/index.tsx
index 4eeaef08..b23dcb3a 100644
--- a/docs/pages/full/index.tsx
+++ b/docs/pages/full/index.tsx
@@ -16,12 +16,15 @@ import {
import type {
DataType,
JsonViewerKeyRenderer,
+ JsonViewerOnAdd,
JsonViewerOnChange,
+ JsonViewerOnDelete,
JsonViewerTheme
} from '@textea/json-viewer'
import {
applyValue,
defineDataType,
+ deleteValue,
JsonViewer,
stringType
} from '@textea/json-viewer'
@@ -336,6 +339,8 @@ const IndexPage: FC = () => {
highlightUpdates={highlightUpdates}
indentWidth={indent}
theme={theme}
+ enableAdd={true}
+ enableDelete={true}
displayDataTypes={displayDataTypes}
displaySize={displaySize}
groupArraysAfterLength={groupArraysAfterLength}
@@ -345,6 +350,17 @@ const IndexPage: FC = () => {
linkType,
imageDataType
]}
+ onAdd={
+ useCallback(
+ (path) => {
+ const key = prompt('Key:')
+ if (key === null) return
+ const value = prompt('Value:')
+ if (value === null) return
+ setSrc(src => applyValue(src, [...path, key], value))
+ }, []
+ )
+ }
onChange={
useCallback(
(path, oldValue, newValue) => {
@@ -352,6 +368,13 @@ const IndexPage: FC = () => {
}, []
)
}
+ onDelete={
+ useCallback(
+ (path, value) => {
+ setSrc(src => deleteValue(src, path, value))
+ }, []
+ )
+ }
sx={{
paddingLeft: 2
}}
diff --git a/docs/pages/how-to/data-types.mdx b/docs/pages/how-to/data-types.mdx
index 55c1194a..dcef7d48 100644
--- a/docs/pages/how-to/data-types.mdx
+++ b/docs/pages/how-to/data-types.mdx
@@ -25,6 +25,7 @@ The `is` function takes a value and a path and returns true if the value belongs
The `Component` prop is a React component that renders the value of the data type. It receives a `DataItemProps` object as a `prop`, which includes the following:
+- `props.path` - The path to the value.
- `props.value` - The value to render.
- `props.inspect` - A Boolean flag indicating whether the value is being inspected (expanded).
- `props.setInspect` - A function that can be used to toggle the inspect state.
diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx
index d7d858a2..0642b348 100644
--- a/src/components/DataKeyPair.tsx
+++ b/src/components/DataKeyPair.tsx
@@ -8,12 +8,14 @@ import { useInspect } from '../hooks/useInspect'
import { useJsonViewerStore } from '../stores/JsonViewerStore'
import { useTypeComponents } from '../stores/typeRegistry'
import type { DataItemProps } from '../type'
-import { copyString, getValueSize } from '../utils'
+import { copyString, getValueSize, isPlainObject } from '../utils'
import {
+ AddBoxIcon,
CheckIcon,
ChevronRightIcon,
CloseIcon,
ContentCopyIcon,
+ DeleteIcon,
EditIcon,
ExpandMoreIcon
} from './Icons'
@@ -82,6 +84,51 @@ export const DataKeyPair: FC = (props) => {
const isRoot = root === value
const isNumberKey = Number.isInteger(Number(key))
+ const storeEnableAdd = useJsonViewerStore(store => store.enableAdd)
+ const onAdd = useJsonViewerStore(store => store.onAdd)
+ const enableAdd = useMemo(() => {
+ if (!onAdd || nestedIndex !== undefined) return false
+
+ if (storeEnableAdd === false) {
+ return false
+ }
+ if (propsEditable === false) {
+ // props.editable is false which means we cannot provide the suitable way to edit it
+ return false
+ }
+ if (typeof storeEnableAdd === 'function') {
+ return !!storeEnableAdd(path, value)
+ }
+
+ if (Array.isArray(value) || isPlainObject(value)) {
+ return true
+ }
+
+ return false
+ }, [onAdd, nestedIndex, path, storeEnableAdd, propsEditable, value])
+
+ const storeEnableDelete = useJsonViewerStore(store => store.enableDelete)
+ const onDelete = useJsonViewerStore(store => store.onDelete)
+ const enableDelete = useMemo(() => {
+ if (!onDelete || nestedIndex !== undefined) return false
+
+ if (isRoot) {
+ // don't allow delete root
+ return false
+ }
+ if (storeEnableDelete === false) {
+ return false
+ }
+ if (propsEditable === false) {
+ // props.editable is false which means we cannot provide the suitable way to edit it
+ return false
+ }
+ if (typeof storeEnableDelete === 'function') {
+ return !!storeEnableDelete(path, value)
+ }
+ return storeEnableDelete
+ }, [onDelete, nestedIndex, isRoot, path, storeEnableDelete, propsEditable, value])
+
const enableClipboard = useJsonViewerStore(store => store.enableClipboard)
const { copy, copied } = useClipboard()
@@ -205,6 +252,26 @@ export const DataKeyPair: FC = (props) => {
)}
+ {enableAdd && (
+ {
+ event.preventDefault()
+ onAdd?.(path)
+ }}
+ >
+
+
+ )}
+ {enableDelete && (
+ {
+ event.preventDefault()
+ onDelete?.(path, value)
+ }}
+ >
+
+
+ )}
>
)
},
@@ -217,9 +284,13 @@ export const DataKeyPair: FC = (props) => {
editable,
editing,
enableClipboard,
+ enableAdd,
+ enableDelete,
tempValue,
path,
value,
+ onAdd,
+ onDelete,
startEditing,
abortEditing,
commitEditing
diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx
index 20bd5bae..ea688711 100644
--- a/src/components/Icons.tsx
+++ b/src/components/Icons.tsx
@@ -10,6 +10,7 @@ const BaseIcon: FC = ({ d, ...props }) => {
)
}
+const AddBox = 'M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14zm-8-2h2v-4h4v-2h-4V7h-2v4H7v2h4z'
const Check = 'M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'
const ChevronRight = 'M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'
const CircularArrows = 'M 12 2 C 10.615 1.998 9.214625 2.2867656 7.890625 2.8847656 L 8.9003906 4.6328125 C 9.9043906 4.2098125 10.957 3.998 12 4 C 15.080783 4 17.738521 5.7633175 19.074219 8.3222656 L 17.125 9 L 21.25 11 L 22.875 7 L 20.998047 7.6523438 C 19.377701 4.3110398 15.95585 2 12 2 z M 6.5097656 4.4882812 L 2.2324219 5.0820312 L 3.734375 6.3808594 C 1.6515335 9.4550558 1.3615962 13.574578 3.3398438 17 C 4.0308437 18.201 4.9801562 19.268234 6.1601562 20.115234 L 7.1699219 18.367188 C 6.3019219 17.710187 5.5922656 16.904 5.0722656 16 C 3.5320014 13.332354 3.729203 10.148679 5.2773438 7.7128906 L 6.8398438 9.0625 L 6.5097656 4.4882812 z M 19.929688 13 C 19.794687 14.08 19.450734 15.098 18.927734 16 C 17.386985 18.668487 14.531361 20.090637 11.646484 19.966797 L 12.035156 17.9375 L 8.2402344 20.511719 L 10.892578 23.917969 L 11.265625 21.966797 C 14.968963 22.233766 18.681899 20.426323 20.660156 17 C 21.355156 15.801 21.805219 14.445 21.949219 13 L 19.929688 13 z'
@@ -17,6 +18,11 @@ const Close = 'M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19
const ContentCopy = 'M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'
const Edit = 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'
const ExpandMore = 'M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z'
+const Delete = 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zM8 9h8v10H8zm7.5-5l-1-1h-5l-1 1H5v2h14V4z'
+
+export const AddBoxIcon: FC = (props) => {
+ return
+}
export const CheckIcon: FC = (props) => {
return
@@ -45,3 +51,7 @@ export const EditIcon: FC = (props) => {
export const ExpandMoreIcon: FC = (props) => {
return
}
+
+export const DeleteIcon: FC = (props) => {
+ return
+}
diff --git a/src/index.tsx b/src/index.tsx
index 0519816b..f3180eb8 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -50,19 +50,23 @@ const JsonViewerInner: FC = (props) => {
value: props.value
}))
}, [props.value, setState])
- useSetIfNotUndefinedEffect('editable', props.editable)
+ useSetIfNotUndefinedEffect('rootName', props.rootName)
useSetIfNotUndefinedEffect('indentWidth', props.indentWidth)
- useSetIfNotUndefinedEffect('onChange', props.onChange)
- useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
useSetIfNotUndefinedEffect('keyRenderer', props.keyRenderer)
- useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
+ useSetIfNotUndefinedEffect('enableAdd', props.enableAdd)
+ useSetIfNotUndefinedEffect('enableDelete', props.enableDelete)
useSetIfNotUndefinedEffect('enableClipboard', props.enableClipboard)
- useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
- useSetIfNotUndefinedEffect('rootName', props.rootName)
- useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
- useSetIfNotUndefinedEffect('displaySize', props.displaySize)
+ useSetIfNotUndefinedEffect('editable', props.editable)
+ useSetIfNotUndefinedEffect('onChange', props.onChange)
useSetIfNotUndefinedEffect('onCopy', props.onCopy)
useSetIfNotUndefinedEffect('onSelect', props.onSelect)
+ useSetIfNotUndefinedEffect('onAdd', props.onAdd)
+ useSetIfNotUndefinedEffect('onDelete', props.onDelete)
+ useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
+ useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
+ useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
+ useSetIfNotUndefinedEffect('displaySize', props.displaySize)
+ useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
useEffect(() => {
if (props.theme === 'light') {
setState({
@@ -179,4 +183,4 @@ export const JsonViewer = function JsonViewer (props: JsonViewerProps null
DefaultKeyRenderer.when = () => false
export type JsonViewerState = {
- inspectCache: Record
- hoverPath: { path: Path; nestedIndex?: number } | null
+ rootName: false | string
indentWidth: number
- groupArraysAfterLength: number
+ keyRenderer: JsonViewerKeyRenderer
+ enableAdd: boolean | ((path: Path, currentValue: U) => boolean)
+ enableDelete: boolean | ((path: Path, currentValue: U) => boolean)
enableClipboard: boolean
- highlightUpdates: boolean
- maxDisplayLength: number
+ editable: boolean | ((path: Path, currentValue: U) => boolean)
+ onChange: JsonViewerOnChange
+ onCopy: JsonViewerOnCopy | undefined
+ onSelect: JsonViewerOnSelect | undefined
+ onAdd: JsonViewerOnAdd | undefined
+ onDelete: JsonViewerOnDelete | undefined
defaultInspectDepth: number
defaultInspectControl?: (path: Path, value: unknown) => boolean
+ maxDisplayLength: number
+ groupArraysAfterLength: number
collapseStringsAfterLength: number
objectSortKeys: boolean | ((a: string, b: string) => number)
quotesOnKeys: boolean
- colorspace: Colorspace
- editable: boolean | ((path: Path, currentValue: U) => boolean)
displayDataTypes: boolean
- rootName: false | string
- prevValue: T | undefined
- value: T
- onChange: JsonViewerOnChange
- onCopy: JsonViewerOnCopy | undefined
- onSelect: JsonViewerOnSelect | undefined
- keyRenderer: JsonViewerKeyRenderer
displaySize: boolean | ((path: Path, value: unknown) => boolean)
+ highlightUpdates: boolean
+
+ inspectCache: Record
+ hoverPath: { path: Path; nestedIndex?: number } | null
+ colorspace: Colorspace
+ value: T
+ prevValue: T | undefined
getInspectCache: (path: Path, nestedIndex?: number) => boolean
setInspectCache: (path: Path, action: SetStateAction, nestedIndex?: number) => void
@@ -50,33 +55,38 @@ export type JsonViewerState = {
export const createJsonViewerStore = (props: JsonViewerProps) => {
return create()((set, get) => ({
// provided by user
- enableClipboard: props.enableClipboard ?? true,
- highlightUpdates: props.highlightUpdates ?? false,
- indentWidth: props.indentWidth ?? 3,
- groupArraysAfterLength: props.groupArraysAfterLength ?? 100,
- collapseStringsAfterLength:
- (props.collapseStringsAfterLength === false)
- ? Number.MAX_VALUE
- : props.collapseStringsAfterLength ?? 50,
- maxDisplayLength: props.maxDisplayLength ?? 30,
rootName: props.rootName ?? 'root',
+ indentWidth: props.indentWidth ?? 3,
+ keyRenderer: props.keyRenderer ?? DefaultKeyRenderer,
+ enableAdd: props.enableAdd ?? false,
+ enableDelete: props.enableDelete ?? false,
+ enableClipboard: props.enableClipboard ?? true,
+ editable: props.editable ?? false,
onChange: props.onChange ?? (() => {}),
onCopy: props.onCopy ?? undefined,
onSelect: props.onSelect ?? undefined,
- keyRenderer: props.keyRenderer ?? DefaultKeyRenderer,
- editable: props.editable ?? false,
+ onAdd: props.onAdd ?? undefined,
+ onDelete: props.onDelete ?? undefined,
defaultInspectDepth: props.defaultInspectDepth ?? 5,
defaultInspectControl: props.defaultInspectControl ?? undefined,
+ maxDisplayLength: props.maxDisplayLength ?? 30,
+ groupArraysAfterLength: props.groupArraysAfterLength ?? 100,
+ collapseStringsAfterLength:
+ (props.collapseStringsAfterLength === false)
+ ? Number.MAX_VALUE
+ : props.collapseStringsAfterLength ?? 50,
objectSortKeys: props.objectSortKeys ?? false,
quotesOnKeys: props.quotesOnKeys ?? true,
displayDataTypes: props.displayDataTypes ?? true,
+ displaySize: props.displaySize ?? true,
+ highlightUpdates: props.highlightUpdates ?? false,
+
// internal state
inspectCache: {},
hoverPath: null,
colorspace: lightColorspace,
value: props.value,
prevValue: undefined,
- displaySize: props.displaySize ?? true,
getInspectCache: (path, nestedIndex) => {
const target = nestedIndex !== undefined
diff --git a/src/type.ts b/src/type.ts
index 6903d956..7a501e83 100644
--- a/src/type.ts
+++ b/src/type.ts
@@ -19,7 +19,7 @@ export type JsonViewerOnChange = (
/**
* @param path path to the target value
- * @param value
+ * @param value the value to be copied
* @param copy the function to copy the value to clipboard
*/
export type JsonViewerOnCopy = (
@@ -30,13 +30,29 @@ export type JsonViewerOnCopy = (
/**
* @param path path to the target value
- * @param value
+ * @param value the value to be selected
*/
export type JsonViewerOnSelect = (
path: Path,
value: U,
) => void
+/**
+ * @param path path to the parent target where the value will be added
+ */
+export type JsonViewerOnAdd = (
+ path: Path,
+) => void
+
+/**
+ * @param path path to the target value
+ * @param value the value to be deleted
+ */
+export type JsonViewerOnDelete = (
+ path: Path,
+ value: U,
+) => void
+
export interface DataItemProps {
inspect: boolean
setInspect: Dispatch>
@@ -151,18 +167,28 @@ export type JsonViewerProps = {
* @see https://viewer.textea.io/how-to/data-types
*/
valueTypes?: DataType[]
- /** Callback when value changed. */
- onChange?: JsonViewerOnChange
- /** Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. */
- onCopy?: JsonViewerOnCopy
- /** Callback when value selected. */
- onSelect?: JsonViewerOnSelect
+
+ /**
+ * Whether enable add feature.
+ *
+ * @default false
+ * */
+ enableAdd?: boolean | ((path: Path, currentValue: U) => boolean)
+
+ /**
+ * Whether enable delete feature.
+ *
+ * @default false
+ * */
+ enableDelete?: boolean | ((path: Path, currentValue: U) => boolean)
+
/**
* Whether enable clipboard feature.
*
* @default true
*/
enableClipboard?: boolean
+
/**
* Whether this value can be edited.
*
@@ -171,6 +197,18 @@ export type JsonViewerProps = {
* @default false
*/
editable?: boolean | ((path: Path, currentValue: U) => boolean)
+
+ /** Callback when value changed. */
+ onChange?: JsonViewerOnChange
+ /** Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. */
+ onCopy?: JsonViewerOnCopy
+ /** Callback when value selected. */
+ onSelect?: JsonViewerOnSelect
+ /** Callback when the add button is clicked. This is the function which implements the add feature. Please see the official demo for more details. */
+ onAdd?: JsonViewerOnAdd
+ /** Callback when the delete button is clicked. This is the function which implements the delete feature. Please see the official demo for more details. */
+ onDelete?: JsonViewerOnDelete
+
/**
* Default inspect depth for nested objects.
* _If the number is set too large, it could result in performance issues._
@@ -178,12 +216,14 @@ export type JsonViewerProps = {
* @default 5
*/
defaultInspectDepth?: number
+
/**
* Default inspect control for nested objects.
*
* Provide a function to customize which fields should be expanded by default.
*/
defaultInspectControl?: (path: Path, value: unknown) => boolean
+
/**
* Hide items after reaching the count.
* `Array` and `Object` will be affected.
@@ -193,6 +233,7 @@ export type JsonViewerProps = {
* @default 30
*/
maxDisplayLength?: number
+
/**
* When an integer value is assigned, arrays will be displayed in groups by count of the value.
* Groups are displayed with bracket notation and can be expanded and collapsed by clicking on the brackets.
@@ -200,6 +241,7 @@ export type JsonViewerProps = {
* @default 100
*/
groupArraysAfterLength?: number
+
/**
* Cut off the string after reaching the count.
* Collapsed strings are followed by an ellipsis.
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 5cdb6e10..d67cdcfc 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -5,7 +5,7 @@ import type { DataItemProps, DataType, EditorProps, Path } from '../type'
// reference: https://github.com/immerjs/immer/blob/main/src/utils/common.ts
const objectCtorString = Object.prototype.constructor.toString()
-function isPlainObject (value: any): boolean {
+export function isPlainObject (value: any): boolean {
if (!value || typeof value !== 'object') return false
const proto = Object.getPrototypeOf(value)
@@ -38,6 +38,9 @@ function shallowCopy (value: any) {
return value
}
+/**
+ * Apply a value to a given path of an object.
+ */
export function applyValue (input: any, path: (string | number)[], value: any) {
if (typeof input !== 'object' || input === null) {
if (path.length !== 0) {
@@ -63,6 +66,38 @@ export function applyValue (input: any, path: (string | number)[], value: any) {
return input
}
+/**
+ * Delete a value from a given path of an object.
+ */
+export function deleteValue (input: any, path: (string | number)[], value: any) {
+ if (typeof input !== 'object' || input === null) {
+ if (path.length !== 0) {
+ throw new Error('path is incorrect')
+ }
+ return value
+ }
+
+ const shouldCopy = shouldShallowCopy(input)
+ if (shouldCopy) input = shallowCopy(input)
+
+ const [key, ...restPath] = path
+ if (key !== undefined) {
+ if (key === '__proto__') {
+ throw new TypeError('Modification of prototype is not allowed')
+ }
+ if (restPath.length > 0) {
+ input[key] = deleteValue(input[key], restPath, value)
+ } else {
+ if (Array.isArray(input)) {
+ input.splice(Number(key), 1)
+ } else {
+ delete input[key]
+ }
+ }
+ }
+ return input
+}
+
/**
* @deprecated use `defineDataType` instead
*/
diff --git a/tests/util.test.tsx b/tests/util.test.tsx
index 350832b4..65ab2e33 100644
--- a/tests/util.test.tsx
+++ b/tests/util.test.tsx
@@ -3,7 +3,7 @@ import type { ComponentType } from 'react'
import { describe, expect, test } from 'vitest'
import type { DataItemProps, Path } from '../src'
-import { applyValue, createDataType, isCycleReference } from '../src'
+import { applyValue, createDataType, deleteValue, isCycleReference } from '../src'
import { safeStringify, segmentArray } from '../src/utils'
describe('function applyValue', () => {
@@ -17,6 +17,13 @@ describe('function applyValue', () => {
}).toThrow()
})
+ test('prototype polluting', () => {
+ const original = {}
+ expect(() => {
+ applyValue(original, ['__proto__', 'polluted'], 1)
+ }).toThrow()
+ })
+
test('undefined', () => {
patches.forEach(patch => {
const newValue = applyValue(undefined, [], patch)
@@ -90,6 +97,99 @@ describe('function applyValue', () => {
})
})
+describe('function deleteValue', () => {
+ const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4]
+ test('incorrect arguments', () => {
+ expect(() => {
+ deleteValue({}, ['not', 'exist'], 1)
+ }).toThrow()
+ expect(() => {
+ deleteValue(1, ['not', 'exist'], 1)
+ }).toThrow()
+ })
+
+ test('prototype polluting', () => {
+ const original = {}
+ expect(() => {
+ deleteValue(original, ['__proto__', 'polluted'], 1)
+ }).toThrow()
+ })
+
+ test('undefined', () => {
+ patches.forEach(patch => {
+ const newValue = deleteValue(undefined, [], patch)
+ expect(newValue).is.eq(patch)
+ })
+ })
+
+ test('null', () => {
+ patches.forEach(patch => {
+ const newValue = deleteValue(null, [], patch)
+ expect(newValue).is.eq(patch)
+ })
+ })
+
+ test('number', () => {
+ patches.forEach(patch => {
+ const newValue = deleteValue(1, [], patch)
+ expect(newValue).is.eq(patch)
+ })
+ patches.forEach(patch => {
+ const newValue = deleteValue(114514, [], patch)
+ expect(newValue).is.eq(patch)
+ })
+ })
+
+ test('string', () => {
+ patches.forEach(patch => {
+ const newValue = deleteValue('', [], patch)
+ expect(newValue).is.eq(patch)
+ })
+ })
+
+ test('object', () => {
+ const original = {
+ foo: 1,
+ bar: 2
+ }
+ const newValue = deleteValue(original, ['foo'], 1)
+ expect(newValue).is.deep.eq({
+ bar: 2
+ })
+ })
+
+ test('object nested', () => {
+ const original = {
+ foo: {
+ bar: {
+ baz: 1,
+ qux: 2
+ }
+ }
+ }
+ const newValue = deleteValue(original, ['foo', 'bar', 'qux'], 2)
+ expect(newValue).is.deep.eq({
+ foo: {
+ bar: {
+ baz: 1
+ }
+ }
+ })
+ })
+
+ test('array', () => {
+ const original = [1, 2, 3]
+ const newValue = deleteValue(original, [1], 2)
+ expect(newValue).is.deep.eq([1, 3])
+ })
+
+ test('array nested', () => {
+ const original = [1, [2, [3, 4]]]
+ const newValue = deleteValue(original, [1, 1, 1], 4)
+ expect(newValue).is.deep.eq([1, [2, [3]]])
+ })
+})
+
describe('function isCycleReference', () => {
test('root is leaf', () => {
const root = {