Skip to content

Commit

Permalink
feat: implement support for vanilla JS components in onRenderValue
Browse files Browse the repository at this point in the history
…using Svelte Action (#398)
  • Loading branch information
josdejong committed Feb 2, 2024
1 parent b8be1a5 commit db482ea
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 109 deletions.
208 changes: 119 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,118 +279,148 @@ const editor = new JSONEditor({

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

- `onRenderValue(props: RenderValueProps) : RenderValueComponentDescription[]`
- `onRenderValue(props: RenderValueProps) : RenderValueComponentDescription[]`

_EXPERIMENTAL! This API will most likely change in future versions._
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`.

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`.
For JSON Schema enums, there is a ready-made value renderer `renderJSONSchemaEnum` which renders enums using the `EnumValue` component. This can be used like:

For JSON Schema enums, there is a value renderer `renderJSONSchemaEnum` which renders enums using the `EnumValue` component. This can be used like:
```js
import { renderJSONSchemaEnum, renderValue } from 'svelte-jsoneditor'

function onRenderValue(props) {
// use the enum renderer, and fallback on the default renderer
return renderJSONSchemaEnum(props, schema, schemaDefinitions) || renderValue(props)
}
```

The callback `onRenderValue` must return an array with one or multiple renderers. Each renderer can be either a Svelte component or a Svelte action:

```ts
interface SvelteComponentRenderer {
component: typeof SvelteComponent<RenderValuePropsOptional>
props: Record<string, unknown>
}

interface SvelteActionRenderer {
action: Action // Svelte Action
props: Record<string, unknown>
}
```

```js
import { renderJSONSchemaEnum, renderValue } from 'svelte-jsoneditor'
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:

function onRenderValue(props) {
// use the enum renderer, and fallback on the default renderer
return renderJSONSchemaEnum(props, schema, schemaDefinitions) || renderValue(props)
```js
const myRendererAction = {
action: (node) => {
// attach something to the HTML DOM node
return {
update: (node) => {
// update the DOM
},
destroy: () => {
// cleanup the DOM
}
}
}
}
```

- `onRenderMenu(items: MenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean }) : MenuItem[] | undefined`.
Callback which can be used to make changes to the menu items. New items can
be added, or existing items can be removed or reorganized. When the function
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.

A menu item `MenuItem` can be one of the following types:

- Button:

```ts
interface MenuButton {
type: 'button'
onClick: () => void
icon?: IconDefinition
text?: string
title?: string
className?: string
disabled?: boolean
}
```

- `onRenderMenu(items: MenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean }) : MenuItem[] | undefined`.
Callback which can be used to make changes to the menu items. New items can
be added, or existing items can be removed or reorganized. When the function
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.
- Separator (gray vertical line between a group of items):

A menu item `MenuItem` can be one of the following types:
```ts
interface MenuSeparator {
type: 'separator'
}
```

- Button:
- Space (fills up empty space):

```ts
interface MenuButton {
type: 'button'
onClick: () => void
icon?: IconDefinition
text?: string
title?: string
className?: string
disabled?: boolean
}
```
```ts
interface MenuSpace {
type: 'space'
}
```

- Separator (gray vertical line between a group of items):
- `onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined`.
Callback which can be used to make changes to the context menu items. New items can
be added, or existing items can be removed or reorganized. When the function
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.

```ts
interface MenuSeparator {
type: 'separator'
}
```
A menu item `ContextMenuItem` can be one of the following types:

- Space (fills up empty space):
- Button:

```ts
interface MenuSpace {
type: 'space'
}
```

- `onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined`.
Callback which can be used to make changes to the context menu items. New items can
be added, or existing items can be removed or reorganized. When the function
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.

A menu item `ContextMenuItem` can be one of the following types:

- Button:

```ts
interface MenuButton {
type: 'button'
onClick: () => void
icon?: IconDefinition
text?: string
title?: string
className?: string
disabled?: boolean
}
```
```ts
interface MenuButton {
type: 'button'
onClick: () => void
icon?: IconDefinition
text?: string
title?: string
className?: string
disabled?: boolean
}
```

- Dropdown button:
- Dropdown button:

```ts
interface MenuDropDownButton {
type: 'dropdown-button'
main: MenuButton
width?: string
items: MenuButton[]
}
```
```ts
interface MenuDropDownButton {
type: 'dropdown-button'
main: MenuButton
width?: string
items: MenuButton[]
}
```

- Separator (gray line between a group of items):
- Separator (gray line between a group of items):

```ts
interface MenuSeparator {
type: 'separator'
}
```
```ts
interface MenuSeparator {
type: 'separator'
}
```

- Menu row and column:
- Menu row and column:

```ts
interface MenuLabel {
type: 'label'
text: string
}
```ts
interface MenuLabel {
type: 'label'
text: string
}

interface ContextMenuColumn {
type: 'column'
items: Array<MenuButton | MenuDropDownButton | MenuLabel | MenuSeparator>
}
interface ContextMenuColumn {
type: 'column'
items: Array<MenuButton | MenuDropDownButton | MenuLabel | MenuSeparator>
}

interface ContextMenuRow {
type: 'row'
items: Array<MenuButton | MenuDropDownButton | ContextMenuColumn>
}
```
interface ContextMenuRow {
type: 'row'
items: Array<MenuButton | MenuDropDownButton | ContextMenuColumn>
}
```

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

Expand Down
10 changes: 10 additions & 0 deletions src/lib/components/__snapshots__/JSONEditor.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ exports[`JSONEditor > render table mode 1`] = `
<!--&lt;JSONValue&gt;-->
Expand All @@ -320,6 +321,7 @@ exports[`JSONEditor > render table mode 1`] = `
<!--&lt;JSONValue&gt;-->
Expand Down Expand Up @@ -352,6 +354,7 @@ exports[`JSONEditor > render table mode 1`] = `
<!--&lt;JSONValue&gt;-->
Expand All @@ -372,6 +375,7 @@ exports[`JSONEditor > render table mode 1`] = `
<!--&lt;JSONValue&gt;-->
Expand Down Expand Up @@ -404,6 +408,7 @@ exports[`JSONEditor > render table mode 1`] = `
<!--&lt;JSONValue&gt;-->
Expand All @@ -424,6 +429,7 @@ exports[`JSONEditor > render table mode 1`] = `
<!--&lt;JSONValue&gt;-->
Expand Down Expand Up @@ -1649,6 +1655,7 @@ exports[`JSONEditor > render tree mode 1`] = `
<!--&lt;JSONValue&gt;-->
</div>
Expand Down Expand Up @@ -1825,6 +1832,7 @@ exports[`JSONEditor > render tree mode 1`] = `
<!--&lt;JSONValue&gt;-->
</div>
Expand Down Expand Up @@ -1888,6 +1896,7 @@ exports[`JSONEditor > render tree mode 1`] = `
<!--&lt;JSONValue&gt;-->
</div>
Expand Down Expand Up @@ -2064,6 +2073,7 @@ exports[`JSONEditor > render tree mode 1`] = `
<!--&lt;JSONValue&gt;-->
</div>
Expand Down
20 changes: 17 additions & 3 deletions src/lib/components/modes/tablemode/JSONValue.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
JSONSelection,
SearchResultItem
} from '$lib/types'
import { isSvelteActionRenderer } from '$lib/typeguards.js'
import type { JSONPatchDocument, JSONPath } from 'immutable-json-patch'
import { isEditingSelection, isValueSelection } from '$lib/logic/selection.js'
import { createNestedValueOperations } from '$lib/logic/operations.js'
Expand Down Expand Up @@ -47,7 +48,20 @@
</script>

{#each renderers as renderer}
{#key renderer.component}
<svelte:component this={renderer.component} {...renderer.props} />
{/key}
{#if isSvelteActionRenderer(renderer)}
{@const action = renderer.action}
{#key renderer.action}
<div
role="button"
tabindex="-1"
class="jse-value jse-readonly-password"
data-type="selectable-value"
use:action={renderer.props}
/>
{/key}
{:else}
{#key renderer.component}
<svelte:component this={renderer.component} {...renderer.props} />
{/key}
{/if}
{/each}
20 changes: 17 additions & 3 deletions src/lib/components/modes/treemode/JSONValue.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type { JSONEditorContext, JSONSelection, SearchResultItem } from '$lib/types.js'
import type { JSONPath } from 'immutable-json-patch'
import { isEditingSelection, isValueSelection } from '$lib/logic/selection.js'
import { isSvelteActionRenderer } from '$lib/typeguards.js'
export let path: JSONPath
export let value: unknown
Expand Down Expand Up @@ -34,7 +35,20 @@
</script>

{#each renderers as renderer}
{#key renderer.component}
<svelte:component this={renderer.component} {...renderer.props} />
{/key}
{#if isSvelteActionRenderer(renderer)}
{@const action = renderer.action}
{#key renderer.action}
<div
role="button"
tabindex="-1"
class="jse-value jse-readonly-password"
data-type="selectable-value"
use:action={renderer.props}
/>
{/key}
{:else}
{#key renderer.component}
<svelte:component this={renderer.component} {...renderer.props} />
{/key}
{/if}
{/each}
Loading

0 comments on commit db482ea

Please sign in to comment.