diff --git a/.gitignore b/.gitignore index 5277e492..58f78d66 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 9fbd07cd..ff8c77db 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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: @@ -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) } ``` @@ -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: @@ -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) diff --git a/custom-component-library/README.md b/custom-component-library/README.md index 4f6d0ccb..c1ea6929 100644 --- a/custom-component-library/README.md +++ b/custom-component-library/README.md @@ -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 @@ -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 + diff --git a/custom-component-library/components/BigInt/component.tsx b/custom-component-library/components/BigInt/component.tsx new file mode 100644 index 00000000..64a838d1 --- /dev/null +++ b/custom-component-library/components/BigInt/component.tsx @@ -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> = (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 ? ( + >} + {...rest} + handleEdit={() => { + handleEdit(BigInt(nodeData.value as string)) + }} + /> + ) : ( + {value as bigint} + ) +} diff --git a/custom-component-library/components/BigInt/definition.ts b/custom-component-library/components/BigInt/definition.ts new file mode 100644 index 00000000..6214d3a6 --- /dev/null +++ b/custom-component-library/components/BigInt/definition.ts @@ -0,0 +1,20 @@ +import { isCollection, type CustomNodeDefinition } from '@json-edit-react' +import { BigIntComponent, BigIntProps } from './component' + +export const BigIntDefinition: CustomNodeDefinition = { + 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, +} diff --git a/custom-component-library/components/BigInt/index.ts b/custom-component-library/components/BigInt/index.ts new file mode 100644 index 00000000..75c618f1 --- /dev/null +++ b/custom-component-library/components/BigInt/index.ts @@ -0,0 +1 @@ +export * from './definition' diff --git a/custom-component-library/components/BooleanToggle/component.tsx b/custom-component-library/components/BooleanToggle/component.tsx new file mode 100644 index 00000000..1da7e72d --- /dev/null +++ b/custom-component-library/components/BooleanToggle/component.tsx @@ -0,0 +1,26 @@ +/** + * Boolean Toggle + */ + +import React from 'react' +import { toPathString, type CustomNodeProps } from '@json-edit-react' + +export const BooleanToggleComponent: React.FC = (props) => { + const { nodeData, value, handleEdit, canEdit } = props + const { path } = nodeData + return ( + { + // In this case we submit the data value immediately, not just the local + // state + handleEdit(!nodeData.value) + // setValue(!value) + }} + /> + ) +} diff --git a/custom-component-library/components/BooleanToggle/definition.ts b/custom-component-library/components/BooleanToggle/definition.ts new file mode 100644 index 00000000..76af1902 --- /dev/null +++ b/custom-component-library/components/BooleanToggle/definition.ts @@ -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, +} diff --git a/custom-component-library/components/BooleanToggle/index.ts b/custom-component-library/components/BooleanToggle/index.ts new file mode 100644 index 00000000..75c618f1 --- /dev/null +++ b/custom-component-library/components/BooleanToggle/index.ts @@ -0,0 +1 @@ +export * from './definition' diff --git a/custom-component-library/components/DateObject/component.tsx b/custom-component-library/components/DateObject/component.tsx index 25435046..939d507a 100644 --- a/custom-component-library/components/DateObject/component.tsx +++ b/custom-component-library/components/DateObject/component.tsx @@ -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> = (props) => { - const { nodeData, isEditing, setValue, getStyles, canEdit, value, handleEdit, onError } = props +export interface DateObjectProps { + showTime?: boolean +} + +export const DateObjectCustomComponent: React.FC> = (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 ? ( >} handleEdit={() => { const newDate = new Date(value as string) @@ -32,7 +58,7 @@ export const DateObjectCustomComponent: React.FC> = (pr styles={getStyles('string', nodeData)} canEdit={canEdit} pathString={toPathString(nodeData.path)} - value={nodeData.value.toLocaleString()} + value={displayValue} /> ) } diff --git a/custom-component-library/components/DateObject/definition.ts b/custom-component-library/components/DateObject/definition.ts index 165819a6..3dee5cdb 100644 --- a/custom-component-library/components/DateObject/definition.ts +++ b/custom-component-library/components/DateObject/definition.ts @@ -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 = { condition: (nodeData) => nodeData.value instanceof Date, element: DateObjectCustomComponent, showEditTools: true, diff --git a/custom-component-library/components/DatePicker/Button.tsx b/custom-component-library/components/DatePicker/Button.tsx new file mode 100644 index 00000000..f8c01d0e --- /dev/null +++ b/custom-component-library/components/DatePicker/Button.tsx @@ -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 = ({ + color = 'rgb(49, 130, 206)', + textColor = 'white', + text = 'Button', + onClick = () => {}, +}) => { + const buttonBaseStyles: React.CSSProperties = { + backgroundColor: color, + color: textColor, + } + + return ( + + ) +} diff --git a/demo/src/customComponents/DateTimePicker.tsx b/custom-component-library/components/DatePicker/component.tsx similarity index 63% rename from demo/src/customComponents/DateTimePicker.tsx rename to custom-component-library/components/DatePicker/component.tsx index 3af40575..c449ef8d 100644 --- a/demo/src/customComponents/DateTimePicker.tsx +++ b/custom-component-library/components/DatePicker/component.tsx @@ -10,17 +10,17 @@ import React from 'react' import DatePicker from 'react-datepicker' -import { Button } from '@chakra-ui/react' -import { CustomNodeProps, CustomNodeDefinition } from '../imports' +import { Button } from './Button' +import { CustomNodeProps } from '@json-edit-react' // Styles import 'react-datepicker/dist/react-datepicker.css' -// For better matching with Chakra-UI import './style.css' -interface DatePickerCustomProps { - dateFormat: string - showTimeSelect: boolean +export interface DatePickerCustomProps { + dateFormat?: string + dateTimeFormat?: string + showTime?: boolean } export const DateTimePicker: React.FC> = ({ @@ -35,7 +35,11 @@ export const DateTimePicker: React.FC> = nodeData, customNodeProps, }) => { - const { dateFormat = 'MMM d, yyyy h:mm aa', showTimeSelect = true } = customNodeProps ?? {} + const { + dateFormat = 'MMM d, yyyy', + dateTimeFormat = 'MMM d, yyyy h:mm aa', + showTime = true, + } = customNodeProps ?? {} const date = new Date(value as string) @@ -56,8 +60,8 @@ export const DateTimePicker: React.FC> = // @ts-expect-error -- isNan can take any input selected={isNaN(date) ? null : date} - showTimeSelect={showTimeSelect} - dateFormat={dateFormat} + showTimeSelect={showTime} + dateFormat={showTime ? dateTimeFormat : dateFormat} onChange={(date: Date | null) => date && setValue(date.toISOString())} open={true} onKeyDown={handleKeyPress} @@ -66,12 +70,8 @@ export const DateTimePicker: React.FC> = {/* These buttons are not really necessary -- you can either use the standard Ok/Cancel icons, or keyboard Enter/Esc, but shown for demo purposes */} - - +