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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ temp.js
demo/src/firebaseConfig.json

# Built package in demo/custom library
build_package
demo/src/package
custom-component-library/components/package
.original-readme.md
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ A highly-configurable [React](https://github.com/facebook/react) component for e
- 🎨 **Customisable UI** — built-in or custom [themes](#themes--styles), CSS overrides or targeted classes
- 📦 **Self-contained** — plain HTML/CSS, so no dependence on external UI libraries
- 🔍 **Search & filter** — find data by key, value or custom function
- 🚧 **[Custom components](#custom-nodes)** — replace specific nodes with specialised components (e.g. date picker, links, images)
- 🚧 **[Custom components](#custom-nodes)** — replace specific nodes with specialised components (e.g. date picker, links, images, `undefined`, `BigInt`, `Symbol`)
- 🌏 **[Localisation](#localisation)** — easily translate UI labels and messages
- 🔄 **[Drag-n-drop](#drag-n-drop)** re-ordering within objects/arrays
- 🎹 **[Keyboard customisation](#keyboard-customisation)** — define your own key bindings
Expand Down Expand Up @@ -71,6 +71,7 @@ A highly-configurable [React](https://github.com/facebook/react) component for e
- [Localisation](#localisation)
- [Custom Nodes](#custom-nodes)
- [Active hyperlinks](#active-hyperlinks)
- [Handling JSON](#handling-json)
- [Custom Collection nodes](#custom-collection-nodes)
- [Custom Text](#custom-text)
- [Custom Buttons](#custom-buttons)
Expand Down Expand Up @@ -918,8 +919,9 @@ Your `translations` object doesn't have to be exhaustive — only define the key

You can replace certain nodes in the data tree with your own custom components. An example might be for an image display, or a custom date editor, or just to add some visual bling. See the "Custom Nodes" data set in the [interactive demo](https://carlosnz.github.io/json-edit-react/?data=customNodes) to see it in action. (There is also a custom Date picker that appears when editing ISO strings in the other data sets.)

> [!NOTE]
> Coming soon: a **Custom Component** library
> [!TIP]
> There are a selection of useful Custom components ready for you to use in my [Custom Component Library](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md).
> Please contribute your own if you think they'd be useful to others.

Custom nodes are provided in the `customNodeDefinitions` prop, as an array of objects of following structure:

Expand All @@ -942,6 +944,10 @@ Custom nodes are provided in the `customNodeDefinitions` prop, as an array of ob
showCollectionWrapper // boolean (optional), default true
wrapperElement // React component (optional) to wrap *outside* the normal collection wrapper
wrapperProps // object (optional) -- props for the above wrapper component

// For JSON conversion -- only needed if editing as JSON text
stringifyReplacer // function for stringifying to JSON (if non-JSON data type)
parseReviver?: // function for parsing as JSON (if non-JSON data type)
}
```

Expand Down Expand Up @@ -973,6 +979,17 @@ return (
)
```

### Handling JSON

If you implement a Custom Node that uses a non-JSON data type (e.g. `BigInt`, `Date`), then if you edit your data as full JSON text, these values will be stripped out by the default `JSON.stringify` and `JSON.parse` methods. In this case, you can provide [**replacer**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer) and [**reviver**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#the_reviver_parameter) methods to serialize and de-serialize your data as you see fit. For example the [`BigInt` component](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/components/BigInt/definition.ts) in the [custom component library](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/components/DateObject/definition.ts) serializes the value into JSON text like so:

```
{
"__type": "BigInt",
"value": 1234567890123456789012345678901234567890
}
```

### Custom Collection nodes

In most cases it will be preferable (and simpler) to create custom nodes to match *value* nodes (i.e. not `array` or `object` *collection* nodes), which is what all the [Demo](https://carlosnz.github.io/json-edit-react/?data=customNodes) examples show. However, if you *do* wish to target a whole collection node, there are a couple of other things to know:
Expand Down Expand Up @@ -1199,6 +1216,9 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s

## Changelog

- **1.25.7**:
- Handle non-standard data types (e.g. `undefined`, `BigInt`) when stringifying/parsing JSON
- More custom components (See [library ReadMe](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md))
- **1.25.6**:
- Expose a few more components and props to custom components
- Start building Custom Component library (separate to main package)
Expand Down
23 changes: 18 additions & 5 deletions custom-component-library/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
A collection of [Custom Components](https://github.com/CarlosNZ/json-edit-react#custom-nodes) for **json-edit-react**.

A work in progress.
Eventually, I'd like to publish these in a separate package so you can easily import them. But for now just copy the code out of this repo.

Contains a [Vite](https://vite.dev/) web-app for previewing and developing components.

The individual components are in the `/components` folder, along with demo data (in `data.ts`).

> [!NOTE]
> If you create a custom component that you think would be useful to others, please [create a PR](https://github.com/CarlosNZ/json-edit-react/pulls) for it.

## Components

These are the ones I'm planning for now:
These are the ones currently available:

- [x] Hyperlink/URL
- [x] Undefined
- [x] Date Object
- [ ] Date Picker (with ISO string)
- [ ] `NaN`
- [ ] BigInt
- [x] Date/Time Picker (with ISO string)
- [x] Boolean Toggle
- [x] `NaN`
- [x] BigInt

## Development

Expand All @@ -33,3 +37,12 @@ Launch app:
yarn dev
```

## Guidelines for development:

Custom components should consider the following:

- Must respect editing restrictions
- If including CSS classes, please prefix with `jer-`
- Handle keyboard input if possible
- Provide customisation options, particularly styles

39 changes: 39 additions & 0 deletions custom-component-library/components/BigInt/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'
import { toPathString, StringEdit, type CustomNodeProps } from '@json-edit-react'

export interface BigIntProps {
style?: React.CSSProperties
descriptionStyle?: React.CSSProperties
}

export const BigIntComponent: React.FC<CustomNodeProps<BigIntProps>> = (props) => {
const {
setValue,
isEditing,
getStyles,
nodeData,
customNodeProps = {},
value,
handleEdit,
...rest
} = props
const { path } = nodeData
const { style = { color: '#006291', fontSize: '90%' } } = customNodeProps

const editDisplayValue = typeof value === 'bigint' ? String(value) : (value as string)

return isEditing ? (
<StringEdit
pathString={toPathString(path)}
styles={getStyles('input', nodeData)}
value={editDisplayValue}
setValue={setValue as React.Dispatch<React.SetStateAction<string>>}
{...rest}
handleEdit={() => {
handleEdit(BigInt(nodeData.value as string))
}}
/>
) : (
<span style={style}>{value as bigint}</span>
)
}
20 changes: 20 additions & 0 deletions custom-component-library/components/BigInt/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { isCollection, type CustomNodeDefinition } from '@json-edit-react'
import { BigIntComponent, BigIntProps } from './component'

export const BigIntDefinition: CustomNodeDefinition<BigIntProps> = {
condition: ({ value }) => typeof value === 'bigint',
element: BigIntComponent,
// customNodeProps: {},
showOnView: true,
showEditTools: true,
showOnEdit: true,
name: 'BigInt', // shown in the Type selector menu
showInTypesSelector: true,
defaultValue: BigInt(9007199254740992),
stringifyReplacer: (value) =>
typeof value === 'bigint' ? { __type: 'bigint', value: String(value) } : value,
parseReviver: (value) =>
isCollection(value) && '__type' in value && 'value' in value
? BigInt(value.value as string)
: value,
}
1 change: 1 addition & 0 deletions custom-component-library/components/BigInt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './definition'
26 changes: 26 additions & 0 deletions custom-component-library/components/BooleanToggle/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Boolean Toggle
*/

import React from 'react'
import { toPathString, type CustomNodeProps } from '@json-edit-react'

export const BooleanToggleComponent: React.FC<CustomNodeProps> = (props) => {
const { nodeData, value, handleEdit, canEdit } = props
const { path } = nodeData
return (
<input
className="jer-input-boolean"
type="checkbox"
disabled={!canEdit}
name={toPathString(path)}
checked={value as boolean}
onChange={() => {
// In this case we submit the data value immediately, not just the local
// state
handleEdit(!nodeData.value)
// setValue(!value)
}}
/>
)
}
13 changes: 13 additions & 0 deletions custom-component-library/components/BooleanToggle/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { type CustomNodeDefinition } from '@json-edit-react'
import { BooleanToggleComponent } from './component'

export const BooleanToggleDefinition: CustomNodeDefinition<{
linkStyles?: React.CSSProperties
stringTruncate?: number
}> = {
condition: ({ value }) => typeof value === 'boolean',
element: BooleanToggleComponent,
showOnView: true,
showOnEdit: false,
showEditTools: true,
}
1 change: 1 addition & 0 deletions custom-component-library/components/BooleanToggle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './definition'
36 changes: 31 additions & 5 deletions custom-component-library/components/DateObject/component.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import React, { useRef } from 'react'
import { StringDisplay, toPathString, StringEdit, type CustomNodeProps } from 'json-edit-react'
import { StringDisplay, toPathString, StringEdit, type CustomNodeProps } from '@json-edit-react'

export const DateObjectCustomComponent: React.FC<CustomNodeProps<unknown>> = (props) => {
const { nodeData, isEditing, setValue, getStyles, canEdit, value, handleEdit, onError } = props
export interface DateObjectProps {
showTime?: boolean
}

export const DateObjectCustomComponent: React.FC<CustomNodeProps<DateObjectProps>> = (props) => {
const {
nodeData,
isEditing,
setValue,
getStyles,
canEdit,
value,
handleEdit,
onError,
customNodeProps = {},
} = props
const lastValidDate = useRef(value)

const { showTime = true } = customNodeProps

if (value instanceof Date) lastValidDate.current = value

const editDisplayValue =
value instanceof Date
? showTime
? value.toISOString()
: value.toDateString()
: (value as string)
const displayValue = showTime
? (nodeData.value as Date).toLocaleString()
: (nodeData.value as Date).toLocaleDateString()

return isEditing ? (
<StringEdit
styles={getStyles('input', nodeData)}
pathString={toPathString(nodeData.path)}
{...props}
value={value instanceof Date ? value.toISOString() : (value as string)}
value={editDisplayValue}
setValue={setValue as React.Dispatch<React.SetStateAction<string>>}
handleEdit={() => {
const newDate = new Date(value as string)
Expand All @@ -32,7 +58,7 @@ export const DateObjectCustomComponent: React.FC<CustomNodeProps<unknown>> = (pr
styles={getStyles('string', nodeData)}
canEdit={canEdit}
pathString={toPathString(nodeData.path)}
value={nodeData.value.toLocaleString()}
value={displayValue}
/>
)
}
6 changes: 3 additions & 3 deletions custom-component-library/components/DateObject/definition.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DateObjectCustomComponent } from './component'
import { type CustomNodeDefinition } from 'json-edit-react'
import { DateObjectCustomComponent, DateObjectProps } from './component'
import { type CustomNodeDefinition } from '@json-edit-react'

export const DateObjectDefinition: CustomNodeDefinition = {
export const DateObjectDefinition: CustomNodeDefinition<DateObjectProps> = {
condition: (nodeData) => nodeData.value instanceof Date,
element: DateObjectCustomComponent,
showEditTools: true,
Expand Down
27 changes: 27 additions & 0 deletions custom-component-library/components/DatePicker/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react'

// Define the props interface for the Button component
interface ButtonProps {
color?: string
textColor?: string
text?: string
onClick?: () => void
}

export const Button: React.FC<ButtonProps> = ({
color = 'rgb(49, 130, 206)',
textColor = 'white',
text = 'Button',
onClick = () => {},
}) => {
const buttonBaseStyles: React.CSSProperties = {
backgroundColor: color,
color: textColor,
}

return (
<button className="jer-button" style={buttonBaseStyles} onClick={onClick}>
{text}
</button>
)
}
Loading