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 .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ module.exports = {
'react-hooks/exhaustive-deps': 0,
'@typescript-eslint/prefer-nullish-coalescing': 0,
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/indent': 0,
},
}
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Features include:
- [Icons](#icons)
- [Localisation](#localisation)
- [Custom Nodes](#custom-nodes)
- [Custom Collection nodes](#custom-collection-nodes)
- [Active hyperlinks](#active-hyperlinks)
- [Custom Text](#custom-text)
- [Undo functionality](#undo-functionality)
Expand Down Expand Up @@ -423,10 +424,12 @@ Custom nodes are provided in the `customNodeDefinitions` prop, as an array of ob
showEditTools // boolean, default true
name // string (appears in Types selector)
showInTypesSelector, // boolean (optional), default false
// Only affects Collection nodes:
showCollectionWrapper // boolean (optional), default true
}
```

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.
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.

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)

Expand All @@ -436,6 +439,12 @@ Also, by default, your component will be treated as a "display" element, i.e. it

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.)

### Custom Collection nodes

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:
- 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.
- 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.

### Active hyperlinks

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:
Expand Down
24 changes: 19 additions & 5 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,25 @@ function App() {

const restrictEdit: FilterFunction | boolean = (() => {
const customRestrictor = demoData[selectedData]?.restrictEdit
if (customRestrictor) return (input) => !allowEdit || customRestrictor(input)
if (typeof customRestrictor === 'function')
return (input) => !allowEdit || customRestrictor(input)
if (customRestrictor !== undefined) return customRestrictor
return !allowEdit
})()

const restrictDelete: FilterFunction | boolean = (() => {
const customRestrictor = demoData[selectedData]?.restrictDelete
if (customRestrictor) return (input) => !allowDelete || customRestrictor(input)
if (typeof customRestrictor === 'function')
return (input) => !allowDelete || customRestrictor(input)
if (customRestrictor !== undefined) return customRestrictor
return !allowDelete
})()

const restrictAdd: FilterFunction | boolean = (() => {
const customRestrictor = demoData[selectedData]?.restrictAdd
if (customRestrictor) return (input) => !allowAdd || customRestrictor(input)
if (typeof customRestrictor === 'function')
return (input) => !allowAdd || customRestrictor(input)
if (customRestrictor !== undefined) return customRestrictor
return !allowAdd
})()

Expand Down Expand Up @@ -277,7 +283,7 @@ function App() {
keySort={sortKeys}
defaultValue={demoData[selectedData]?.defaultValue ?? defaultNewValue}
showArrayIndices={showIndices}
minWidth={450}
minWidth={'min(500px, 95vw)'}
maxWidth="min(650px, 90vw)"
className="block-shadow"
stringTruncate={90}
Expand Down Expand Up @@ -434,21 +440,28 @@ function App() {
<Flex w="100%" justify="flex-start">
<Checkbox
isChecked={allowEdit}
disabled={demoData[selectedData].restrictEdit !== undefined}
onChange={() => setAllowEdit(!allowEdit)}
w="50%"
>
Allow Edit
</Checkbox>
<Checkbox
isChecked={allowDelete}
disabled={demoData[selectedData].restrictDelete !== undefined}
onChange={() => setAllowDelete(!allowDelete)}
w="50%"
>
Allow Delete
</Checkbox>
</Flex>
<Flex w="100%" justify="flex-start">
<Checkbox isChecked={allowAdd} onChange={() => setAllowAdd(!allowAdd)} w="50%">
<Checkbox
isChecked={allowAdd}
disabled={demoData[selectedData].restrictAdd !== undefined}
onChange={() => setAllowAdd(!allowAdd)}
w="50%"
>
Allow Add
</Checkbox>
<Checkbox
Expand Down Expand Up @@ -477,6 +490,7 @@ function App() {
</FormLabel>
<Input
className="inputWidth"
disabled={demoData[selectedData].defaultValue !== undefined}
type="text"
value={defaultNewValue}
onChange={(e) => setDefaultNewValue(e.target.value)}
Expand Down
43 changes: 11 additions & 32 deletions demo/src/JsonEditImport.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
import {
JsonEditor,
themes,
Theme,
ThemeName,
ThemeInput,
CustomNodeProps,
CustomNodeDefinition,
CustomTextDefinitions,
FilterFunction,
LinkCustomComponent,
LinkCustomNodeDefinition,
matchNode,
assign,
// } from './json-edit-react/src'
} from 'json-edit-react'
// } from './package'
/**
* Quickly switch between importing from local src or installed package
*/

export {
JsonEditor,
themes,
type Theme,
type ThemeName,
type ThemeInput,
type CustomNodeProps,
type CustomNodeDefinition,
type CustomTextDefinitions,
type FilterFunction,
LinkCustomComponent,
LinkCustomNodeDefinition,
matchNode,
assign,
}
/* Installed package */
// export * from 'json-edit-react'

/* Local src */
export * from './json-edit-react/src'

/* Compiled local package */
// export * from './package'
116 changes: 27 additions & 89 deletions demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ interface DemoData {
data: object
rootName?: string
collapse?: number
restrictEdit?: FilterFunction
restrictDelete?: FilterFunction
restrictAdd?: FilterFunction
restrictEdit?: boolean | FilterFunction
restrictDelete?: boolean | FilterFunction
restrictAdd?: boolean | FilterFunction
restrictTypeSelection?: boolean | DataType[]
searchFilter?: 'key' | 'value' | 'all' | SearchFilterFunction
searchPlaceholder?: string
Expand Down Expand Up @@ -438,6 +438,9 @@ export const demoData: Record<string, DemoData> = {
},
searchPlaceholder: 'Search by character name',
data: data.customNodes,
restrictEdit: ({ level }) => level > 0,
restrictAdd: true,
restrictDelete: true,
customNodeDefinitions: [
{
condition: ({ key, value }) =>
Expand Down Expand Up @@ -480,6 +483,23 @@ export const demoData: Record<string, DemoData> = {
},
hideKey: true,
},
// Uncomment to test a custom Collection node
// {
// condition: ({ key }) => key === 'portrayedBy',
// element: ({ nodeData, data, getStyles }) => {
// const styles = getStyles('string', nodeData)
// return (
// <ol style={{ ...styles, paddingLeft: '3em' }}>
// {data.map((val) => (
// <li key={val}>{val}</li>
// ))}
// </ol>
// )
// },
// showEditTools: true,
// // hideKey: true,
// // showCollectionWrapper: false,
// },
{
...dateNodeDefinition,
showOnView: true,
Expand All @@ -491,102 +511,20 @@ export const demoData: Record<string, DemoData> = {
ITEM_SINGLE: ({ key, value, size }) => {
if (value instanceof Object && 'name' in value)
return `${value.name} (${(value as any)?.publisher ?? ''})`
if (key === 'aliases' && Array.isArray(value))
return `${size} ${size === 1 ? 'name' : 'names'}`
if (key === 'aliases' && Array.isArray(value)) return `One name`
if (key === 'portrayedBy' && Array.isArray(value)) return `One actor`
return null
},
ITEMS_MULTIPLE: ({ key, value, size }) => {
if (value instanceof Object && 'name' in value)
return `${value.name} (${(value as any)?.publisher ?? ''})`
if (key === 'aliases' && Array.isArray(value))
return `${size} ${size === 1 ? 'name' : 'names'}`
if (key === 'aliases' && Array.isArray(value)) return `${size} names`
if (key === 'portrayedBy' && Array.isArray(value)) return `${size} actors`
return null
},
},
styles: {
string: ({ key }) => (key === 'name' ? { fontWeight: 'bold', fontSize: '120%' } : null),
},
},
// Enable to test more complex features of Custom nodes
// testCustomNodes: {
// name: '🔧 Custom Nodes',
// description: (
// <Flex flexDir="column" gap={2}>
// <Text>
// This data set shows <strong>Custom Nodes</strong> — you can provide your own components to
// present specialised data in a unique way, or provide a more complex editing mechanism for
// a specialised data structure, say.
// </Text>
// <Text>
// In this example, compare the raw JSON (edit the data root) with what is presented here.
// </Text>
// <Text>
// See the{' '}
// <a href="https://github.com/CarlosNZ/json-edit-react#custom-nodes">Custom Nodes</a>{' '}
// section of the documentation for more info.
// </Text>
// </Flex>
// ),
// rootName: 'Superheroes',
// collapse: 2,
// data: data.customNodes,
// customNodeDefinitions: [
// {
// condition: ({ key, value }) =>
// key === 'logo' &&
// typeof value === 'string' &&
// value.startsWith('http') &&
// value.endsWith('.png'),
// element: ({ data }) => {
// const truncate = (string: string, length = 50) =>
// string.length < length ? string : `${string.slice(0, length - 2).trim()}...`
// return (
// <div style={{ maxWidth: 250 }}>
// <a href={data} target="_blank">
// <img src={data} style={{ maxHeight: 75 }} />
// <p style={{ fontSize: '0.75em' }}>{truncate(data)}</p>{' '}
// </a>
// </div>
// )
// },
// },
// {
// condition: ({ key }) => key === 'publisher',
// element: ({ data }) => {
// return (
// <p
// style={{
// padding: '0.5em 1em',
// border: '2px solid red',
// borderRadius: '1.5em',
// backgroundColor: 'yellow',
// marginTop: '0.5em',
// marginRight: '1em',
// fontFamily: 'sans-serif',
// }}
// >
// Presented by: <strong>{data}</strong>
// </p>
// )
// },
// hideKey: true,
// showEditTools: false,
// },
// {
// condition: ({ key }) => key === 'aliases',
// element: ({ data }) => {
// return (
// <ol style={{ paddingLeft: 50, color: 'orange' }}>
// {data.map((val) => (
// <li key={val}>{val}</li>
// ))}
// </ol>
// )
// },
// // showOnEdit: true,
// // showOnView: false,
// // hideKey: true,
// },
// ],
// },
}
Loading