Skip to content

Commit 06d05b8

Browse files
authored
#203 Option to render custom object as "Value" (#204)
* Evaluate custom nodes in parent, create "Enhanced Link" component * Update README.md
1 parent dc278d2 commit 06d05b8

File tree

15 files changed

+176
-21
lines changed

15 files changed

+176
-21
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ A highly-configurable [React](https://github.com/facebook/react) component for e
7575
- [Active hyperlinks](#active-hyperlinks)
7676
- [Handling JSON](#handling-json)
7777
- [Custom Collection nodes](#custom-collection-nodes)
78+
- [Displaying Collections as Values](#displaying-collections-as-values)
7879
- [Custom Text](#custom-text)
7980
- [Custom Buttons](#custom-buttons)
8081
- [Keyboard customisation](#keyboard-customisation)
@@ -946,6 +947,7 @@ Custom nodes are provided in the `customNodeDefinitions` prop, as an array of ob
946947
showCollectionWrapper // boolean (optional), default true
947948
wrapperElement // React component (optional) to wrap *outside* the normal collection wrapper
948949
wrapperProps // object (optional) -- props for the above wrapper component
950+
renderCollectionAsValue // For special "object" data that should be treated like a "Value" node
949951
950952
// For JSON conversion -- only needed if editing as JSON text
951953
stringifyReplacer // function for stringifying to JSON (if non-JSON data type)
@@ -963,7 +965,7 @@ By default, your component will be presented to the right of the property key it
963965
964966
Also, by default, your component will be treated as a "display" element, i.e. it will appear in the JSON viewer, but when editing, it will revert to the standard editing interface. This can be changed, however, with the `showOnEdit`, `showOnView` and `showEditTools` props. For example, a Date picker might only be required when *editing* and left as-is for display. The `showEditTools` prop refers to the editing icons (copy, add, edit, delete) that appear to the right of each value on hover. If you choose to disable these but you still want to your component to have an "edit" mode, you'll have to provide your own UI mechanism to toggle editing.
965967
966-
You can allow users to create new instances of your special nodes by selecting them as a "Type" in the types selector when editing/adding values. Set `showInTypesSelector: true` to enable this. However, if this is enabled you need to also provide a `name` (which is what the user will see in the selector) and a `defaultValue` which is the data that is inserted when the user selects this "type". (The `defaultValue` must return `true` if passed through the `condition` function in order for it to be immediately displayed using your custom component.)
968+
You can allow users to create new instances of your special nodes by selecting them as a "Type" in the types selector when editing/adding values. Set `showInTypesSelector: true` to enable this. However, if this is enabled you need to *also* provide a `name` (which is what the user will see in the selector) and a `defaultValue` which is the data that is inserted when the user selects this "type". (The `defaultValue` must return `true` if passed through the `condition` function in order for it to be immediately displayed using your custom component.)
967969
968970
### Active hyperlinks
969971
@@ -1002,6 +1004,15 @@ In most cases it will be preferable (and simpler) to create custom nodes to matc
10021004
<img width="450" alt="custom node levels" src="image/custom_component_levels.png">
10031005
- There is one additional prop, `showCollectionWrapper` (default `true`), which, when set to `false`, hides the surrounding collection elements (namely the hide/show chevron and the brackets). In this case, you would have to provide your own hide/show mechanism in your component should you want it.
10041006
1007+
### Displaying Collections as Values
1008+
1009+
If you have a specialised `object` that you would like to display as though it were a regular "value" -- for example, a JavaScript [`Date` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) -- you can set the `renderCollectionAsValue` to `true`. This passes the entire object as a value rather than being rendered as a "collection" of key-value pairs, but you'll have to make sure your custom component handles it appropriately.
1010+
1011+
There are two examples in the [Custom Component Library](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md):
1012+
1013+
- [Date Object](#add-link)
1014+
- ["Enhanced" link](#add-link) (object with "url" and "text" fields, displayed as clickable string)
1015+
10051016
10061017
## Custom Text
10071018

custom-component-library/components/DateObject/definition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const DateObjectDefinition: CustomNodeDefinition<DateObjectProps> = {
99
name: 'Date Object', // shown in the Type selector menu
1010
showInTypesSelector: true,
1111
defaultValue: new Date(),
12+
renderCollectionAsValue: true,
1213
// IMPORTANT: This component can't be used in conjunction with a ISO string
1314
// matcher (such as the DatePicker in this repo) -- because JSON.stringify
1415
// automatically serializes Date objects to ISO Strings, there's no way to
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* A custom "URL" renderer -- an object that has "text" and "url" properties,
3+
* but is displayed as a clickable string
4+
*/
5+
6+
import React, { useState } from 'react'
7+
import { toPathString, StringDisplay, StringEdit, type CustomNodeProps } from '@json-edit-react'
8+
9+
export interface EnhancedLinkProps {
10+
linkStyles?: React.CSSProperties
11+
propertyStyles?: React.CSSProperties
12+
labels?: { text: string; url: string }
13+
fieldNames?: { text: string; url: string }
14+
stringTruncate?: number
15+
[key: string]: unknown
16+
}
17+
18+
type EnhancedLink = {
19+
[key: string]: string
20+
}
21+
22+
export const EnhancedLinkCustomComponent: React.FC<CustomNodeProps<EnhancedLinkProps>> = (
23+
props
24+
) => {
25+
const { setIsEditing, getStyles, nodeData, customNodeProps = {}, isEditing, handleEdit } = props
26+
const {
27+
linkStyles = { fontWeight: 'bold', textDecoration: 'underline' },
28+
propertyStyles = {},
29+
labels: { text: textLabel, url: urlLabel } = { text: 'Text', url: 'Link' },
30+
fieldNames: { text: textField, url: urlField } = { text: 'text', url: 'url' },
31+
stringTruncate = 120,
32+
} = customNodeProps
33+
const [text, setText] = useState((nodeData.value as EnhancedLink)[textField])
34+
const [url, setUrl] = useState((nodeData.value as EnhancedLink)[urlField])
35+
36+
const styles = getStyles('string', nodeData)
37+
38+
return (
39+
<div
40+
onClick={(e) => {
41+
if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true)
42+
}}
43+
style={styles}
44+
>
45+
{isEditing ? (
46+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.3em', marginTop: '0.4em' }}>
47+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
48+
<span style={propertyStyles}>{textLabel}:</span>
49+
<StringEdit
50+
styles={getStyles('input', nodeData)}
51+
pathString={toPathString(nodeData.path)}
52+
{...props}
53+
value={text}
54+
setValue={(val) => setText(val)}
55+
/>
56+
</div>
57+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5em' }}>
58+
<span style={propertyStyles}>{urlLabel}:</span>
59+
<StringEdit
60+
styles={getStyles('input', nodeData)}
61+
pathString={toPathString(nodeData.path)}
62+
{...props}
63+
value={url}
64+
setValue={(val) => setUrl(val)}
65+
handleEdit={() => {
66+
handleEdit({ [textField]: text, [urlField]: url })
67+
}}
68+
/>
69+
</div>
70+
</div>
71+
) : (
72+
<StringDisplay
73+
{...props}
74+
pathString={toPathString(nodeData.path)}
75+
styles={{ ...styles }}
76+
value={text}
77+
stringTruncate={stringTruncate}
78+
TextWrapper={({ children }) => {
79+
return (
80+
<a
81+
href={url}
82+
target="_blank"
83+
rel="noreferrer"
84+
style={{ ...styles, ...linkStyles, cursor: 'pointer' }}
85+
>
86+
{children}
87+
</a>
88+
)
89+
}}
90+
/>
91+
)}
92+
</div>
93+
)
94+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { isCollection, type CustomNodeDefinition } from '@json-edit-react'
2+
import { EnhancedLinkCustomComponent, EnhancedLinkProps } from './component'
3+
4+
const TEXT_FIELD = 'text'
5+
const URL_FIELD = 'url'
6+
7+
export const EnhancedLinkCustomNodeDefinition: CustomNodeDefinition<EnhancedLinkProps> = {
8+
condition: ({ value }) => isCollection(value) && TEXT_FIELD in value && URL_FIELD in value,
9+
element: EnhancedLinkCustomComponent,
10+
name: 'Enhanced Link', // shown in the Type selector menu
11+
showInTypesSelector: true,
12+
defaultValue: {
13+
[TEXT_FIELD]: 'This is the text that is displayed',
14+
[URL_FIELD]: 'https://link.goes.here',
15+
},
16+
customNodeProps: { fieldNames: { text: TEXT_FIELD, url: URL_FIELD } },
17+
showOnEdit: true,
18+
renderCollectionAsValue: true,
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './definition'

custom-component-library/components/data.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
export const testData = {
7-
intro: `# json-edit-react
7+
Intro: `# json-edit-react
88
99
## Custom Component library
1010
@@ -22,14 +22,18 @@ export const testData = {
2222
Click [here](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md) for more info
2323
`,
2424
'Active Links': {
25-
url: 'https://carlosnz.github.io/json-edit-react/',
26-
longUrl:
25+
Url: 'https://carlosnz.github.io/json-edit-react/',
26+
'Long URL':
2727
'https://www.google.com/maps/place/Sky+Tower/@-36.8465603,174.7609398,818m/data=!3m1!1e3!4m6!3m5!1s0x6d0d47f06d4bdc25:0x2d1b5c380ad9387!8m2!3d-36.848448!4d174.762191!16zL20vMDFuNXM2?entry=ttu&g_ep=EgoyMDI1MDQwOS4wIKXMDSoASAFQAw%3D%3D',
28+
'Enhanced Link': {
29+
text: 'This link displays custom text',
30+
url: 'https://github.com/CarlosNZ/json-edit-react/tree/main/custom-component-library#custom-component-library',
31+
},
2832
},
2933
'Date & Time': {
3034
Date: new Date().toISOString(),
3135
'Show Time in Date?': true,
32-
info: 'Date is stored as ISO string. To use JS Date objects, set STORE_DATE_AS_DATE_OBJECT to true in App.tsx.',
36+
info: 'Replaced in App.tsx',
3337
},
3438

3539
'Non-JSON types': {

custom-component-library/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './Hyperlink'
2+
export * from './EnhancedLink'
23
export * from './DateObject'
34
export * from './Undefined'
45
export * from './DatePicker'

custom-component-library/src/App.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ import {
1212
SymbolDefinition,
1313
BigIntDefinition,
1414
MarkdownNodeDefinition,
15+
EnhancedLinkCustomNodeDefinition,
1516
} from '../components'
1617
import { testData } from '../components/data'
1718
import { JsonData, JsonEditor } from '@json-edit-react'
1819

19-
// @ts-expect-error redefine after initialisation
20-
testData['Date & Time'].Date = STORE_DATE_AS_DATE_OBJECT ? new Date() : new Date().toISOString()
20+
if (testData?.['Date & Time']) {
21+
// @ts-expect-error redefine after initialisation
22+
testData['Date & Time'].Date = STORE_DATE_AS_DATE_OBJECT ? new Date() : new Date().toISOString()
23+
24+
testData['Date & Time'].info = STORE_DATE_AS_DATE_OBJECT
25+
? 'Date is stored a JS Date object. To use ISO string, set STORE_DATE_AS_DATE_OBJECT to false in App.tsx.'
26+
: 'Date is stored as ISO string. To use JS Date objects, set STORE_DATE_AS_DATE_OBJECT to true in App.tsx.'
27+
}
2128

2229
type TestData = typeof testData
2330

@@ -36,8 +43,11 @@ function App() {
3643
LinkCustomNodeDefinition,
3744
{
3845
...(STORE_DATE_AS_DATE_OBJECT ? DateObjectDefinition : DatePickerDefinition),
39-
customNodeProps: { showTime: (data as TestData)['Date & Time']['Show Time in Date?'] },
46+
customNodeProps: {
47+
showTime: (data as TestData)?.['Date & Time']?.['Show Time in Date?'] ?? false,
48+
},
4049
},
50+
EnhancedLinkCustomNodeDefinition,
4151
UndefinedDefinition,
4252
BooleanToggleDefinition,
4353
NanDefinition,
@@ -49,7 +59,7 @@ function App() {
4959
},
5060
{
5161
...MarkdownNodeDefinition,
52-
condition: ({ key }) => key === 'intro',
62+
condition: ({ key }) => key === 'Intro',
5363
hideKey: true,
5464
},
5565
]}

src/CollectionNode.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useMemo, useRef, useCallback } from 'react'
1+
import React, { useEffect, useState, useRef, useCallback } from 'react'
22
import { ValueNodeWrapper } from './ValueNodeWrapper'
33
import { EditButtons, InputButtons } from './ButtonPanels'
44
import { getCustomNode } from './CustomNode'
@@ -56,6 +56,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
5656
newKeyOptions,
5757
translate,
5858
customNodeDefinitions,
59+
customNodeData,
5960
jsonParse,
6061
jsonStringify,
6162
TextEditor,
@@ -163,10 +164,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
163164
showOnEdit,
164165
showOnView,
165166
showCollectionWrapper = true,
166-
} = useMemo(
167-
() => getCustomNode(customNodeDefinitions, nodeData),
168-
[nodeData, customNodeDefinitions]
169-
)
167+
} = customNodeData
170168

171169
const childrenEditing = areChildrenBeingEdited(pathString)
172170

@@ -304,13 +302,16 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
304302
parentData: data,
305303
fullData: nodeData.fullData,
306304
}
305+
306+
const childCustomNodeData = getCustomNode(customNodeDefinitions, childNodeData)
307+
307308
return (
308309
<div
309310
className="jer-collection-element"
310311
key={key}
311312
style={getStyles('collectionElement', childNodeData)}
312313
>
313-
{isCollection(value) ? (
314+
{isCollection(value) && !childCustomNodeData?.renderCollectionAsValue ? (
314315
<CollectionNode
315316
key={key}
316317
{...props}
@@ -319,6 +320,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
319320
nodeData={childNodeData}
320321
showCollectionCount={showCollectionCount}
321322
canDragOnto={canEdit}
323+
customNodeData={childCustomNodeData}
322324
/>
323325
) : (
324326
<ValueNodeWrapper
@@ -329,6 +331,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
329331
nodeData={childNodeData}
330332
canDragOnto={canEdit}
331333
showLabel={collectionType === 'object' ? true : showArrayIndices}
334+
customNodeData={childCustomNodeData}
332335
/>
333336
)}
334337
</div>

src/CustomNode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface CustomNodeData {
1414
showEditTools?: boolean
1515
showCollectionWrapper?: boolean
1616
passOriginalNode?: boolean
17+
renderCollectionAsValue?: boolean
1718
}
1819

1920
// Fetches matching custom nodes (based on condition filter) from custom node

src/JsonEditor.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { getTranslateFunction } from './localisation'
3333
import { ValueNodeWrapper } from './ValueNodeWrapper'
3434

3535
import './style.css'
36+
import { getCustomNode } from './CustomNode'
3637

3738
const Editor: React.FC<JsonEditorProps> = ({
3839
data: srcData,
@@ -336,6 +337,8 @@ const Editor: React.FC<JsonEditorProps> = ({
336337
[keySort]
337338
)
338339

340+
const customNodeData = getCustomNode(customNodeDefinitions, nodeData)
341+
339342
const otherProps = {
340343
mainContainerRef: mainContainerRef as React.MutableRefObject<Element>,
341344
name: rootName,
@@ -369,6 +372,7 @@ const Editor: React.FC<JsonEditorProps> = ({
369372
stringTruncate,
370373
translate,
371374
customNodeDefinitions,
375+
customNodeData,
372376
customButtons,
373377
parentData: null,
374378
jsonParse: jsonParseReplacement,
@@ -401,7 +405,7 @@ const Editor: React.FC<JsonEditorProps> = ({
401405
className={`jer-editor-container ${className ?? ''}`}
402406
style={mainContainerStyles}
403407
>
404-
{isCollection(data) ? (
408+
{isCollection(data) && !customNodeData.renderCollectionAsValue ? (
405409
<CollectionNode data={data} {...otherProps} />
406410
) : (
407411
<ValueNodeWrapper data={data as ValueData} showLabel {...otherProps} />

src/KeyDisplay.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export const KeyDisplay: React.FC<KeyDisplayProps> = ({
4040
}) => {
4141
const { setCurrentlyEditingElement } = useTreeState()
4242

43+
const displayEmptyStringText = name === '' && path.length > 0
44+
const displayColon = name !== '' || path.length > 0
45+
4346
if (!isEditingKey)
4447
return (
4548
<span
@@ -52,14 +55,14 @@ export const KeyDisplay: React.FC<KeyDisplayProps> = ({
5255
onDoubleClick={() => canEditKey && setCurrentlyEditingElement(path, 'key')}
5356
onClick={handleClick}
5457
>
55-
{name === '' ? (
58+
{displayEmptyStringText ? (
5659
<span className={path.length > 0 ? 'jer-empty-string' : undefined}>
5760
{/* display "<empty string>" using pseudo class CSS */}
5861
</span>
5962
) : (
6063
`${name}`
6164
)}
62-
<span className="jer-key-colon">:</span>
65+
{displayColon ? <span className="jer-key-colon">:</span> : null}
6366
</span>
6467
)
6568

0 commit comments

Comments
 (0)