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
2 changes: 1 addition & 1 deletion .changeset/customnode-field-renames.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ Renamed the `CustomNodeDefinition` fields and props type for consistency, around
- **Visibility flags** (now all positive `show*`): `hideKey` → `showKey` (**polarity inverted** — `showKey` defaults to `true`), `showInTypesSelector` → `showInTypeSelector`.
- **Types**: `CustomNodeProps` → `CustomComponentProps` (the props your component receives; also resolves the long-standing `CustomNodeProps` / `CustomNodeDefinition` name clash). The new `CustomWrapperProps` types `wrapperComponent`, which now receives its config as `wrapperProps` (previously delivered as `customNodeProps`). `CustomNodeDefinition` and `CustomKeyProps` keep their names.

All 12 components in `@json-edit-react/components` use the new field names. Consumers spreading/overriding a shipped definition (e.g. `{ ...DatePickerDefinition, customNodeProps: {...} }`) must rename to `componentProps`, and custom-component bodies must rename the props type (`CustomNodeProps` → `CustomComponentProps`) and the config prop they destructure (`customNodeProps` → `componentProps`).
All 12 components in `@json-edit-react/components` use the new field names. Consumers overriding a shipped definition's `customNodeProps` must rename to `componentProps`, and custom-component bodies must rename the props type (`CustomNodeProps` → `CustomComponentProps`) and the config prop they destructure (`customNodeProps` → `componentProps`).

See the [migration guide](../migration-guide.md#13-customnodedefinition-field-renames) for the full mapping and before/after examples.
9 changes: 9 additions & 0 deletions .changeset/definition-factories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@json-edit-react/components': minor
---

Pre-built custom-node definitions are exported as **definition factories**: `hyperlinkDefinition()`, `enhancedLinkDefinition()`, `datePickerDefinition()`, `dateObjectDefinition()`, `colorPickerDefinition()`, `markdownDefinition()`, `imageDefinition()`, `booleanToggleDefinition()`, `bigIntDefinition()`, `nanDefinition()`, `symbolDefinition()`, `undefinedDefinition()`.

Calling a factory with no arguments yields the standard definition. Passing overrides customizes it without losing the built-in safety condition: a `condition` override is *targeting* — ANDed with the definition's guard, so e.g. `markdownDefinition({ condition: ({ key }) => key === 'description' })` can never match a value the component can't render — `componentProps` is shallow-merged with the defaults, any other field replaces its default, and the explicit `guard` key replaces the guard itself. The override surface is typed by the exported `DefinitionOverrides<T>`.

The underlying components and their props types (`MarkdownComponent`, `DateTimePicker`, `LinkCustomComponent`, …) are exported alongside the factories, for wrapping or use in fully hand-rolled definitions.
2 changes: 1 addition & 1 deletion .changeset/initial-components-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
Split custom components into a separate publishable package.

- New package: `@json-edit-react/components` ships 12 ready-to-use custom node components: `Hyperlink`, `EnhancedLink`, `DatePicker`, `DateObject`, `ColorPicker`, `Markdown`, `Image`, `BooleanToggle`, `BigInt`, `NaN`, `Symbol`, `Undefined`. Heavy third-party libraries (`react-datepicker`, `react-markdown`, `react-colorful`) are bundled as regular dependencies but loaded lazily at runtime via `React.lazy`, so unused components contribute zero to the consumer's bundle.
- **Breaking (json-edit-react v2)**: the old `LinkCustomComponent` and `LinkCustomNodeDefinition` are no longer exported from `json-edit-react`. Replaced by `LinkCustomComponent` + `LinkCustomNodeDefinition` (functionally a superset, with configurable `componentProps`) from `@json-edit-react/components`. Migration: `import { LinkCustomNodeDefinition } from '@json-edit-react/components'`.
- **Breaking (json-edit-react v2)**: the old `LinkCustomComponent` and `LinkCustomNodeDefinition` are no longer exported from `json-edit-react`. Replaced by `LinkCustomComponent` + the `hyperlinkDefinition` definition factory (functionally a superset, with configurable `componentProps`) from `@json-edit-react/components`. Migration: `import { hyperlinkDefinition } from '@json-edit-react/components'` and pass `hyperlinkDefinition()` to `customNodeDefinitions`.
- The `custom-component-library` workspace is now a downstream consumer of `@json-edit-react/components` — its `components/` folder moved into the new package; its app imports from `@json-edit-react/components` like any other consumer would.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1150,18 +1150,18 @@ npm i @json-edit-react/components

```js
import { JsonEditor } from 'json-edit-react'
import { LinkCustomNodeDefinition } from '@json-edit-react/components'
import { hyperlinkDefinition } from '@json-edit-react/components'

// ...Other stuff
return (
<JsonEditor
{...otherProps}
customNodeDefinitions={[LinkCustomNodeDefinition, ...otherCustomDefinitions]}
customNodeDefinitions={[hyperlinkDefinition(), ...otherCustomDefinitions]}
/>
)
```

For object-shaped link data (e.g. `{ text, url }` pairs displayed as a clickable string), use `EnhancedLinkCustomNodeDefinition` from the same package.
For object-shaped link data (e.g. `{ text, url }` pairs displayed as a clickable string), use `enhancedLinkDefinition` from the same package.

### Handling JSON

Expand Down
20 changes: 11 additions & 9 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ import { ArrowBackIcon, ArrowForwardIcon, InfoIcon } from '@chakra-ui/icons'
import { demoDataDefinitions } from './demoData'
import { useDatabase } from './useDatabase'
import './style.css'
import { getConditionalDefinitions, getLineHeight, truncate } from './helpers'
import { getLineHeight, truncate } from './helpers'
import { RenderProfiler } from './RenderProfiler'
import { Loading } from '../../packages/components/src/_common/Loading'
import { CodeEditor } from '@json-edit-react/components'
import { type CustomComponentLibraryData } from './demoData/data'
const SourceIndicator = lazy(() => import('./SourceIndicator'))
const JsonEditor = lazy(() =>
import('@json-edit-react').then((m) => ({ default: m.JsonEditor }))
Expand Down Expand Up @@ -179,13 +178,16 @@ function App() {
// }
// }, [])

const customNodeDefinitions =
selectedDataSet === 'customComponentLibrary'
? getConditionalDefinitions(
data as CustomComponentLibraryData,
dataDefinition?.customNodeDefinitions ?? []
)
: dataDefinition.customNodeDefinitions
// Data sets whose definitions are configured by the data itself declare
// them as a function; rebuild when the data changes. Static lists pass
// through with their module-scope identity intact.
const customNodeDefinitions = useMemo(
() =>
typeof dataDefinition.customNodeDefinitions === 'function'
? dataDefinition.customNodeDefinitions(data)
: dataDefinition.customNodeDefinitions,
[dataDefinition, data]
)

const updateState = (patch: Partial<AppState>) => setState({ ...state, ...patch })

Expand Down
6 changes: 5 additions & 1 deletion demo/src/demoData/data.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Data for Demo page -- imported by dataDefinitions.tsx

// Custom Component Library Data defined separately so we can infer a type for
// it, which is required in App.tsx
const customComponentLibraryData = {
Intro: `# json-edit-react

Expand Down Expand Up @@ -30,7 +34,7 @@ const customComponentLibraryData = {
},
'Simple boolean toggle': false,
'Date & Time': {
'Date ISO String': new Date().toISOString(),
'Date Picker': new Date().toISOString(),
'Date Object': new Date(),
'Show Time in Date?': true,
// info: 'Inserted in App.tsx',
Expand Down
131 changes: 75 additions & 56 deletions demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import React from 'react'
import { data } from './data'
import { data, type CustomComponentLibraryData } from './data'
import { Flex, Box, Link, Text, UnorderedList, ListItem } from '@chakra-ui/react'
import {
DatePickerDefinition,
LinkCustomNodeDefinition,
DateObjectDefinition,
UndefinedDefinition,
BooleanToggleDefinition,
NanDefinition,
SymbolDefinition,
BigIntDefinition,
MarkdownNodeDefinition,
EnhancedLinkCustomNodeDefinition,
ImageNodeDefinition,
ColorPickerNodeDefinition,
datePickerDefinition,
hyperlinkDefinition,
dateObjectDefinition,
undefinedDefinition,
booleanToggleDefinition,
nanDefinition,
symbolDefinition,
bigIntDefinition,
markdownDefinition,
enhancedLinkDefinition,
imageDefinition,
colorPickerDefinition,
} from '@json-edit-react/components'
import {
CustomNodeDefinition,
JsonData,
FilterFunction,
CustomTextDefinitions,
assign,
Expand Down Expand Up @@ -52,6 +53,9 @@ const codenameGlossary: Record<string, string> = {
bp: 'blood pressure',
}

// eslint-disable-next-line -- any is correct here
type DemoNodeDefinitions = CustomNodeDefinition<Record<string, any>>[]

export interface DemoData {
name: string
description: React.JSX.Element
Expand Down Expand Up @@ -80,8 +84,9 @@ export interface DemoData {
showErrorMessages?: boolean
defaultValue?: DefaultValueFunction
newKeyOptions?: string[] | NewKeyOptionsFunction
// eslint-disable-next-line -- any is correct here
customNodeDefinitions?: CustomNodeDefinition<Record<string, any>>[]
// Either a static list, or — for data sets whose definitions are
// configured by values in the data itself — a function of the current data
customNodeDefinitions?: DemoNodeDefinitions | ((data: JsonData) => DemoNodeDefinitions)
customTextDefinitions?: CustomTextDefinitions
styles?: Partial<ThemeStyles>
customTextEditorAvailable?: boolean
Expand Down Expand Up @@ -120,7 +125,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
rootName: 'data',
collapse: 2,
data: data.intro,
customNodeDefinitions: [DatePickerDefinition],
customNodeDefinitions: [datePickerDefinition()],
// allowEdit: ({ key }) => key !== 'number',
customTextEditorAvailable: true,
allowTypeSelection: ({ key }) => {
Expand Down Expand Up @@ -247,7 +252,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
return false
},
collapse: 1,
customNodeDefinitions: [DatePickerDefinition, LinkCustomNodeDefinition],
customNodeDefinitions: [datePickerDefinition(), hyperlinkDefinition()],
data: data.starWars,
},
jsonPlaceholder: {
Expand Down Expand Up @@ -565,7 +570,9 @@ export const demoDataDefinitions: Record<string, DemoData> = {
searchPlaceholder: 'Search guestbook',
customNodeDefinitions: [
{
condition: DatePickerDefinition.condition,
// Borrow the pre-built definition's ISO-date condition; the component
// here is a custom read-only display, not the date picker
condition: datePickerDefinition().condition,
component: ({ data, getStyles, nodeData }) => {
return (
<p style={getStyles('string', nodeData)}>{new Date(data as string).toLocaleString()}</p>
Expand Down Expand Up @@ -733,12 +740,9 @@ export const demoDataDefinitions: Record<string, DemoData> = {
},
showKey: false,
},
{
...DatePickerDefinition,
showOnView: true,
showInTypeSelector: true,
datePickerDefinition({
componentProps: { showTime: false, dateFormat: 'MMM d, yyyy' },
},
}),
// Uncomment to test a custom Collection node
// {
// condition: ({ key }) => key === 'portrayedBy',
Expand Down Expand Up @@ -1059,7 +1063,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
<Link href="https://github.com/CarlosNZ/json-edit-react#custom-nodes" isExternal>
Custom Node definitions & components
</Link>{' '}
for common (yet non-JSON) data types or useful data structures.
for common data types or useful data structures.
</Text>
<Text>
See their implementation in the{' '}
Expand All @@ -1086,39 +1090,54 @@ export const demoDataDefinitions: Record<string, DemoData> = {
rootName: 'components',
collapse: 3,
data: data.customComponentLibrary,
customNodeDefinitions: [
// Must keep this one first as we override it by index in App.tsx
{
...DateObjectDefinition,
componentProps: { showTime: false },
},
ImageNodeDefinition,
LinkCustomNodeDefinition,
EnhancedLinkCustomNodeDefinition,
UndefinedDefinition,
BooleanToggleDefinition,
NanDefinition,
SymbolDefinition,
BigIntDefinition,
ColorPickerNodeDefinition,
{
...MarkdownNodeDefinition,
// Value-type check so a node switched to another type (e.g. number)
// renders natively rather than as markdown text
condition: ({ key, value }) => key === 'Markdown' && typeof value === 'string',
},
{
...MarkdownNodeDefinition,
condition: ({ key, value }) => key === 'Intro' && typeof value === 'string',
showKey: false,
componentProps: {
components: {
// @ts-expect-error Ignore _ var
a: ({ _, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
// Some of these definitions are configured by values in the data set
// itself (the "Image properties" and "Show Time in Date?" nodes), so the
// list is a function of the current data, rebuilt as it's edited
customNodeDefinitions: (currentData) => {
const libraryData = currentData as CustomComponentLibraryData
return [
dateObjectDefinition({
componentProps: {
showTime: libraryData?.['Date & Time']?.['Show Time in Date?'] ?? false,
},
},
},
],
}),
datePickerDefinition({
componentProps: {
showTime: libraryData?.['Date & Time']?.['Show Time in Date?'] ?? false,
},
}),
imageDefinition({
componentProps: {
imageStyles: {
maxHeight: libraryData?.Images?.['Image properties']?.maxHeight,
maxWidth: libraryData?.Images?.['Image properties']?.maxWidth,
},
},
}),
hyperlinkDefinition(),
enhancedLinkDefinition(),
undefinedDefinition(),
booleanToggleDefinition(),
nanDefinition(),
symbolDefinition(),
bigIntDefinition(),
colorPickerDefinition(),
// The factory ANDs these conditions with the built-in string guard,
// so a node switched to another type (e.g. number) renders natively
// rather than as markdown text
markdownDefinition({ condition: ({ key }) => key === 'Markdown' }),
markdownDefinition({
condition: ({ key }) => key === 'Intro',
showKey: false,
componentProps: {
components: {
// @ts-expect-error Ignore _ var
a: ({ _, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
},
},
}),
]
},
customTextEditorAvailable: true,
},
}
30 changes: 1 addition & 29 deletions demo/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { JsonData, type CustomNodeDefinition } from '@json-edit-react'
import { type CustomComponentLibraryData } from './demoData/data'
import { JsonData } from '@json-edit-react'

export const truncate = (string: string, length = 200) =>
string.length < length ? string : `${string.slice(0, length - 2).trim()}...`
Expand All @@ -21,30 +20,3 @@ const jsonStringify = (data: JsonData) =>
},
2
)

// For the "CustomNodeLibrary" data, returns modified definitions dependent on
// the data
export const getConditionalDefinitions = (
data: CustomComponentLibraryData,
customNodeDefinitions: CustomNodeDefinition[]
) =>
customNodeDefinitions.map((definition) => {
if (definition?.name === 'Image')
return {
...definition,
componentProps: {
imageStyles: {
maxHeight: data?.Images?.['Image properties']?.maxHeight,
maxWidth: data?.Images?.['Image properties']?.maxWidth,
},
},
}

if (definition?.name === 'Date Object')
return {
...definition,
componentProps: { showTime: data?.['Date & Time']?.['Show Time in Date?'] ?? false },
}

return definition
})
16 changes: 11 additions & 5 deletions migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ If you only have a few minutes, these are the changes most likely to affect exis
| What changed | Migration |
|---|---|
| Pre-built themes split into a separate package | `npm i @json-edit-react/themes` and update theme imports |
| `LinkCustomComponent` / `LinkCustomNodeDefinition` moved | `npm i @json-edit-react/components` and update those imports |
| `LinkCustomComponent` / `LinkCustomNodeDefinition` moved | `npm i @json-edit-react/components`; the definition is now the `hyperlinkDefinition()` factory — see §2 |
| Several internal helpers are now part of the public API | No action needed — purely additive |
| `JsonEditor` is now generic on the data type (`JsonEditor<T>`) | No action needed — defaults to `JsonData`, source-compatible. Opt in by writing `<JsonEditor<MyShape> ... />` |
| `setData` is now required; `viewOnly` removed; new `JsonViewer` export | Switch read-only usage to `<JsonViewer>`; replace `viewOnly={cond}` with the relevant `allow*` toggles, including `allowDrag` if drag was enabled — see §6 |
Expand Down Expand Up @@ -89,19 +89,25 @@ Update imports:
```diff
- import { JsonEditor, LinkCustomNodeDefinition } from 'json-edit-react'
+ import { JsonEditor } from 'json-edit-react'
+ import { LinkCustomNodeDefinition } from '@json-edit-react/components'
+ import { hyperlinkDefinition } from '@json-edit-react/components'
```

Usage is unchanged:
The definition is now a **factory** — call it (with no arguments for the v1 behaviour) rather than passing the object directly:

```jsx
<JsonEditor
data={data}
setData={setData}
customNodeDefinitions={[LinkCustomNodeDefinition]}
customNodeDefinitions={[hyperlinkDefinition()]}
/>
```

If you previously spread `LinkCustomNodeDefinition` to customize it, pass the overrides to the factory instead. A `condition` override is combined (AND) with the built-in URL check rather than replacing it, and `componentProps` overrides shallow-merge with the defaults:

```jsx
customNodeDefinitions={[hyperlinkDefinition({ condition: ({ key }) => key === 'homepage' })]}
```

### What you also get

`@json-edit-react/components` ships 12 components in total — the original `LinkCustomComponent` plus 11 more that previously existed only as demo code, not as an installable package:
Expand All @@ -118,7 +124,7 @@ Usage is unchanged:
| `BooleanToggle` | Booleans rendered as a toggle switch |
| `BigInt`, `NaN`, `Symbol`, `Undefined` | Non-JSON-native value displays |

Each component ships with a matching `*CustomNodeDefinition` ready to drop into the `customNodeDefinitions` prop.
Each component ships with a matching definition factory (`datePickerDefinition()`, `markdownDefinition()`, …) ready to drop into the `customNodeDefinitions` prop.

### Bundle impact

Expand Down
Loading
Loading