Skip to content
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

feat: implement support for vanilla JS components in onRenderValue (WIP) #398

Merged
merged 5 commits into from
Feb 2, 2024
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
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
Loading