From daae7c181aa2d7974c7faf37d6b562424ebd20fb Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 13 Jun 2026 10:23:55 +1200 Subject: [PATCH 1/3] Build Factory factory, implement for Markdown and DatePicker --- demo/src/demoData/dataDefinitions.tsx | 36 ++++++------ .../components/src/DatePicker/definition.ts | 24 ++++---- packages/components/src/DatePicker/index.ts | 1 + .../components/src/Markdown/definition.ts | 11 +++- packages/components/src/Markdown/index.ts | 1 + .../src/_common/createDefinitionFactory.ts | 55 +++++++++++++++++++ packages/components/src/index.ts | 4 ++ 7 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 packages/components/src/_common/createDefinitionFactory.ts diff --git a/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index da46504c..cdd68214 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -2,7 +2,7 @@ import React from 'react' import { data } from './data' import { Flex, Box, Link, Text, UnorderedList, ListItem } from '@chakra-ui/react' import { - DatePickerDefinition, + datePickerDefinition, LinkCustomNodeDefinition, DateObjectDefinition, UndefinedDefinition, @@ -10,7 +10,7 @@ import { NanDefinition, SymbolDefinition, BigIntDefinition, - MarkdownNodeDefinition, + markdownDefinition, EnhancedLinkCustomNodeDefinition, ImageNodeDefinition, ColorPickerNodeDefinition, @@ -120,7 +120,7 @@ export const demoDataDefinitions: Record = { rootName: 'data', collapse: 2, data: data.intro, - customNodeDefinitions: [DatePickerDefinition], + customNodeDefinitions: [datePickerDefinition()], // allowEdit: ({ key }) => key !== 'number', customTextEditorAvailable: true, allowTypeSelection: ({ key }) => { @@ -247,7 +247,7 @@ export const demoDataDefinitions: Record = { return false }, collapse: 1, - customNodeDefinitions: [DatePickerDefinition, LinkCustomNodeDefinition], + customNodeDefinitions: [datePickerDefinition(), LinkCustomNodeDefinition], data: data.starWars, }, jsonPlaceholder: { @@ -565,7 +565,9 @@ export const demoDataDefinitions: Record = { 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 (

{new Date(data as string).toLocaleString()}

@@ -733,12 +735,9 @@ export const demoDataDefinitions: Record = { }, 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', @@ -1101,15 +1100,12 @@ export const demoDataDefinitions: Record = { 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', + // 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: { @@ -1117,7 +1113,7 @@ export const demoDataDefinitions: Record = { a: ({ _, ...props }) => , }, }, - }, + }), ], customTextEditorAvailable: true, }, diff --git a/packages/components/src/DatePicker/definition.ts b/packages/components/src/DatePicker/definition.ts index bb5ee694..e949c443 100644 --- a/packages/components/src/DatePicker/definition.ts +++ b/packages/components/src/DatePicker/definition.ts @@ -1,14 +1,11 @@ /** - * An example Custom Component: - * https://github.com/CarlosNZ/json-edit-react#custom-nodes - * - * A date/time picker which can be configure to show (using the - * CustomNodeDefinitions at the bottom of this file) when an ISO date/time - * string is present in the JSON data, and present a Date picker interface - * rather than requiring the user to edit the ISO string directly. + * A date/time picker which shows when an ISO date/time string is present in + * the JSON data, presenting a calendar interface rather than requiring the + * user to edit the ISO string directly. */ -import { CustomNodeDefinition } from 'json-edit-react' +import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { DatePickerCustomProps, DateTimePicker } from './component' // Styles @@ -18,9 +15,12 @@ import './style.css' const ISO_STRING_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/ -// Definition for custom node behaviour -export const DatePickerDefinition: CustomNodeDefinition = { - // Condition is a regex to match ISO strings +// The condition doubles as the guard: it keeps the date parser away from +// anything but ISO strings. Consumer `condition` overrides are targeting, +// ANDed with this by the factory; a non-ISO date format needs the explicit +// `guard` override (realistically along with `fromStandardType` and +// `defaultValue`). +const DatePickerDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'string' && ISO_STRING_REGEX.test(value), component: DateTimePicker, showOnView: true, @@ -41,3 +41,5 @@ export const DatePickerDefinition: CustomNodeDefinition = }, componentProps: { showTime: true }, } + +export const datePickerDefinition = createDefinitionFactory(DatePickerDefinition) diff --git a/packages/components/src/DatePicker/index.ts b/packages/components/src/DatePicker/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/DatePicker/index.ts +++ b/packages/components/src/DatePicker/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/Markdown/definition.ts b/packages/components/src/Markdown/definition.ts index 74dc0929..90046ac4 100644 --- a/packages/components/src/Markdown/definition.ts +++ b/packages/components/src/Markdown/definition.ts @@ -1,10 +1,15 @@ import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { MarkdownComponent, MarkdownCustomProps } from './component' -export const MarkdownNodeDefinition: CustomNodeDefinition = { - condition: ({ value }) => typeof value === 'string', // Over-ride this for specific cases +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const MarkdownNodeDefinition: CustomNodeDefinition = { + condition: ({ value }) => typeof value === 'string', component: MarkdownComponent, - // componentProps: {}, showOnView: true, showOnEdit: false, } + +export const markdownDefinition = createDefinitionFactory(MarkdownNodeDefinition) diff --git a/packages/components/src/Markdown/index.ts b/packages/components/src/Markdown/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/Markdown/index.ts +++ b/packages/components/src/Markdown/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/_common/createDefinitionFactory.ts b/packages/components/src/_common/createDefinitionFactory.ts new file mode 100644 index 00000000..9463c414 --- /dev/null +++ b/packages/components/src/_common/createDefinitionFactory.ts @@ -0,0 +1,55 @@ +import { type CustomNodeDefinition, type FilterFunction } from 'json-edit-react' + +/** + * Builds the customization factory for a pre-built custom-node definition. + * + * A pre-built definition's `condition` doubles as its guard — the check that + * keeps its component safe to render (Markdown's `typeof value === 'string'`, + * DatePicker's ISO regex). Consumers customizing a definition almost always + * want to narrow *where* it applies, not loosen *what* it can render, so the + * factory re-interprets a consumer-supplied `condition` as targeting and ANDs + * it with the guard: narrowing can never expose the component to data it + * can't handle. Replacing the guard itself requires the explicit `guard` + * override — a deliberate, named act rather than the default spelling. + * + * The base definition objects are deliberately not exported from the package: + * spreading one and overriding `condition` silently drops the guard, and the + * factory only closes that door if it's the only one. + */ + +export type DefinitionOverrides> = Partial< + Omit, 'condition'> +> & { + // Replaces the definition's default targeting (usually "everywhere the + // guard passes"); always ANDed with the guard + condition?: FilterFunction + // Replaces the guard — the component's safety contract becomes the + // consumer's responsibility + guard?: FilterFunction +} + +export const createDefinitionFactory = + >( + base: CustomNodeDefinition, + defaultTargeting: FilterFunction = () => true + ) => + (overrides: DefinitionOverrides = {}): CustomNodeDefinition => { + const { + guard = base.condition, + condition: targeting = defaultTargeting, + componentProps, + ...rest + } = overrides + const definition: CustomNodeDefinition = { + ...base, + ...rest, + condition: (nodeData) => guard(nodeData) && targeting(nodeData), + } + // Shallow-merged rather than override-wins, so a consumer can add one + // prop without re-stating the base's defaults (e.g. DatePicker's + // `{ showTime: true }`). Conditional so definitions without + // componentProps don't gain an empty object. + if (base.componentProps || componentProps) + definition.componentProps = { ...base.componentProps, ...componentProps } as T + return definition + } diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index fd0218e7..46bf0d91 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -12,3 +12,7 @@ export * from './Image' export * from './ColorPicker' export * from './ReactSelect' export * from './CodeEditor' + +// The definition factories' override surface; the factory builder itself +// (`createDefinitionFactory`) stays internal +export { type DefinitionOverrides } from './_common/createDefinitionFactory' From 29aed04ecf9b35e528556e530ebd20adf69f0380 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 13 Jun 2026 10:36:55 +1200 Subject: [PATCH 2/3] Implement across all 12 components --- .changeset/customnode-field-renames.md | 2 +- .changeset/definition-factories.md | 9 ++++ .changeset/initial-components-package.md | 2 +- README.md | 6 +-- demo/src/demoData/dataDefinitions.tsx | 46 +++++++++---------- migration-guide.md | 16 +++++-- packages/components/README.md | 12 ++--- packages/components/src/BigInt/definition.ts | 9 +++- packages/components/src/BigInt/index.ts | 1 + .../src/BooleanToggle/definition.ts | 11 +++-- .../components/src/BooleanToggle/index.ts | 1 + .../components/src/ColorPicker/definition.ts | 9 +++- packages/components/src/ColorPicker/index.ts | 1 + .../components/src/DateObject/definition.ts | 8 +++- packages/components/src/DateObject/index.ts | 1 + .../components/src/EnhancedLink/definition.ts | 8 +++- packages/components/src/EnhancedLink/index.ts | 1 + .../components/src/Hyperlink/definition.ts | 9 +++- packages/components/src/Hyperlink/index.ts | 1 + packages/components/src/Image/definition.ts | 9 +++- packages/components/src/Image/index.ts | 1 + packages/components/src/NaN/definition.ts | 8 +++- packages/components/src/NaN/index.ts | 1 + packages/components/src/Symbol/definition.ts | 9 +++- packages/components/src/Symbol/index.ts | 1 + .../components/src/Undefined/definition.ts | 8 +++- packages/components/src/Undefined/index.ts | 1 + 27 files changed, 129 insertions(+), 62 deletions(-) create mode 100644 .changeset/definition-factories.md diff --git a/.changeset/customnode-field-renames.md b/.changeset/customnode-field-renames.md index 258774cd..29d34af4 100644 --- a/.changeset/customnode-field-renames.md +++ b/.changeset/customnode-field-renames.md @@ -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. diff --git a/.changeset/definition-factories.md b/.changeset/definition-factories.md new file mode 100644 index 00000000..1bb9b371 --- /dev/null +++ b/.changeset/definition-factories.md @@ -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`. + +The underlying components and their props types (`MarkdownComponent`, `DateTimePicker`, `LinkCustomComponent`, …) are exported alongside the factories, for wrapping or use in fully hand-rolled definitions. diff --git a/.changeset/initial-components-package.md b/.changeset/initial-components-package.md index af3093c1..e3821ef1 100644 --- a/.changeset/initial-components-package.md +++ b/.changeset/initial-components-package.md @@ -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. diff --git a/README.md b/README.md index de827c0b..a002da89 100644 --- a/README.md +++ b/README.md @@ -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 ( ) ``` -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 diff --git a/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index cdd68214..08a8f617 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -3,17 +3,17 @@ import { data } from './data' import { Flex, Box, Link, Text, UnorderedList, ListItem } from '@chakra-ui/react' import { datePickerDefinition, - LinkCustomNodeDefinition, - DateObjectDefinition, - UndefinedDefinition, - BooleanToggleDefinition, - NanDefinition, - SymbolDefinition, - BigIntDefinition, + hyperlinkDefinition, + dateObjectDefinition, + undefinedDefinition, + booleanToggleDefinition, + nanDefinition, + symbolDefinition, + bigIntDefinition, markdownDefinition, - EnhancedLinkCustomNodeDefinition, - ImageNodeDefinition, - ColorPickerNodeDefinition, + enhancedLinkDefinition, + imageDefinition, + colorPickerDefinition, } from '@json-edit-react/components' import { CustomNodeDefinition, @@ -247,7 +247,7 @@ export const demoDataDefinitions: Record = { return false }, collapse: 1, - customNodeDefinitions: [datePickerDefinition(), LinkCustomNodeDefinition], + customNodeDefinitions: [datePickerDefinition(), hyperlinkDefinition()], data: data.starWars, }, jsonPlaceholder: { @@ -1086,20 +1086,16 @@ export const demoDataDefinitions: Record = { 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, + dateObjectDefinition({ componentProps: { showTime: false } }), + imageDefinition(), + 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 diff --git a/migration-guide.md b/migration-guide.md index 6b9c4c7e..0b0add69 100644 --- a/migration-guide.md +++ b/migration-guide.md @@ -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`) | No action needed — defaults to `JsonData`, source-compatible. Opt in by writing ` ... />` | | `setData` is now required; `viewOnly` removed; new `JsonViewer` export | Switch read-only usage to ``; replace `viewOnly={cond}` with the relevant `allow*` toggles, including `allowDrag` if drag was enabled — see §6 | @@ -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 ``` +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: @@ -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 diff --git a/packages/components/README.md b/packages/components/README.md index 846dc828..7ba3d696 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -16,7 +16,7 @@ pnpm add @json-edit-react/components ## Available components -Each component ships a React component plus a `CustomNodeDefinition` ready to drop into `customNodeDefinitions`. +Each component ships a React component plus a definition factory that produces a `CustomNodeDefinition` ready to drop into `customNodeDefinitions`. | Component | Use case | |---|---| @@ -46,18 +46,12 @@ Standalone components that plug into JsonEditor's `CustomSelect` and `TextEditor ```tsx import { JsonEditor } from 'json-edit-react' -import { - LinkCustomNodeDefinition, - DateTimePickerDefinition, -} from '@json-edit-react/components' +import { hyperlinkDefinition, datePickerDefinition } from '@json-edit-react/components' ``` diff --git a/packages/components/src/BigInt/definition.ts b/packages/components/src/BigInt/definition.ts index 441a524a..861568c3 100644 --- a/packages/components/src/BigInt/definition.ts +++ b/packages/components/src/BigInt/definition.ts @@ -1,10 +1,13 @@ import { isCollection, type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { BigIntComponent, BigIntProps } from './component' -export const BigIntDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const BigIntDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'bigint', component: BigIntComponent, - // componentProps: {}, showOnView: true, showEditTools: true, showOnEdit: true, @@ -31,3 +34,5 @@ export const BigIntDefinition: CustomNodeDefinition = { ? BigInt(value.value as string) : value, } + +export const bigIntDefinition = createDefinitionFactory(BigIntDefinition) diff --git a/packages/components/src/BigInt/index.ts b/packages/components/src/BigInt/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/BigInt/index.ts +++ b/packages/components/src/BigInt/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/BooleanToggle/definition.ts b/packages/components/src/BooleanToggle/definition.ts index 127d7e67..a21ceb7c 100644 --- a/packages/components/src/BooleanToggle/definition.ts +++ b/packages/components/src/BooleanToggle/definition.ts @@ -1,13 +1,16 @@ import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { BooleanToggleComponent } from './component' -export const BooleanToggleDefinition: CustomNodeDefinition<{ - linkStyles?: React.CSSProperties - stringTruncateLength?: number -}> = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const BooleanToggleDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'boolean', component: BooleanToggleComponent, showOnView: true, showOnEdit: false, showEditTools: true, } + +export const booleanToggleDefinition = createDefinitionFactory(BooleanToggleDefinition) diff --git a/packages/components/src/BooleanToggle/index.ts b/packages/components/src/BooleanToggle/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/BooleanToggle/index.ts +++ b/packages/components/src/BooleanToggle/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/ColorPicker/definition.ts b/packages/components/src/ColorPicker/definition.ts index cd56d078..ac1da89f 100644 --- a/packages/components/src/ColorPicker/definition.ts +++ b/packages/components/src/ColorPicker/definition.ts @@ -1,12 +1,15 @@ import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { ColorPickerComponent, ColorPickerProps } from './component' import { colord } from 'colord' -export const ColorPickerNodeDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const ColorPickerNodeDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'string' && colord(value).isValid(), component: ColorPickerComponent, name: 'Color Picker', - // componentProps: {}, showOnView: true, showOnEdit: true, showInTypeSelector: true, @@ -21,3 +24,5 @@ export const ColorPickerNodeDefinition: CustomNodeDefinition = return String(value ?? '') }, } + +export const colorPickerDefinition = createDefinitionFactory(ColorPickerNodeDefinition) diff --git a/packages/components/src/ColorPicker/index.ts b/packages/components/src/ColorPicker/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/ColorPicker/index.ts +++ b/packages/components/src/ColorPicker/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/DateObject/definition.ts b/packages/components/src/DateObject/definition.ts index dcbc0274..a8a419fe 100644 --- a/packages/components/src/DateObject/definition.ts +++ b/packages/components/src/DateObject/definition.ts @@ -1,7 +1,11 @@ import { DateObjectCustomComponent, DateObjectProps } from './component' import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' -export const DateObjectDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const DateObjectDefinition: CustomNodeDefinition = { condition: (nodeData) => nodeData.value instanceof Date, component: DateObjectCustomComponent, showEditTools: true, @@ -32,3 +36,5 @@ export const DateObjectDefinition: CustomNodeDefinition = { ? new Date(value) : value, } + +export const dateObjectDefinition = createDefinitionFactory(DateObjectDefinition) diff --git a/packages/components/src/DateObject/index.ts b/packages/components/src/DateObject/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/DateObject/index.ts +++ b/packages/components/src/DateObject/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/EnhancedLink/definition.ts b/packages/components/src/EnhancedLink/definition.ts index 2b64392a..6a0deb7d 100644 --- a/packages/components/src/EnhancedLink/definition.ts +++ b/packages/components/src/EnhancedLink/definition.ts @@ -1,4 +1,5 @@ import { isCollection, type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { EnhancedLinkCustomComponent, EnhancedLinkProps } from './component' const TEXT_FIELD = 'text' @@ -9,7 +10,10 @@ const DEFAULT_LINK = { [URL_FIELD]: 'https://link.goes.here', } -export const EnhancedLinkCustomNodeDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const EnhancedLinkCustomNodeDefinition: CustomNodeDefinition = { condition: ({ value }) => isCollection(value) && TEXT_FIELD in value && URL_FIELD in value, component: EnhancedLinkCustomComponent, name: 'Enhanced Link', // shown in the Type selector menu @@ -42,3 +46,5 @@ export const EnhancedLinkCustomNodeDefinition: CustomNodeDefinition = { - // Condition is a regex to match url strings +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const LinkCustomNodeDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'string' && /^https?:\/\/.+\..+$/.test(value), component: LinkCustomComponent, componentProps: { stringTruncateLength: 80 }, showOnView: true, showOnEdit: false, } + +export const hyperlinkDefinition = createDefinitionFactory(LinkCustomNodeDefinition) diff --git a/packages/components/src/Hyperlink/index.ts b/packages/components/src/Hyperlink/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/Hyperlink/index.ts +++ b/packages/components/src/Hyperlink/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/Image/definition.ts b/packages/components/src/Image/definition.ts index 1b76408f..7fd96df2 100644 --- a/packages/components/src/Image/definition.ts +++ b/packages/components/src/Image/definition.ts @@ -1,13 +1,18 @@ import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { ImageComponent, ImageProps } from './component' const imageLinkRegex = /^https?:\/\/[^\s]+?\.(?:jpe?g|png|svg|gif)/i -export const ImageNodeDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const ImageNodeDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'string' && imageLinkRegex.test(value), component: ImageComponent, - // componentProps: {}, showOnView: true, showOnEdit: false, name: 'Image', } + +export const imageDefinition = createDefinitionFactory(ImageNodeDefinition) diff --git a/packages/components/src/Image/index.ts b/packages/components/src/Image/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/Image/index.ts +++ b/packages/components/src/Image/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/NaN/definition.ts b/packages/components/src/NaN/definition.ts index 93d6e8d0..7f20cb7c 100644 --- a/packages/components/src/NaN/definition.ts +++ b/packages/components/src/NaN/definition.ts @@ -1,7 +1,11 @@ import { NotANumberComponent, NaNProps } from './component' import { isCollection, type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' -export const NanDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const NanDefinition: CustomNodeDefinition = { condition: ({ value }) => Number.isNaN(value), component: NotANumberComponent, showEditTools: true, @@ -15,3 +19,5 @@ export const NanDefinition: CustomNodeDefinition = { ? NaN : value, } + +export const nanDefinition = createDefinitionFactory(NanDefinition) diff --git a/packages/components/src/NaN/index.ts b/packages/components/src/NaN/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/NaN/index.ts +++ b/packages/components/src/NaN/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/Symbol/definition.ts b/packages/components/src/Symbol/definition.ts index be62ee52..aefeb700 100644 --- a/packages/components/src/Symbol/definition.ts +++ b/packages/components/src/Symbol/definition.ts @@ -1,10 +1,13 @@ import { isCollection, type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' import { SymbolComponent, SymbolProps } from './component' -export const SymbolDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const SymbolDefinition: CustomNodeDefinition = { condition: ({ value }) => typeof value === 'symbol', component: SymbolComponent, - // componentProps: {}, showOnView: true, showEditTools: true, showOnEdit: true, @@ -23,3 +26,5 @@ export const SymbolDefinition: CustomNodeDefinition = { ? Symbol((value.value as string) ?? null) : value, } + +export const symbolDefinition = createDefinitionFactory(SymbolDefinition) diff --git a/packages/components/src/Symbol/index.ts b/packages/components/src/Symbol/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/Symbol/index.ts +++ b/packages/components/src/Symbol/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' diff --git a/packages/components/src/Undefined/definition.ts b/packages/components/src/Undefined/definition.ts index 3a0f85eb..9e6aef7d 100644 --- a/packages/components/src/Undefined/definition.ts +++ b/packages/components/src/Undefined/definition.ts @@ -1,7 +1,11 @@ import { UndefinedCustomComponent, UndefinedProps } from './component' import { type CustomNodeDefinition } from 'json-edit-react' +import { createDefinitionFactory } from '../_common/createDefinitionFactory' -export const UndefinedDefinition: CustomNodeDefinition = { +// The condition doubles as the guard: consumer `condition` overrides are +// targeting, ANDed with this by the factory; replacing it requires the +// explicit `guard` override. +const UndefinedDefinition: CustomNodeDefinition = { condition: ({ value }) => value === undefined, component: UndefinedCustomComponent, showEditTools: true, @@ -14,3 +18,5 @@ export const UndefinedDefinition: CustomNodeDefinition = { // stringifyReplacer: // parseReviver: } + +export const undefinedDefinition = createDefinitionFactory(UndefinedDefinition) diff --git a/packages/components/src/Undefined/index.ts b/packages/components/src/Undefined/index.ts index 75c618f1..618b7d06 100644 --- a/packages/components/src/Undefined/index.ts +++ b/packages/components/src/Undefined/index.ts @@ -1 +1,2 @@ export * from './definition' +export * from './component' From 01929c62aa61203d72474ffd7acc55784041d646 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:51:18 +1200 Subject: [PATCH 3/3] Update dynamic custom node definitions in demo --- demo/src/App.tsx | 20 ++++--- demo/src/demoData/data.tsx | 6 +- demo/src/demoData/dataDefinitions.tsx | 85 ++++++++++++++++++--------- demo/src/helpers.ts | 30 +--------- 4 files changed, 73 insertions(+), 68 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index a5233059..484bf64e 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -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 })) @@ -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) => setState({ ...state, ...patch }) diff --git a/demo/src/demoData/data.tsx b/demo/src/demoData/data.tsx index 18b972ae..29ec1a08 100644 --- a/demo/src/demoData/data.tsx +++ b/demo/src/demoData/data.tsx @@ -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 @@ -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', diff --git a/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index 08a8f617..c04d7ae0 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -1,5 +1,5 @@ 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, @@ -17,6 +17,7 @@ import { } from '@json-edit-react/components' import { CustomNodeDefinition, + JsonData, FilterFunction, CustomTextDefinitions, assign, @@ -52,6 +53,9 @@ const codenameGlossary: Record = { bp: 'blood pressure', } +// eslint-disable-next-line -- any is correct here +type DemoNodeDefinitions = CustomNodeDefinition>[] + export interface DemoData { name: string description: React.JSX.Element @@ -80,8 +84,9 @@ export interface DemoData { showErrorMessages?: boolean defaultValue?: DefaultValueFunction newKeyOptions?: string[] | NewKeyOptionsFunction - // eslint-disable-next-line -- any is correct here - customNodeDefinitions?: CustomNodeDefinition>[] + // 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 customTextEditorAvailable?: boolean @@ -1058,7 +1063,7 @@ export const demoDataDefinitions: Record = { Custom Node definitions & components {' '} - for common (yet non-JSON) data types or useful data structures. + for common data types or useful data structures. See their implementation in the{' '} @@ -1085,32 +1090,54 @@ export const demoDataDefinitions: Record = { rootName: 'components', collapse: 3, data: data.customComponentLibrary, - customNodeDefinitions: [ - dateObjectDefinition({ componentProps: { showTime: false } }), - imageDefinition(), - 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 }) => , + // 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 }) => , + }, + }, + }), + ] + }, customTextEditorAvailable: true, }, } diff --git a/demo/src/helpers.ts b/demo/src/helpers.ts index 787cb8d2..244b6561 100644 --- a/demo/src/helpers.ts +++ b/demo/src/helpers.ts @@ -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()}...` @@ -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 - })