Plus · JsonViewer

JsonViewer

Used for displaying and editing JSON data

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 setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';
</script>

Basic Usage

Basic usage of JsonViewer. Pass in the height and width parameters to set the height and width of the component, and the JSON string through modelValue as the initial value. When the content changes, update:modelValue and change are emitted with the editor's current text, so v-model works. The editor is re-created only when modelValue becomes a value different from the editor's current content, so v-model echoing the same content keeps the caret; with a one-way :modelValue the editor keeps what the user typed instead of being pulled back. To read the value, call getValue() through a template ref, please refer to Methods for details.

Code
<script setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';

    const data = `{
    "name": "Semi",
    "version": "0.0.0"
}`;
</script>

<template>
    <div :style="{ marginBottom: '16px' }">
        <JsonViewer :height="100" :modelValue="data" :width="700" />
    </div>
</template>

Different lineHeight

Configure the lineHeight parameter of options to set a fixed line height (unit: px, default 20).

Code
<script setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';

    const data = `{
    "name": "Semi",
    "version": "0.0.0"
}`;
    const wrapperStyle = { marginBottom: '12px', overflow: 'hidden' };
</script>

<template>
    <div>
        <div :style="wrapperStyle">
            <JsonViewer :height="100" :modelValue="data" :options="{ lineHeight: 20 }" :width="700" />
        </div>
        <div :style="wrapperStyle">
            <JsonViewer :height="120" :modelValue="data" :options="{ lineHeight: 24 }" :width="700" />
        </div>
        <div :style="wrapperStyle">
            <JsonViewer :height="120" :modelValue="data" :options="{ lineHeight: 26 }" :width="700" />
        </div>
    </div>
</template>

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 setup lang="ts">
    import { JsonViewer } from '@fibrous/semi-ui-vue';

    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>

<template>
    <div :style="{ marginBottom: '16px' }">
        <JsonViewer :height="120" :modelValue="data" :options="options" :width="700" />
    </div>
</template>

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 setup lang="ts">
    import { useTemplateRef } from 'vue';
    import { Button, JsonViewer, type JsonViewerHandle } from '@fibrous/semi-ui-vue';

    const data = `{
  "name": "Semi",
  "version": "0.0.0"
}`;
    const options = { formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' } };

    const jsonviewer = useTemplateRef<JsonViewerHandle>('jsonviewer');
</script>

<template>
    <div>
        <Button @click="jsonviewer?.format()">格式化</Button>
        <div :style="{ marginBottom: '16px', marginTop: '16px' }">
            <JsonViewer ref="jsonviewer" :height="100" :modelValue="data" :options="options" :width="700" />
        </div>
    </div>
</template>

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

path in customRenderRule is computed more accurately: the key token and the value token of the same key-value pair share the same path (they both map to the property path, e.g. root.<key>).

As a result, rules that only rely on path (such as path === '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 tokenType of 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 slot. The content of a matched token is drawn by the #customRender="{ item, index, content }" slot, 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') => boolean
      • value: Value to match (JSON key or value). When match is a function, value will be passed as the parsed primitive when possible (number / boolean / null / string), so strict comparisons like === work; when match is a string or RegExp, matching is still based on textual content (string tokens are unquoted)
      • path: Current matching path, format is root.key1.key2.key3[0].key4
      • tokenType: Current token type, 'key' for JSON property name, 'value' for JSON value. Useful for distinguishing between key and value matching
  • name: Optional rule name; use item.name in the #customRender slot to tell rules apart
  • #customRender slot: scope parameters are item, index and content
    • content: Matched content. For string values, includes double quotes (e.g., "name", "Semi")
    • Without the slot, or when the slot renders nothing, custom rendering is skipped and the token keeps its text
Code
<script setup lang="ts">
    import { Image, JsonViewer, Popover, Rating, Tag, type JsonViewerOptions } from '@fibrous/semi-ui-vue';

    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 slot 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>

<template>
    <div>
        <div :style="{ marginBottom: '16px', marginTop: '16px' }">
            <JsonViewer :height="200" :modelValue="data" :options="options" :showSearch="false" :width="600">
                <template #customRender="{ item, content }">
                    <Popover v-if="item.name === 'popover'" showArrow trigger="hover">
                        <span>{{ content }}</span>
                        <template #content>我是用户自定义的渲染</template>
                    </Popover>
                    <Rating v-else-if="item.name === 'rating'" :defaultValue="Number(content)" disabled :size="10" />
                    <Tag v-else-if="item.name === 'tag'" shape="circle" size="small">{{ content }}</Tag>
                    <Popover v-else showArrow trigger="hover">
                        <span>{{ content }}</span>
                        <template #content>
                            <Image :height="100" :src="unquote(content)" :width="100" />
                        </template>
                    </Popover>
                </template>
            </JsonViewer>
        </div>
    </div>
</template>

Custom Search Button

Use the #searchButton slot to customize the rendering of the search button, enabling fixed positioning, custom styles, and more. Providing the slot replaces the default button as a whole; defaultButton is the default button's VNode, rendered with <component :is>. When showSearch is false, neither is rendered.

Code
<script setup lang="ts">
    import { IconSearch } from '@fibrous/semi-icons-vue';
    import { Button, JsonViewer } from '@fibrous/semi-ui-vue';

    const data = `{
    "name": "Semi",
    "version": "0.0.0"
}`;
</script>

<template>
    <div :style="{ marginBottom: '16px' }">
        <JsonViewer :height="200" :modelValue="data" :width="700">
            <template #searchButton="{ defaultButton, showSearchBar, onToggleSearchBar }">
                <div :style="{ position: 'absolute', top: '10px', right: '10px', zIndex: 10 }">
                    <Button v-if="!showSearchBar" @click="onToggleSearchBar">
                        <template #icon>
                            <IconSearch />
                        </template>
                        搜索
                    </Button>
                    <component :is="defaultButton" v-else />
                </div>
            </template>
        </JsonViewer>
    </div>
</template>

API Reference

JsonViewer

Props

PropertyDescriptionTypeDefault
modelValueText of the editor, used with v-model. 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.stringundefined
widthWidth. A number is taken as px; a string is used as is.string | numbersemiDefault("JsonViewer", "width", 400)
heightHeight. A number is taken as px; a string is used as is.string | numbersemiDefault("JsonViewer", "height", 400)
showSearchWhether to show the search button.booleansemiDefault("JsonViewer", "showSearch", true)
optionsEditor 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.JsonViewerOptionssemiGlobalDefault("JsonViewer", "options")
limitSearchButtonBoundsWhether to limit the search button drag bounds within the component container.booleansemiDefault("JsonViewer", "limitSearchButtonBounds", false)

Events

EventDescriptionCallback parameters
update:modelValueEmitted when the content changes, for v-model to write back, with the editor's current text.[value: string]
changeEmitted when the content changes, with the editor's current text. It is a component event, so @change does not receive the native change bubbling up from the search bar inputs.[value: string]

Slots

SlotDescriptionScope parameters
customRenderReplaces the content of a token matched by options.customRenderRule, with the matched rule item, its index and the token text content as scope parameters. Without it, or when empty, the token keeps its text. Only takes effect in read-only mode.JsonViewerCustomRenderSlotProps
searchButtonReplaces the default search button as a whole. defaultButton is the default button's VNode, rendered with <component :is>; the other parameters build a custom search UI. Not rendered when showSearch is false.JsonViewerSearchButtonSlotProps

Exposes

NameDescriptionType
getValueReturns the editor's current text.Type() => string
formatFormats the content with formatOptions.Type() => void
searchSearches for text; the optional parameters control case sensitivity, whole word and regular expression.Type(searchText: string, caseSensitive?: boolean | undefined, wholeWord?: boolean | undefined, regex?: boolean | undefined) => void
getSearchResultsReturns the current search results.Type() => FindMatch[] | null | undefined
prevSearchMoves to the previous search result, with an optional step.Type(step?: number | undefined) => void
nextSearchMoves to the next search result, with an optional step.Type(step?: number | undefined) => void
replaceReplaces the current search match. An empty string deletes the match; without a match it does nothing.Type(replaceText: string) => void
replaceAllReplaces all search matches. An empty string deletes them.Type(replaceText: string) => void
getStyleReturns the current width and height.Type() => { width: string | number; height: string | number; }

class, style and native attributes fall on the root element. When the root has no role and aria-label or aria-labelledby is passed, the root gets role="group"; an explicit role wins.

JsonViewerOptions

AttributeDescriptionTypeDefault
lineHeightHeight of each line of content, unit:pxnumber20
autoWrapWhether to wrap lines automatically.booleantrue
readOnlyWhether to be read-only.booleanfalse
customRenderRuleCustom render rulesCustomRenderRule[]-
formatOptionsContent format settingFormattingOptions-
completionOptionsAuto-completion settingCompletionOptions-
ariaLabelAccessible name of the content area (the read-only scroll region, or the editable textbox)stringlocale JsonViewer.content

CustomRenderRule

The type is named JsonViewerCustomRenderRule in Vue. A rule holds data only; the content of a matched token is drawn by the #customRender slot.

AttributeDescriptionTypeDefault
matchMatching rulestring | RegExp | (value: string | number | boolean | null, path: string, tokenType: 'key' | 'value') => boolean-
nameRule name, to tell rules apart in the #customRender slotstring-

FormattingOptions

AttributeDescriptionTypeDefault
tabSizeIndent sizenumber4
insertSpacesWhether to use spaces for indentationbooleantrue
eolLine break characterstring'\n'

CompletionOptions

AttributeDescriptionTypeDefault
staticCompletionsStatic 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 readCompletionItem[]-

SearchControls

With the #searchButton slot, the scope parameters (type JsonViewerSearchButtonSlotProps) contain the following properties besides defaultButton:

AttributeDescriptionType
showSearchBarWhether the search bar is currently visibleboolean
onToggleSearchBarToggle search bar visibility() => void
onSearchExecute search(text: string, caseSensitive?: boolean, wholeWord?: boolean, regex?: boolean) => void
onPrevSearchNavigate to previous search result() => void
onNextSearchNavigate to next search result() => void
onReplaceReplace current search match(text: string) => void
onReplaceAllReplace all search matches(text: string) => void

Methods

Methods exposed by the component can be called through a template ref (type JsonViewerHandle) to achieve certain special interactions.

MethodDescription
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 NameSizeSize (Gzip)
JsonViewer203.14kb51.23kb
MonacoEditor5102.0 KB1322.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 Name1k lines5k lines10 thousand lines100 thousand lines500 thousand lines1 million lines3 million lines
JsonViewer30.42ms30.66ms36.87ms52.73ms111.02ms178.81ms506.25ms
ReactMonacoEditor72.01ms73.76ms76.64ms97.89ms133.31ms202.79ms495.53ms
Performance improvement57.70%58.41%51.87%46.11%---