When to use
The JsonViewer component can be used for the display and editing of JSON data.
Semi mainly referred to the design concept of the text-buffer data structure of VS Code, reused some utilities and data type definitions (Token parsing, language services, etc.), and implemented the JsonViewer component in combination with our functional/style customization requirements. Visually, it will be more coordinated with other components within the Semi Design Next system, and it will be more convenient for customized rendering and customization of specific data types.
Compared with directly using MonacoEditor, Semi JsonViewer has additional processing in engineering construction, is simpler to use, and there is no need to pay attention to complex configurations such as Webpack plugins and worker loaders. At the same time, since we only focus on the JSON data format, it is more lightweight. While being ready to use out of the box, it has a smaller size (📦-96%), a more extreme loading speed (🚀 -53.5%), and less memory occupation (⬇️71.6% reduction). For data with five million lines and below, data loading and parsing can be completed within 1 second.
Detailed comparison data can be referred to in the Performance section.
- If you only need to preview/edit JSON and don't need to modify other more complex programming languages, we recommend that you choose
JsonViewer. - If you also need to handle data/code files in other formats and the full capabilities of a code editor (syntax highlighting, code completion, error prompts, complex editing, etc.) are essential and the build product size is not a key concern, we recommend that you choose
Monaco Editor.
Demos
How to import
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
</script>
Basic Usage
Basic usage of JsonViewer. Pass height and width to set the height and width of the component, and pass the JSON string through value as the initial value. onChange is called with the editor's current text when the content changes. value can be bound with bind:value; the editor is re-created only when value becomes different from the editor's current content, so echoing the same content back keeps the caret; without a binding the editor keeps what the user typed. To read the value, call getValue() through bind:this (type JsonViewerHandle), please refer to Methods for details.
Code
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
</script>
<div style="margin-bottom: 16px">
<JsonViewer height={100} value={data} width={700} />
</div>Different lineHeight
Configure the lineHeight parameter of options to set a fixed line height (unit: px, default 20).
Code
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
const wrapperStyle = 'margin-bottom: 12px; overflow: hidden';
</script>
<div>
<div style={wrapperStyle}>
<JsonViewer height={100} options={{ lineHeight: 20 }} value={data} width={700} />
</div>
<div style={wrapperStyle}>
<JsonViewer height={120} options={{ lineHeight: 24 }} value={data} width={700} />
</div>
<div style={wrapperStyle}>
<JsonViewer height={120} options={{ lineHeight: 26 }} value={data} width={700} />
</div>
</div>Autowrap
Configure the autoWrap parameter of options. When it is set to true, the component will automatically wrap lines according to the length of the content.
Code
<script lang="ts">
import { JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0",
"description": "Semi Design is a design system that defines a set of mid_back design and front_end basic components."
}`;
const options = { autoWrap: true };
</script>
<div style="margin-bottom: 16px">
<JsonViewer height={120} {options} value={data} width={700} />
</div>Format options
Configure options.formatOptions to set the formatting configuration of the component.
- tabSize: number,set the indent size to 4, which means each level of indentation is 4 spaces.
- insertSpaces: boolean,when it is true, it means using spaces for indentation, and when it is false, it means using tabs.
- eol: string,set the line break character, which can be
\n,\r\n,
Code
<script lang="ts">
import { Button, JsonViewer, type JsonViewerHandle } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
const options = { formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' } };
let jsonviewer = $state<JsonViewerHandle>();
</script>
<div>
<Button onclick={() => jsonviewer?.format()}>格式化</Button>
<div style="margin-bottom: 16px; margin-top: 16px">
<JsonViewer bind:this={jsonviewer} height={100} {options} value={data} width={700} />
</div>
</div>Custom Render Rules
By configuring the options.customRenderRule parameter, you can customize how JSON content is rendered (Note: only works in read-only mode).
Behavior notes
pathincustomRenderRuleis computed more accurately: the key token and the value token of the same key-value pair share the samepath(they both map to the property path, e.g.root.<key>).As a result, rules that only rely on
path(such aspath === 'root.<key>') may match both the key and the value, which can change which rule “wins” compared to previous versions.If you want to match only the key or only the value, use the third argument
tokenTypeof function match:const targetPath = 'root.<key>'; // Key only match: (_value, path, tokenType) => tokenType === 'key' && path === targetPath; // Value only match: (value, path, tokenType) => tokenType === 'value' && path === targetPath;
customRenderRule is an array of rules that hold data only: match is the matching condition, and the optional name tells rules apart in the customRender snippet. The content of a matched token is drawn by {#snippet customRender({ item, index, content })}, where item is the matched rule and index is its position in customRenderRule.
match: Matching condition, can be one of three types:- String: Exact match
- Regular expression: Match by regex
- Function: Custom matching logic, with signature
(value: string | number | boolean | null, path: string, tokenType: 'key' | 'value') => booleanvalue: Value to match (JSON key or value). Whenmatchis a function,valuewill be passed as the parsed primitive when possible (number / boolean / null / string), so strict comparisons like===work; whenmatchis a string or RegExp, matching is still based on textual content (string tokens are unquoted)path: Current matching path, format isroot.key1.key2.key3[0].key4tokenType: Current token type,'key'for JSON property name,'value'for JSON value. Useful for distinguishing between key and value matching
name: Optional rule name; useitem.namein thecustomRendersnippet to tell rules apartcustomRendersnippet: the arguments are{ item, index, content }content: Matched content. For string values, includes double quotes (e.g.,"name","Semi")- Without the snippet, or when it draws nothing, custom rendering is skipped and the token keeps its text
Code
<script lang="ts">
import { Image, JsonViewer, Popover, Rating, Tag, type JsonViewerOptions } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "2.7.4",
"rating": 5,
"tags": ["design", "react", "ui"],
"image": "/assets/root-web-sites/abstract.jpg"
}`;
// A rule holds data only; the customRender snippet draws the token and tells the rules apart by name
const options: JsonViewerOptions = {
formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' },
customRenderRule: [
{ name: 'popover', match: 'Semi' },
{ name: 'rating', match: (value) => value == 5 },
{
name: 'tag',
match: (_value, path) => path === 'root.tags[0]' || path === 'root.tags[1]' || path === 'root.tags[2]',
},
{ name: 'image', match: /^http/ },
],
readOnly: true,
autoWrap: true,
};
// content is the raw string token with its quotes; without them it is a valid URL
const unquote = (content: string) => content.replace(/^"|"$/g, '');
</script>
{#snippet imagePreview(src: string)}
<Image height={100} {src} width={100} />
{/snippet}
<div>
<div style="margin-bottom: 16px; margin-top: 16px">
<JsonViewer height={200} {options} showSearch={false} value={data} width={600}>
{#snippet customRender({ item, content: text })}
{#if item.name === 'popover'}
<Popover showArrow trigger="hover">
{#snippet content()}我是用户自定义的渲染{/snippet}
<span>{text}</span>
</Popover>
{:else if item.name === 'rating'}
<Rating disabled size={10} value={Number(text)} />
{:else if item.name === 'tag'}
<Tag shape="circle" size="small">{text}</Tag>
{:else}
<Popover showArrow trigger="hover">
{#snippet content()}{@render imagePreview(unquote(text))}{/snippet}
<span>{text}</span>
</Popover>
{/if}
{/snippet}
</JsonViewer>
</div>
</div>Custom Search Button
Use the searchButton snippet to customize the rendering of the search button, enabling fixed positioning, custom styles, and more. Providing the snippet replaces the default button as a whole; defaultButton is the default button's snippet, drawn with {@render defaultButton()}. When showSearch is false, neither is rendered.
Code
<script lang="ts">
import { IconSearch } from '@fibrae/semi-icons-svelte';
import { Button, JsonViewer } from '@fibrae/semi-ui-svelte';
const data = `{
"name": "Semi",
"version": "0.0.0"
}`;
</script>
<div style="margin-bottom: 16px">
<JsonViewer height={200} value={data} width={700}>
{#snippet searchButton({ defaultButton, showSearchBar, onToggleSearchBar })}
<div style="position: absolute; top: 10px; right: 10px; z-index: 10">
{#if !showSearchBar}
<Button onclick={onToggleSearchBar}>
{#snippet icon()}
<IconSearch />
{/snippet}
搜索
</Button>
{:else}
{@render defaultButton()}
{/if}
</div>
{/snippet}
</JsonViewer>
</div>API Reference
JsonViewer
Props
| Property | Description | Type | Default |
|---|---|---|---|
| customRender | Replaces the content of a token matched by options.customRenderRule, with the matched rule item, its index and the token text content as snippet arguments. A snippet that draws nothing leaves the token's text in place. Only takes effect in read-only mode. | TypeSnippet<[JsonViewerCustomRenderArgs]> | - |
| height | Height. A number is taken as px; a string is used as is. | number | string | 400 |
| limitSearchButtonBounds | Whether to limit the search button drag bounds within the component container. | boolean | false |
| onChange | Called when the content changes, with the editor's current text. | Type(value: string) => void | - |
| options | Editor options; see JsonViewerOptions for the fields. The defaults of ariaLabel, readOnly and autoWrap merge per key, so passing some fields keeps the defaults of the others. A deep change re-creates the editor; mutating a nested object such as formatOptions or customRenderRule in place goes unnoticed, so replace it with a new object. | JsonViewerOptions | - |
| searchButton | Replaces the default search button as a whole. defaultButton is the default button's snippet, drawn with {@render defaultButton()}; the other arguments build a custom search UI. Not rendered when showSearch is false. | TypeSnippet<[JsonViewerSearchButtonArgs]> | - |
| showSearch | Whether to show the search button. | boolean | true |
| value | Text of the editor, used with bind:value. A value different from the editor's current content re-creates the editor; echoing the current content back keeps the caret. Without it the editor starts empty. | string | - |
| width | Width. A number is taken as px; a string is used as is. | number | string | 400 |
Methods
| Method | Description | Type |
|---|---|---|
| format | Formats the content with formatOptions. | Type() => void |
| getSearchResults | Returns the current search results. | Type() => ReturnType<JsonViewerFoundation['getSearchResults']> |
| getStyle | Returns the current width and height. | Type() => { width: string | number; height: string | number; } |
| getValue | Returns the editor's current text. | Type() => string |
| nextSearch | Moves to the next search result, with an optional step. | Type(step?: number) => void |
| prevSearch | Moves to the previous search result, with an optional step. | Type(step?: number) => void |
| replace | Replaces the current search match. An empty string deletes the match; without a match it does nothing. | Type(replaceText: string) => void |
| replaceAll | Replaces all search matches. An empty string deletes them. | Type(replaceText: string) => void |
| search | Searches for text; the optional parameters control case sensitivity, whole word and regular expression. | Type(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) => void |
class, style and native attributes fall on the root element. The editor owns the root's content, so there is no children. When the root has no role and aria-label or aria-labelledby is passed, the root gets role="group"; an explicit role wins.
JsonViewerOptions
| Attribute | Description | Type | Default |
|---|---|---|---|
| lineHeight | Height of each line of content, unit:px | number | 20 |
| autoWrap | Whether to wrap lines automatically. | boolean | true |
| readOnly | Whether to be read-only. | boolean | false |
| customRenderRule | Custom render rules | CustomRenderRule[] | - |
| formatOptions | Content format setting | FormattingOptions | - |
| completionOptions | Auto-completion setting | CompletionOptions | - |
| ariaLabel | Accessible name of the content area (the read-only scroll region, or the editable textbox) | string | locale JsonViewer.content |
CustomRenderRule
The type is named JsonViewerCustomRenderRule. A rule holds data only; the content of a matched token is drawn by the customRender snippet.
| Attribute | Description | Type | Default |
|---|---|---|---|
| match | Matching rule | string | RegExp | (value: string | number | boolean | null, path: string, tokenType: 'key' | 'value') => boolean | - |
| name | Rule name, to tell rules apart in the customRender snippet | string | - |
FormattingOptions
| Attribute | Description | Type | Default |
|---|---|---|---|
| tabSize | Indent size | number | 4 |
| insertSpaces | Whether to use spaces for indentation | boolean | true |
| eol | Line break character | string | '\n' |
CompletionOptions
| Attribute | Description | Type | Default |
|---|---|---|---|
| staticCompletions | Static completion items. They are appended only after the candidates inferred from the document structure, and are not offered when nothing can be inferred; an item whose label matches an existing candidate is not added again. Only label, insertText (falling back to label) and documentation of each item are read | CompletionItem[] | - |
SearchControls
With the searchButton snippet, the arguments (type JsonViewerSearchButtonArgs) contain the following properties besides defaultButton:
| Attribute | Description | Type |
|---|---|---|
| showSearchBar | Whether the search bar is currently visible | boolean |
| onToggleSearchBar | Toggle search bar visibility | () => void |
| onSearch | Execute search | (text: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) => void |
| onPrevSearch | Navigate to previous search result | () => void |
| onNextSearch | Navigate to next search result | () => void |
| onReplace | Replace current search match | (text: string) => void |
| onReplaceAll | Replace all search matches | (text: string) => void |
Methods
Methods exported by the component can be called through bind:this (type JsonViewerHandle), for example jsonviewer?.format().
| Method | Description |
|---|---|
| getValue() | Get current value |
| format() | Format current content |
| search(searchText: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) | Search for text with optional parameters |
| getSearchResults() | Get current search results |
| prevSearch(step?: number) | Navigate to previous search result, with optional step size |
| nextSearch(step?: number) | Navigate to next search result, with optional step size |
| replace(replaceText: string) | Replace current search match |
| replaceAll(replaceText: string) | Replace all search matches |
| getStyle() | Get the current width and height |
Performance
Bundle Size
| Libs Name | Size | Size (Gzip) |
|---|---|---|
| JsonViewer | 203.14kb | 51.23kb |
| MonacoEditor | 5102.0 KB | 1322.7 KB |
Time for rendering data of different magnitudes.
For details on the generation method of the test data, please refer to URL
When the data volume exceeds 500,000 lines, ReactMonacoEditor turns off highlighting and other behaviors by default, and the data comparison does not follow the principle of a single variable.
| Libs Name | 1k lines | 5k lines | 10 thousand lines | 100 thousand lines | 500 thousand lines | 1 million lines | 3 million lines |
|---|---|---|---|---|---|---|---|
| JsonViewer | 30.42ms | 30.66ms | 36.87ms | 52.73ms | 111.02ms | 178.81ms | 506.25ms |
| ReactMonacoEditor | 72.01ms | 73.76ms | 76.64ms | 97.89ms | 133.31ms | 202.79ms | 495.53ms |
| Performance improvement | 57.70% | 58.41% | 51.87% | 46.11% | - | - | - |