Skip to content

Commit db482ea

Browse files
authoredFeb 2, 2024
feat: implement support for vanilla JS components in onRenderValue using Svelte Action (#398)
1 parent b8be1a5 commit db482ea

File tree

9 files changed

+288
-109
lines changed

9 files changed

+288
-109
lines changed
 

‎README.md

+119-89
Original file line numberDiff line numberDiff line change
@@ -279,118 +279,148 @@ const editor = new JSONEditor({
279279

280280
To adjust the text color of keys or values, the color of the classes `.jse-key` and `.jse-value` can be overwritten.
281281

282-
- `onRenderValue(props: RenderValueProps) : RenderValueComponentDescription[]`
282+
- `onRenderValue(props: RenderValueProps) : RenderValueComponentDescription[]`
283283

284-
_EXPERIMENTAL! This API will most likely change in future versions._
284+
Customize rendering of the values. By default, `renderValue` is used, which renders a value as an editable div and depending on the value can also render a boolean toggle, a color picker, and a timestamp tag. Multiple components can be rendered alongside each other, like the boolean toggle and color picker being rendered left from the editable div. Built in value renderer components: `EditableValue`, `ReadonlyValue`, `BooleanToggle`, `ColorPicker`, `TimestampTag`, `EnumValue`.
285285

286-
Customize rendering of the values. By default, `renderValue` is used, which renders a value as an editable div and depending on the value can also render a boolean toggle, a color picker, and a timestamp tag. Multiple components can be rendered alongside each other, like the boolean toggle and color picker being rendered left from the editable div. Built in value renderer components: `EditableValue`, `ReadonlyValue`, `BooleanToggle`, `ColorPicker`, `TimestampTag`, `EnumValue`.
286+
For JSON Schema enums, there is a ready-made value renderer `renderJSONSchemaEnum` which renders enums using the `EnumValue` component. This can be used like:
287287

288-
For JSON Schema enums, there is a value renderer `renderJSONSchemaEnum` which renders enums using the `EnumValue` component. This can be used like:
288+
```js
289+
import { renderJSONSchemaEnum, renderValue } from 'svelte-jsoneditor'
290+
291+
function onRenderValue(props) {
292+
// use the enum renderer, and fallback on the default renderer
293+
return renderJSONSchemaEnum(props, schema, schemaDefinitions) || renderValue(props)
294+
}
295+
```
296+
297+
The callback `onRenderValue` must return an array with one or multiple renderers. Each renderer can be either a Svelte component or a Svelte action:
298+
299+
```ts
300+
interface SvelteComponentRenderer {
301+
component: typeof SvelteComponent<RenderValuePropsOptional>
302+
props: Record<string, unknown>
303+
}
304+
305+
interface SvelteActionRenderer {
306+
action: Action // Svelte Action
307+
props: Record<string, unknown>
308+
}
309+
```
289310

290-
```js
291-
import { renderJSONSchemaEnum, renderValue } from 'svelte-jsoneditor'
311+
The `SvelteComponentRenderer` interface can be used to provide Svelte components like the `EnumValue` component mentioned above. The `SvelteActionRenderer` expects a [Svelte Action](https://svelte.dev/docs/svelte-action) as `action` property. Since this interface is a plain JavaScript interface, this allows to create custom components in a vanilla JS environment. Basically it is a function that gets a DOM node passed, and needs to return an object with `update` and `destroy` functions:
292312

293-
function onRenderValue(props) {
294-
// use the enum renderer, and fallback on the default renderer
295-
return renderJSONSchemaEnum(props, schema, schemaDefinitions) || renderValue(props)
313+
```js
314+
const myRendererAction = {
315+
action: (node) => {
316+
// attach something to the HTML DOM node
317+
return {
318+
update: (node) => {
319+
// update the DOM
320+
},
321+
destroy: () => {
322+
// cleanup the DOM
323+
}
324+
}
325+
}
326+
}
327+
```
328+
329+
- `onRenderMenu(items: MenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean }) : MenuItem[] | undefined`.
330+
Callback which can be used to make changes to the menu items. New items can
331+
be added, or existing items can be removed or reorganized. When the function
332+
returns `undefined`, the original `items` will be applied. Using the context values `mode` and `modal`, different actions can be taken depending on the mode of the editor and whether the editor is rendered inside a modal or not.
333+
334+
A menu item `MenuItem` can be one of the following types:
335+
336+
- Button:
337+
338+
```ts
339+
interface MenuButton {
340+
type: 'button'
341+
onClick: () => void
342+
icon?: IconDefinition
343+
text?: string
344+
title?: string
345+
className?: string
346+
disabled?: boolean
296347
}
297348
```
298349

299-
- `onRenderMenu(items: MenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean }) : MenuItem[] | undefined`.
300-
Callback which can be used to make changes to the menu items. New items can
301-
be added, or existing items can be removed or reorganized. When the function
302-
returns `undefined`, the original `items` will be applied. Using the context values `mode` and `modal`, different actions can be taken depending on the mode of the editor and whether the editor is rendered inside a modal or not.
350+
- Separator (gray vertical line between a group of items):
303351

304-
A menu item `MenuItem` can be one of the following types:
352+
```ts
353+
interface MenuSeparator {
354+
type: 'separator'
355+
}
356+
```
305357

306-
- Button:
358+
- Space (fills up empty space):
307359

308-
```ts
309-
interface MenuButton {
310-
type: 'button'
311-
onClick: () => void
312-
icon?: IconDefinition
313-
text?: string
314-
title?: string
315-
className?: string
316-
disabled?: boolean
317-
}
318-
```
360+
```ts
361+
interface MenuSpace {
362+
type: 'space'
363+
}
364+
```
319365

320-
- Separator (gray vertical line between a group of items):
366+
- `onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined`.
367+
Callback which can be used to make changes to the context menu items. New items can
368+
be added, or existing items can be removed or reorganized. When the function
369+
returns `undefined`, the original `items` will be applied. Using the context values `mode`, `modal` and `selection`, different actions can be taken depending on the mode of the editor, whether the editor is rendered inside a modal or not and the path of selection.
321370

322-
```ts
323-
interface MenuSeparator {
324-
type: 'separator'
325-
}
326-
```
371+
A menu item `ContextMenuItem` can be one of the following types:
327372

328-
- Space (fills up empty space):
373+
- Button:
329374

330-
```ts
331-
interface MenuSpace {
332-
type: 'space'
333-
}
334-
```
335-
336-
- `onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined`.
337-
Callback which can be used to make changes to the context menu items. New items can
338-
be added, or existing items can be removed or reorganized. When the function
339-
returns `undefined`, the original `items` will be applied. Using the context values `mode`, `modal` and `selection`, different actions can be taken depending on the mode of the editor, whether the editor is rendered inside a modal or not and the path of selection.
340-
341-
A menu item `ContextMenuItem` can be one of the following types:
342-
343-
- Button:
344-
345-
```ts
346-
interface MenuButton {
347-
type: 'button'
348-
onClick: () => void
349-
icon?: IconDefinition
350-
text?: string
351-
title?: string
352-
className?: string
353-
disabled?: boolean
354-
}
355-
```
375+
```ts
376+
interface MenuButton {
377+
type: 'button'
378+
onClick: () => void
379+
icon?: IconDefinition
380+
text?: string
381+
title?: string
382+
className?: string
383+
disabled?: boolean
384+
}
385+
```
356386

357-
- Dropdown button:
387+
- Dropdown button:
358388

359-
```ts
360-
interface MenuDropDownButton {
361-
type: 'dropdown-button'
362-
main: MenuButton
363-
width?: string
364-
items: MenuButton[]
365-
}
366-
```
389+
```ts
390+
interface MenuDropDownButton {
391+
type: 'dropdown-button'
392+
main: MenuButton
393+
width?: string
394+
items: MenuButton[]
395+
}
396+
```
367397

368-
- Separator (gray line between a group of items):
398+
- Separator (gray line between a group of items):
369399

370-
```ts
371-
interface MenuSeparator {
372-
type: 'separator'
373-
}
374-
```
400+
```ts
401+
interface MenuSeparator {
402+
type: 'separator'
403+
}
404+
```
375405

376-
- Menu row and column:
406+
- Menu row and column:
377407

378-
```ts
379-
interface MenuLabel {
380-
type: 'label'
381-
text: string
382-
}
408+
```ts
409+
interface MenuLabel {
410+
type: 'label'
411+
text: string
412+
}
383413
384-
interface ContextMenuColumn {
385-
type: 'column'
386-
items: Array<MenuButton | MenuDropDownButton | MenuLabel | MenuSeparator>
387-
}
414+
interface ContextMenuColumn {
415+
type: 'column'
416+
items: Array<MenuButton | MenuDropDownButton | MenuLabel | MenuSeparator>
417+
}
388418
389-
interface ContextMenuRow {
390-
type: 'row'
391-
items: Array<MenuButton | MenuDropDownButton | ContextMenuColumn>
392-
}
393-
```
419+
interface ContextMenuRow {
420+
type: 'row'
421+
items: Array<MenuButton | MenuDropDownButton | ContextMenuColumn>
422+
}
423+
```
394424

395425
- `onSelect: (selection: JSONEditorSelection | null) => void`
396426

‎src/lib/components/__snapshots__/JSONEditor.test.ts.snap

+10
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ exports[`JSONEditor > render table mode 1`] = `
300300
301301
302302
303+
303304
<!--&lt;JSONValue&gt;-->
304305
305306
@@ -320,6 +321,7 @@ exports[`JSONEditor > render table mode 1`] = `
320321
321322
322323
324+
323325
<!--&lt;JSONValue&gt;-->
324326
325327
@@ -352,6 +354,7 @@ exports[`JSONEditor > render table mode 1`] = `
352354
353355
354356
357+
355358
<!--&lt;JSONValue&gt;-->
356359
357360
@@ -372,6 +375,7 @@ exports[`JSONEditor > render table mode 1`] = `
372375
373376
374377
378+
375379
<!--&lt;JSONValue&gt;-->
376380
377381
@@ -404,6 +408,7 @@ exports[`JSONEditor > render table mode 1`] = `
404408
405409
406410
411+
407412
<!--&lt;JSONValue&gt;-->
408413
409414
@@ -424,6 +429,7 @@ exports[`JSONEditor > render table mode 1`] = `
424429
425430
426431
432+
427433
<!--&lt;JSONValue&gt;-->
428434
429435
@@ -1649,6 +1655,7 @@ exports[`JSONEditor > render tree mode 1`] = `
16491655
16501656
16511657
1658+
16521659
<!--&lt;JSONValue&gt;-->
16531660
16541661
</div>
@@ -1825,6 +1832,7 @@ exports[`JSONEditor > render tree mode 1`] = `
18251832
18261833
18271834
1835+
18281836
<!--&lt;JSONValue&gt;-->
18291837
18301838
</div>
@@ -1888,6 +1896,7 @@ exports[`JSONEditor > render tree mode 1`] = `
18881896
18891897
18901898
1899+
18911900
<!--&lt;JSONValue&gt;-->
18921901
18931902
</div>
@@ -2064,6 +2073,7 @@ exports[`JSONEditor > render tree mode 1`] = `
20642073
20652074
20662075
2076+
20672077
<!--&lt;JSONValue&gt;-->
20682078
20692079
</div>

‎src/lib/components/modes/tablemode/JSONValue.svelte

+17-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
JSONSelection,
88
SearchResultItem
99
} from '$lib/types'
10+
import { isSvelteActionRenderer } from '$lib/typeguards.js'
1011
import type { JSONPatchDocument, JSONPath } from 'immutable-json-patch'
1112
import { isEditingSelection, isValueSelection } from '$lib/logic/selection.js'
1213
import { createNestedValueOperations } from '$lib/logic/operations.js'
@@ -47,7 +48,20 @@
4748
</script>
4849

4950
{#each renderers as renderer}
50-
{#key renderer.component}
51-
<svelte:component this={renderer.component} {...renderer.props} />
52-
{/key}
51+
{#if isSvelteActionRenderer(renderer)}
52+
{@const action = renderer.action}
53+
{#key renderer.action}
54+
<div
55+
role="button"
56+
tabindex="-1"
57+
class="jse-value jse-readonly-password"
58+
data-type="selectable-value"
59+
use:action={renderer.props}
60+
/>
61+
{/key}
62+
{:else}
63+
{#key renderer.component}
64+
<svelte:component this={renderer.component} {...renderer.props} />
65+
{/key}
66+
{/if}
5367
{/each}

‎src/lib/components/modes/treemode/JSONValue.svelte

+17-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import type { JSONEditorContext, JSONSelection, SearchResultItem } from '$lib/types.js'
55
import type { JSONPath } from 'immutable-json-patch'
66
import { isEditingSelection, isValueSelection } from '$lib/logic/selection.js'
7+
import { isSvelteActionRenderer } from '$lib/typeguards.js'
78
89
export let path: JSONPath
910
export let value: unknown
@@ -34,7 +35,20 @@
3435
</script>
3536

3637
{#each renderers as renderer}
37-
{#key renderer.component}
38-
<svelte:component this={renderer.component} {...renderer.props} />
39-
{/key}
38+
{#if isSvelteActionRenderer(renderer)}
39+
{@const action = renderer.action}
40+
{#key renderer.action}
41+
<div
42+
role="button"
43+
tabindex="-1"
44+
class="jse-value jse-readonly-password"
45+
data-type="selectable-value"
46+
use:action={renderer.props}
47+
/>
48+
{/key}
49+
{:else}
50+
{#key renderer.component}
51+
<svelte:component this={renderer.component} {...renderer.props} />
52+
{/key}
53+
{/if}
4054
{/each}

‎src/lib/typeguards.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import type {
99
MenuSeparator,
1010
MenuSpace,
1111
ValidationError,
12-
NestedValidationError
12+
NestedValidationError,
13+
SvelteActionRenderer,
14+
SvelteComponentRenderer
1315
} from './types.js'
1416
import { isObject } from '$lib/utils/typeUtils.js'
1517

@@ -88,3 +90,11 @@ export function isValidationError(value: unknown): value is ValidationError {
8890
export function isNestedValidationError(value: unknown): value is NestedValidationError {
8991
return isObject(value) && isValidationError(value) && typeof value.isChildError === 'boolean'
9092
}
93+
94+
export function isSvelteComponentRenderer(value: unknown): value is SvelteComponentRenderer {
95+
return isObject(value) && 'component' in value && isObject(value.props)
96+
}
97+
98+
export function isSvelteActionRenderer(value: unknown): value is SvelteActionRenderer {
99+
return isObject(value) && typeof value.action === 'function' && isObject(value.props)
100+
}

‎src/lib/types.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { JSONPatchDocument, JSONPath, JSONPointer } from 'immutable-json-patch'
22
import type { SvelteComponent } from 'svelte'
33
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
4+
import type { Action } from 'svelte/action'
45

56
export type TextContent = { text: string }
67

@@ -541,11 +542,18 @@ export interface DraggingState {
541542
didMoveItems: boolean
542543
}
543544

544-
export interface RenderValueComponentDescription {
545+
export type RenderValueComponentDescription = SvelteComponentRenderer | SvelteActionRenderer
546+
547+
export interface SvelteComponentRenderer {
545548
component: typeof SvelteComponent<RenderValuePropsOptional>
546549
props: Record<string, unknown>
547550
}
548551

552+
export interface SvelteActionRenderer {
553+
action: Action<HTMLElement, Record<string, unknown>>
554+
props: Record<string, unknown>
555+
}
556+
549557
export interface TransformModalOptions {
550558
id?: string
551559
rootPath?: JSONPath

‎src/routes/+page.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<a href="examples/custom_json_parser">Custom JSON Parser (Lossless JSON)</a>
3434
</li>
3535
<li>
36-
<a href="examples/custom_value_renderer">Custom value renderer (password, enum)</a>
36+
<a href="examples/custom_value_renderer">Custom value renderer (password, enum, action)</a>
3737
</li>
3838
<li>
3939
<a href="examples/json_schema_validation">JSON Schema validation</a>
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createValueSelection, type OnJSONSelect } from 'svelte-jsoneditor'
2+
import type { Action } from 'svelte/action'
3+
import { type JSONPath } from 'immutable-json-patch'
4+
5+
export interface EvaluatorActionProps {
6+
value: unknown
7+
path: JSONPath
8+
readOnly: boolean
9+
onSelect: OnJSONSelect
10+
}
11+
12+
export const EvaluatorAction: Action<HTMLDivElement, Record<string, unknown>> = (
13+
node,
14+
initialProps
15+
) => {
16+
let props = toEvaluatorProps(initialProps)
17+
18+
function updateResult() {
19+
node.innerText = evaluate(String(props.value))
20+
}
21+
22+
function handleValueDoubleClick(event: MouseEvent) {
23+
if (!props.readOnly) {
24+
event.preventDefault()
25+
event.stopPropagation()
26+
27+
// open in edit mode
28+
props.onSelect(createValueSelection(props.path, true))
29+
}
30+
}
31+
32+
node.addEventListener('dblclick', handleValueDoubleClick)
33+
updateResult()
34+
35+
return {
36+
update: (updatedProps) => {
37+
props = toEvaluatorProps(updatedProps)
38+
updateResult()
39+
},
40+
destroy: () => {
41+
node.removeEventListener('dblclick', handleValueDoubleClick)
42+
}
43+
}
44+
}
45+
46+
function evaluate(expr: string) {
47+
const result = expr
48+
.split('+')
49+
.map((value) => parseFloat(value.trim()))
50+
.reduce((a, b) => a + b)
51+
52+
return `The result of "${expr}" is "${result}" (double-click to edit)`
53+
}
54+
55+
function toEvaluatorProps(props: Record<string, unknown>): EvaluatorActionProps {
56+
// you can add validations and typeguards here if needed
57+
return props as unknown as EvaluatorActionProps
58+
}

‎src/routes/examples/custom_value_renderer/+page.svelte

+46-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
<script>
2-
import { EnumValue, JSONEditor, renderValue } from 'svelte-jsoneditor'
1+
<script lang="ts">
2+
import {
3+
EnumValue,
4+
JSONEditor,
5+
renderValue,
6+
type RenderValueComponentDescription,
7+
type RenderValueProps
8+
} from 'svelte-jsoneditor'
39
import ReadonlyPassword from '../../components/ReadonlyPassword.svelte'
10+
import { EvaluatorAction } from '../../components/EvaluatorAction'
411
512
let content = {
613
text: undefined, // can be used to pass a stringified JSON document instead
714
json: {
815
username: 'John',
916
password: 'secret...',
10-
gender: 'male'
17+
gender: 'male',
18+
evaluate: '2 + 3'
1119
}
1220
}
1321
@@ -18,7 +26,7 @@
1826
{ value: 'other', text: 'Other' }
1927
]
2028
21-
function onRenderValue(props) {
29+
function onRenderValue(props: RenderValueProps): RenderValueComponentDescription[] {
2230
const { path, value, readOnly, parser, isEditing, selection, onSelect, onPatch } = props
2331
2432
const key = props.path[props.path.length - 1]
@@ -53,24 +61,51 @@
5361
]
5462
}
5563
64+
if (key === 'evaluate' && !isEditing) {
65+
return [
66+
{
67+
action: EvaluatorAction,
68+
props: {
69+
value,
70+
path,
71+
readOnly,
72+
onSelect
73+
}
74+
}
75+
]
76+
}
77+
5678
// fallback on the default render components
5779
return renderValue(props)
5880
}
5981
</script>
6082

6183
<svelte:head>
62-
<title>Custom value renderer (password, enum) | svelte-jsoneditor</title>
84+
<title>Custom value renderer (password, enum, action) | svelte-jsoneditor</title>
6385
</svelte:head>
6486

65-
<h1>Custom value renderer (password, enum)</h1>
87+
<h1>Custom value renderer (password, enum, action)</h1>
6688

6789
<p>
68-
Provide a custom <code>onRenderValue</code> method, which hides the value of all fields with the name
69-
"password", and creates an enum for the fields with name "gender".
70-
</p>
71-
<p>
72-
<i>EXPERIMENTAL! This API will most likely change in future versions.</i>
90+
Provide a custom <code>onRenderValue</code> method, which demonstrates three things:
7391
</p>
92+
<ol>
93+
<li>
94+
It hides the value of all fields with the name "password" using a Svelte password component <code
95+
>ReadonlyPassword</code
96+
>
97+
</li>
98+
<li>
99+
It creates an enum component for the fields with name "gender" using a Svelte component <code
100+
>EnumValue</code
101+
>.
102+
</li>
103+
<li>
104+
The creates a custom component for the field named "evaluate" using a Svelte Action, which
105+
evaluates the value as an expression containing an addition of two or more values. This solution
106+
can be used when using svelte-jsoneditor in a Vanilla JS environment.
107+
</li>
108+
</ol>
74109

75110
<div class="editor">
76111
<JSONEditor bind:content {onRenderValue} />

0 commit comments

Comments
 (0)
Please sign in to comment.