Skip to content

Component library in demo #206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 14, 2025
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ Your `translations` object doesn't have to be exhaustive — only define the key
You can replace certain nodes in the data tree with your own custom components. An example might be for an image display, or a custom date editor, or just to add some visual bling. See the "Custom Nodes" data set in the [interactive demo](https://carlosnz.github.io/json-edit-react/?data=customNodes) to see it in action. (There is also a custom Date picker that appears when editing ISO strings in the other data sets.)

> [!TIP]
> There are a selection of useful Custom components ready for you to use in my [Custom Component Library](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md).
> There are a selection of useful Custom components ready for you to use in my [Custom Component Library](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md) — see examples in the [Demo app](https://carlosnz.github.io/json-edit-react/?data=customComponentLibrary).
> Please contribute your own if you think they'd be useful to others.

Custom nodes are provided in the `customNodeDefinitions` prop, as an array of objects of following structure:
Expand Down
4 changes: 3 additions & 1 deletion custom-component-library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Eventually, I'd like to publish these in a separate package so you can easily im

Contains a [Vite](https://vite.dev/) web-app for previewing and developing components.

The individual components are in the `/components` folder, along with demo data (in `data.ts`).
The individual components are in the `/components` folder.

There is a React app provided for demo-ing and testing these components -- [Development](#development) below.

> [!NOTE]
> If you create a custom component that you think would be useful to others, please [create a PR](https://github.com/CarlosNZ/json-edit-react/pulls) for it.
Expand Down
54 changes: 35 additions & 19 deletions custom-component-library/components/DatePicker/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
* rather than requiring the user to edit the ISO string directly.
*/

import React from 'react'
import DatePicker from 'react-datepicker'
import React, { lazy, Suspense } from 'react'
import { Button } from './Button'
import { CustomNodeProps } from '@json-edit-react'

// Styles
import 'react-datepicker/dist/react-datepicker.css'
import './style.css'
import { Loading } from '../_common/Loading'

const DatePicker = lazy(() => import('react-datepicker'))

export interface DatePickerCustomProps {
dateFormat?: string
dateTimeFormat?: string
showTime?: boolean
loadingText?: string
}

export const DateTimePicker: React.FC<CustomNodeProps<DatePickerCustomProps>> = ({
Expand All @@ -39,6 +42,7 @@ export const DateTimePicker: React.FC<CustomNodeProps<DatePickerCustomProps>> =
dateFormat = 'MMM d, yyyy',
dateTimeFormat = 'MMM d, yyyy h:mm aa',
showTime = true,
loadingText = 'Loading Date Picker',
} = customNodeProps ?? {}

const date = new Date(value as string)
Expand All @@ -54,26 +58,38 @@ export const DateTimePicker: React.FC<CustomNodeProps<DatePickerCustomProps>> =
// at all when viewing (and so will show raw ISO strings). However, we've
// defined an alternative here too, when showOnView == true, in which case
// the date/time string is shown as a localised date/time.
<DatePicker
// Check to prevent invalid date (from previous data value) crashing the
// component

// @ts-expect-error -- isNan can take any input
selected={isNaN(date) ? null : date}
showTimeSelect={showTime}
dateFormat={showTime ? dateTimeFormat : dateFormat}
onChange={(date: Date | null) => date && setValue(date.toISOString())}
open={true}
onKeyDown={handleKeyPress}
<Suspense
fallback={
<div style={stringStyle}>
<Loading text={loadingText} />
</div>
}
>
<div style={{ display: 'inline-flex', gap: 10 }}>
{/* These buttons are not really necessary -- you can either use the
<DatePicker
// Check to prevent invalid date (from previous data value) crashing the
// component
// @ts-expect-error -- isNan can take any input
selected={isNaN(date) ? null : date}
showTimeSelect={showTime}
dateFormat={showTime ? dateTimeFormat : dateFormat}
onChange={(date: Date | null) => date && setValue(date.toISOString())}
open={true}
onKeyDown={handleKeyPress}
>
<div style={{ display: 'inline-flex', gap: 10 }}>
{/* These buttons are not really necessary -- you can either use the
standard Ok/Cancel icons, or keyboard Enter/Esc, but shown for demo
purposes */}
<Button textColor={textColour} color={okColour} onClick={handleEdit} text="OK" />
<Button textColor={textColour} color={cancelColour} onClick={handleCancel} text="Cancel" />
</div>
</DatePicker>
<Button textColor={textColour} color={okColour} onClick={handleEdit} text="OK" />
<Button
textColor={textColour}
color={cancelColour}
onClick={handleCancel}
text="Cancel"
/>
</div>
</DatePicker>
</Suspense>
) : (
<div
// Double-click behaviour same as standard elements
Expand Down
25 changes: 14 additions & 11 deletions custom-component-library/components/Markdown/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
* Uses react-markdown to render the markdown content
*/

import React from 'react'
import React, { lazy, Suspense } from 'react'
import { type CustomNodeProps } from '@json-edit-react'
import Markdown from 'react-markdown'
import { Options } from 'react-markdown'
import { Loading } from '../_common/Loading'

export interface LinkProps {
linkStyles?: React.CSSProperties
stringTruncate?: number
[key: string]: unknown
const Markdown = lazy(() => import('react-markdown'))

export interface MarkdownCustomProps extends Options {
loadingText?: string
}

export const MarkdownComponent: React.FC<CustomNodeProps<LinkProps>> = (props) => {
const { setIsEditing, getStyles, nodeData } = props
export const MarkdownComponent: React.FC<CustomNodeProps<MarkdownCustomProps>> = (props) => {
const { setIsEditing, getStyles, nodeData, customNodeProps } = props
const styles = getStyles('string', nodeData)

return (
Expand All @@ -24,10 +25,12 @@ export const MarkdownComponent: React.FC<CustomNodeProps<LinkProps>> = (props) =
if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true)
}}
onDoubleClick={() => setIsEditing(true)}
style={styles}
style={{ ...styles }}
className="jer-markdown-block"
>
{/* TO-DO: Style over-rides */}
<Markdown>{nodeData.value as string}</Markdown>
<Suspense fallback={<Loading text={customNodeProps?.loadingText ?? 'Loading Markdown'} />}>
<Markdown {...props.customNodeProps}>{nodeData.value as string}</Markdown>
</Suspense>
</div>
)
}
4 changes: 2 additions & 2 deletions custom-component-library/components/Markdown/definition.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type CustomNodeDefinition } from '@json-edit-react'
import { MarkdownComponent, LinkProps } from './component'
import { MarkdownComponent, MarkdownCustomProps } from './component'

export const MarkdownNodeDefinition: CustomNodeDefinition<LinkProps> = {
export const MarkdownNodeDefinition: CustomNodeDefinition<MarkdownCustomProps> = {
condition: () => false, // Over-ride this for specific cases
element: MarkdownComponent,
// customNodeProps: {},
Expand Down
6 changes: 6 additions & 0 deletions custom-component-library/components/_common/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react'
import './style.css'

export const Loading: React.FC<{ text?: string }> = ({ text = 'Loading' }) => {
return <div className="jer-simple-loader">{text}...</div>
}
12 changes: 12 additions & 0 deletions custom-component-library/components/_common/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.jer-simple-loader {
width: fit-content;
font-style: italic;
clip-path: inset(0 3ch 0 0);
animation: l4 1s steps(4) infinite;
line-height: 1.2em;
}
@keyframes l4 {
to {
clip-path: inset(0 -1ch 0 0);
}
}
12 changes: 6 additions & 6 deletions custom-component-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
"react-markdown": "^10.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@eslint/js": "^9.28.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.28.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
"typescript-eslint": "^8.34.0",
"vite": "^6.3.5"
}
}
6 changes: 5 additions & 1 deletion custom-component-library/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@ import {
MarkdownNodeDefinition,
EnhancedLinkCustomNodeDefinition,
} from '../components'
import { testData } from '../components/data'
import { testData } from './data'
import { JsonData, JsonEditor } from '@json-edit-react'

if (testData?.['Date & Time']) {
// @ts-expect-error redefine after initialisation
testData['Date & Time'].Date = STORE_DATE_AS_DATE_OBJECT ? new Date() : new Date().toISOString()

// @ts-expect-error adding property
testData['Date & Time'].info = STORE_DATE_AS_DATE_OBJECT
? 'Date is stored a JS Date object. To use ISO string, set STORE_DATE_AS_DATE_OBJECT to false in App.tsx.'
: 'Date is stored as ISO string. To use JS Date objects, set STORE_DATE_AS_DATE_OBJECT to true in App.tsx.'

// @ts-expect-error only used in Demo app
delete testData['Date & Time']['Date Object']
}

type TestData = typeof testData
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* The data to be shown in the json-edit-react component, which showcases the
* custom components defined in here.
* custom components defined here.
*/

export const testData = {
Expand All @@ -14,6 +14,7 @@ export const testData = {
- DateObject
- Undefined
- Markdown
- "Enhanced" link
- BigInt
- BooleanToggle
- NaN
Expand All @@ -26,14 +27,16 @@ export const testData = {
'Long URL':
'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',
'Enhanced Link': {
text: 'This link displays custom text',
text: 'This link displays custom text — try editing me!',
url: 'https://github.com/CarlosNZ/json-edit-react/tree/main/custom-component-library#custom-component-library',
},
},
'Simple boolean toggle': false,
'Date & Time': {
Date: new Date().toISOString(),
'Date Object': new Date(),
'Show Time in Date?': true,
info: 'Replaced in App.tsx',
// info: 'Inserted in App.tsx',
},

'Non-JSON types': {
Expand All @@ -43,5 +46,6 @@ export const testData = {
Symbol2: Symbol('Second one'),
BigInt: 1234567890123456789012345678901234567890n,
},
Markdown: 'This text is **bold** and this is *italic*',
Markdown:
'Uses [react-markdown](https://www.npmjs.com/package/react-markdown) to render **Markdown** *text content*. ',
}
Loading