Skip to content

Commit 7ee6e96

Browse files
authored
#42 Custom collection node fix (#43)
1 parent fba2fce commit 7ee6e96

File tree

9 files changed

+207
-237
lines changed

9 files changed

+207
-237
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ module.exports = {
4343
'react-hooks/exhaustive-deps': 0,
4444
'@typescript-eslint/prefer-nullish-coalescing': 0,
4545
'@typescript-eslint/no-unsafe-argument': 'warn',
46+
'@typescript-eslint/indent': 0,
4647
},
4748
}

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Features include:
3232
- [Icons](#icons)
3333
- [Localisation](#localisation)
3434
- [Custom Nodes](#custom-nodes)
35+
- [Custom Collection nodes](#custom-collection-nodes)
3536
- [Active hyperlinks](#active-hyperlinks)
3637
- [Custom Text](#custom-text)
3738
- [Undo functionality](#undo-functionality)
@@ -423,10 +424,12 @@ Custom nodes are provided in the `customNodeDefinitions` prop, as an array of ob
423424
showEditTools // boolean, default true
424425
name // string (appears in Types selector)
425426
showInTypesSelector, // boolean (optional), default false
427+
// Only affects Collection nodes:
428+
showCollectionWrapper // boolean (optional), default true
426429
}
427430
```
428431
429-
The `condition` is just a [Filter function](#filter-functions), with the same input parameters (`key`, `path`, `level`, `value`, `size`), and `element` is a React component. Every node in the data structure will be run through each condition function, and any that match will be replaced by your custom component. Note that if a node matches more than one custom definition conditions (from multiple components), the *first* one will be used, so place them in the array in priority order.
432+
The `condition` is just a [Filter function](#filter-functions), with the same input parameters (`key`, `path`, `value`, etc.), and `element` is a React component. Every node in the data structure will be run through each condition function, and any that match will be replaced by your custom component. Note that if a node matches more than one custom definition conditions (from multiple components), the *first* one will be used, so place them in the array in priority order.
430433
431434
The component will receive *all* the same props as a standard node component (see codebase), but you can pass additional props to your component if required through the `customNodeProps` object. A thorough example of a custom Date picker is used in the demo (along with a couple of other more basic presentational ones), which you can inspect to see how to utilise the standard props and a couple of custom props. View the source code [here](https://github.com/CarlosNZ/json-edit-react/blob/main/demo/src/customComponents/DateTimePicker.tsx)
432435
@@ -436,6 +439,12 @@ Also, by default, your component will be treated as a "display" element, i.e. it
436439
437440
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.)
438441
442+
### Custom Collection nodes
443+
444+
In most cases it will be preferable to create custom nodes to match *value* nodes (i.e. not `array` or `object` *collection* nodes). However, if you do wish to replace a whole collection, there are a couple of other things to know:
445+
- The descendants of this node can still be displayed using the [React `children`](https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children) property, it just becomes your component's responsibility to handle it.
446+
- There is one additional prop, `showCollectionWrapper` (default `true`), which, when set to `false`, hides the surrounding "wrapper", namely the hide/show chevron and the brackets. In this case, you would have to provide your own hide/show mechanism in your component.
447+
439448
### Active hyperlinks
440449
441450
A simple custom component and definition to turn url strings into clickable links is provided with the main package for you to use out of the box. Just import and use like so:

demo/src/App.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,25 @@ function App() {
8787

8888
const restrictEdit: FilterFunction | boolean = (() => {
8989
const customRestrictor = demoData[selectedData]?.restrictEdit
90-
if (customRestrictor) return (input) => !allowEdit || customRestrictor(input)
90+
if (typeof customRestrictor === 'function')
91+
return (input) => !allowEdit || customRestrictor(input)
92+
if (customRestrictor !== undefined) return customRestrictor
9193
return !allowEdit
9294
})()
9395

9496
const restrictDelete: FilterFunction | boolean = (() => {
9597
const customRestrictor = demoData[selectedData]?.restrictDelete
96-
if (customRestrictor) return (input) => !allowDelete || customRestrictor(input)
98+
if (typeof customRestrictor === 'function')
99+
return (input) => !allowDelete || customRestrictor(input)
100+
if (customRestrictor !== undefined) return customRestrictor
97101
return !allowDelete
98102
})()
99103

100104
const restrictAdd: FilterFunction | boolean = (() => {
101105
const customRestrictor = demoData[selectedData]?.restrictAdd
102-
if (customRestrictor) return (input) => !allowAdd || customRestrictor(input)
106+
if (typeof customRestrictor === 'function')
107+
return (input) => !allowAdd || customRestrictor(input)
108+
if (customRestrictor !== undefined) return customRestrictor
103109
return !allowAdd
104110
})()
105111

@@ -277,7 +283,7 @@ function App() {
277283
keySort={sortKeys}
278284
defaultValue={demoData[selectedData]?.defaultValue ?? defaultNewValue}
279285
showArrayIndices={showIndices}
280-
minWidth={450}
286+
minWidth={'min(500px, 95vw)'}
281287
maxWidth="min(650px, 90vw)"
282288
className="block-shadow"
283289
stringTruncate={90}
@@ -434,21 +440,28 @@ function App() {
434440
<Flex w="100%" justify="flex-start">
435441
<Checkbox
436442
isChecked={allowEdit}
443+
disabled={demoData[selectedData].restrictEdit !== undefined}
437444
onChange={() => setAllowEdit(!allowEdit)}
438445
w="50%"
439446
>
440447
Allow Edit
441448
</Checkbox>
442449
<Checkbox
443450
isChecked={allowDelete}
451+
disabled={demoData[selectedData].restrictDelete !== undefined}
444452
onChange={() => setAllowDelete(!allowDelete)}
445453
w="50%"
446454
>
447455
Allow Delete
448456
</Checkbox>
449457
</Flex>
450458
<Flex w="100%" justify="flex-start">
451-
<Checkbox isChecked={allowAdd} onChange={() => setAllowAdd(!allowAdd)} w="50%">
459+
<Checkbox
460+
isChecked={allowAdd}
461+
disabled={demoData[selectedData].restrictAdd !== undefined}
462+
onChange={() => setAllowAdd(!allowAdd)}
463+
w="50%"
464+
>
452465
Allow Add
453466
</Checkbox>
454467
<Checkbox
@@ -477,6 +490,7 @@ function App() {
477490
</FormLabel>
478491
<Input
479492
className="inputWidth"
493+
disabled={demoData[selectedData].defaultValue !== undefined}
480494
type="text"
481495
value={defaultNewValue}
482496
onChange={(e) => setDefaultNewValue(e.target.value)}

demo/src/JsonEditImport.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,12 @@
1-
import {
2-
JsonEditor,
3-
themes,
4-
Theme,
5-
ThemeName,
6-
ThemeInput,
7-
CustomNodeProps,
8-
CustomNodeDefinition,
9-
CustomTextDefinitions,
10-
FilterFunction,
11-
LinkCustomComponent,
12-
LinkCustomNodeDefinition,
13-
matchNode,
14-
assign,
15-
// } from './json-edit-react/src'
16-
} from 'json-edit-react'
17-
// } from './package'
1+
/**
2+
* Quickly switch between importing from local src or installed package
3+
*/
184

19-
export {
20-
JsonEditor,
21-
themes,
22-
type Theme,
23-
type ThemeName,
24-
type ThemeInput,
25-
type CustomNodeProps,
26-
type CustomNodeDefinition,
27-
type CustomTextDefinitions,
28-
type FilterFunction,
29-
LinkCustomComponent,
30-
LinkCustomNodeDefinition,
31-
matchNode,
32-
assign,
33-
}
5+
/* Installed package */
6+
// export * from 'json-edit-react'
7+
8+
/* Local src */
9+
export * from './json-edit-react/src'
10+
11+
/* Compiled local package */
12+
// export * from './package'

demo/src/demoData/dataDefinitions.tsx

Lines changed: 27 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ interface DemoData {
2626
data: object
2727
rootName?: string
2828
collapse?: number
29-
restrictEdit?: FilterFunction
30-
restrictDelete?: FilterFunction
31-
restrictAdd?: FilterFunction
29+
restrictEdit?: boolean | FilterFunction
30+
restrictDelete?: boolean | FilterFunction
31+
restrictAdd?: boolean | FilterFunction
3232
restrictTypeSelection?: boolean | DataType[]
3333
searchFilter?: 'key' | 'value' | 'all' | SearchFilterFunction
3434
searchPlaceholder?: string
@@ -438,6 +438,9 @@ export const demoData: Record<string, DemoData> = {
438438
},
439439
searchPlaceholder: 'Search by character name',
440440
data: data.customNodes,
441+
restrictEdit: ({ level }) => level > 0,
442+
restrictAdd: true,
443+
restrictDelete: true,
441444
customNodeDefinitions: [
442445
{
443446
condition: ({ key, value }) =>
@@ -480,6 +483,23 @@ export const demoData: Record<string, DemoData> = {
480483
},
481484
hideKey: true,
482485
},
486+
// Uncomment to test a custom Collection node
487+
// {
488+
// condition: ({ key }) => key === 'portrayedBy',
489+
// element: ({ nodeData, data, getStyles }) => {
490+
// const styles = getStyles('string', nodeData)
491+
// return (
492+
// <ol style={{ ...styles, paddingLeft: '3em' }}>
493+
// {data.map((val) => (
494+
// <li key={val}>{val}</li>
495+
// ))}
496+
// </ol>
497+
// )
498+
// },
499+
// showEditTools: true,
500+
// // hideKey: true,
501+
// // showCollectionWrapper: false,
502+
// },
483503
{
484504
...dateNodeDefinition,
485505
showOnView: true,
@@ -491,102 +511,20 @@ export const demoData: Record<string, DemoData> = {
491511
ITEM_SINGLE: ({ key, value, size }) => {
492512
if (value instanceof Object && 'name' in value)
493513
return `${value.name} (${(value as any)?.publisher ?? ''})`
494-
if (key === 'aliases' && Array.isArray(value))
495-
return `${size} ${size === 1 ? 'name' : 'names'}`
514+
if (key === 'aliases' && Array.isArray(value)) return `One name`
515+
if (key === 'portrayedBy' && Array.isArray(value)) return `One actor`
496516
return null
497517
},
498518
ITEMS_MULTIPLE: ({ key, value, size }) => {
499519
if (value instanceof Object && 'name' in value)
500520
return `${value.name} (${(value as any)?.publisher ?? ''})`
501-
if (key === 'aliases' && Array.isArray(value))
502-
return `${size} ${size === 1 ? 'name' : 'names'}`
521+
if (key === 'aliases' && Array.isArray(value)) return `${size} names`
522+
if (key === 'portrayedBy' && Array.isArray(value)) return `${size} actors`
503523
return null
504524
},
505525
},
506526
styles: {
507527
string: ({ key }) => (key === 'name' ? { fontWeight: 'bold', fontSize: '120%' } : null),
508528
},
509529
},
510-
// Enable to test more complex features of Custom nodes
511-
// testCustomNodes: {
512-
// name: '🔧 Custom Nodes',
513-
// description: (
514-
// <Flex flexDir="column" gap={2}>
515-
// <Text>
516-
// This data set shows <strong>Custom Nodes</strong> — you can provide your own components to
517-
// present specialised data in a unique way, or provide a more complex editing mechanism for
518-
// a specialised data structure, say.
519-
// </Text>
520-
// <Text>
521-
// In this example, compare the raw JSON (edit the data root) with what is presented here.
522-
// </Text>
523-
// <Text>
524-
// See the{' '}
525-
// <a href="https://github.com/CarlosNZ/json-edit-react#custom-nodes">Custom Nodes</a>{' '}
526-
// section of the documentation for more info.
527-
// </Text>
528-
// </Flex>
529-
// ),
530-
// rootName: 'Superheroes',
531-
// collapse: 2,
532-
// data: data.customNodes,
533-
// customNodeDefinitions: [
534-
// {
535-
// condition: ({ key, value }) =>
536-
// key === 'logo' &&
537-
// typeof value === 'string' &&
538-
// value.startsWith('http') &&
539-
// value.endsWith('.png'),
540-
// element: ({ data }) => {
541-
// const truncate = (string: string, length = 50) =>
542-
// string.length < length ? string : `${string.slice(0, length - 2).trim()}...`
543-
// return (
544-
// <div style={{ maxWidth: 250 }}>
545-
// <a href={data} target="_blank">
546-
// <img src={data} style={{ maxHeight: 75 }} />
547-
// <p style={{ fontSize: '0.75em' }}>{truncate(data)}</p>{' '}
548-
// </a>
549-
// </div>
550-
// )
551-
// },
552-
// },
553-
// {
554-
// condition: ({ key }) => key === 'publisher',
555-
// element: ({ data }) => {
556-
// return (
557-
// <p
558-
// style={{
559-
// padding: '0.5em 1em',
560-
// border: '2px solid red',
561-
// borderRadius: '1.5em',
562-
// backgroundColor: 'yellow',
563-
// marginTop: '0.5em',
564-
// marginRight: '1em',
565-
// fontFamily: 'sans-serif',
566-
// }}
567-
// >
568-
// Presented by: <strong>{data}</strong>
569-
// </p>
570-
// )
571-
// },
572-
// hideKey: true,
573-
// showEditTools: false,
574-
// },
575-
// {
576-
// condition: ({ key }) => key === 'aliases',
577-
// element: ({ data }) => {
578-
// return (
579-
// <ol style={{ paddingLeft: 50, color: 'orange' }}>
580-
// {data.map((val) => (
581-
// <li key={val}>{val}</li>
582-
// ))}
583-
// </ol>
584-
// )
585-
// },
586-
// // showOnEdit: true,
587-
// // showOnView: false,
588-
// // hideKey: true,
589-
// },
590-
// ],
591-
// },
592530
}

0 commit comments

Comments
 (0)