GrJsonViewer
Берут, когда ответ чужого сервиса разбирают по полям.
Обзор
Когда брать
- ответ чужого сервиса разбирают по полям — вебхук, ответ модели, тело ошибки: нужно найти ключ, а не прочитать документ целиком;
- структура заранее неизвестна — форма приходит от бэкенда и меняется, и раскладывать её по колонкам не по чему;
- значение нужно достать точечно — копируется узел вместе с путём, а не вся простыня;
- данных много — свёртка, поиск и виртуализация держат ответ на тысячи узлов.
Когда взять другое
| Нужно | Берите |
|---|---|
| Прочитать текст или JSON целиком и скопировать разом | GrCodeBlock (пакет @feugene/granularity-code) |
| Дерево своих данных с выбором и чекбоксами | GrTree |
| Данные разложены по колонкам и известны заранее | GrDataTable |
| Пара «характеристика → значение» | GrDescriptionList |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrJsonViewer
:value="value"
rootLabel="order"
:defaultExpandDepth="1"
searchable
/>Примеры 3 у каждого — живое демо и его код
Тела вебхуков с читаемыми значениями
default-expand-depthраскрывает ровно столько, сколько видно с одного взгляда, аcopyableотдаёт поддерево как JSON-текст. Что значит значение, просмотрщик не знает — знает приложение: слотleafрисует центы евро, ISO-даты датами, а URL ссылками, при этом копируется сырое значение.Webhook deliveries · https://api.northwind.example/hooks/billing payload:{4}id:"evt_3Q8x"type:"invoice.paid"created:data:{1}invoice:{6}id:"INV-0418"customer:{3}amount_paid:€1,284.50currency:"eur"hosted_url:https://pay.example.com/i/INV-0418lines:[2]Запись клиента для инженера поддержки
searchableнаходит ключ или значение, не читая весь ответ, а сохранённые запросы поддержки — «Карты», «НДС» — запускают тот же поиск через методfilter().max-heightдержит дерево в собственном окне, аcopyсообщает путь скопированного.GET /v1/customers/cus_N7200 OK · 86 ms · support viewcustomer:{11}id:"cus_N7"object:"customer"name:"Northwind Traders"email:"[email protected]"tax:{4}address:{4}subscription:{6}payment_methods:[2]metadata:{3}balance:-1250delinquent:falseЖурнал запроса к модели в пределах
Картинка в base64 и вектор из 1 536 чисел делают дерево нечитаемым и медленным.
max-string-lengthобрезает картинку до заглушки — копирование всё равно отдаёт целое, —max-array-itemsостанавливает вектор после двадцати чисел, аvirtualдержит в DOM только строки у области просмотра.Request log · req_7f2cA 22 KB image and 1,536 numbers, shown as much as is useful log:{5}id:"req_7f2c"model:"vision-large-2026-08"latency_ms:2140request:{2}messages:[2]0:{2}role:"system"content:"You read receipts and return the merchant, the date and the total."1:{2}role:"user"content:[2]0:{2}type:"text"text:"Taxi receipt from the Lisbon trip."1:{2}type:"image"source:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42m"…temperature:0response:{3}output:{4}merchant:"Taxi Lisboa"date:"2026-10-16"total:24.6currency:"EUR"embedding:[1536]0:01:0.2054452:0.3746023:0.4775934:0.4962275:0.4272136:0.2827417:0.0883278:-0.1216879:-0.31020810:-0.44393611:-0.49925312:-0.46638713:-0.35114314:-0.17387715:0.03410116:0.23605617:0.39631618:0.48657519:0.490891…:1516ещёusage:{2}input_tokens:1288output_tokens:31
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Кегль дерева и высота поиска и кнопок панели. Не задан — из `GrConfigProvider`, иначе `md`. |
ariaLabel | string | undefined | undefined | Имя области для скринридера. Безымянное дерево объявляется просто «дерево». |
virtual | boolean | undefined | false | Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. |
maxHeight | string | number | undefined | undefined | Высота области просмотра со своим скроллом — и без `virtual`. Число — пиксели, строка — как есть. |
rootLabel | string | undefined | undefined | Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. |
defaultExpandDepth | number | undefined | undefined | Сколько уровней раскрыто изначально. |
maxStringLength | number | undefined | undefined | Длина строкового значения, после которой показ обрезается. Не косметика: запрос к модели с картинкой в base64 — это один лист на сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не берут. Копирование при этом отдаёт значение целиком. |
maxArrayItems | number | undefined | undefined | Сколько элементов массива разбирать до заглушки «ещё N». |
searchable | boolean | undefined | undefined | Панель над деревом: поле поиска и «Развернуть всё / Свернуть всё». По умолчанию **включена** (умолчание меняется через `GrConfigProvider`). `false` убирает панель целиком: поиск остаётся на `filter()`, раскрытие — на `expandAll()`/`collapseAll()` из `defineExpose`. |
copyable | boolean | undefined | undefined | Кнопка копирования узла в строке. |
valueобязательный | unknown | — | Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. |
Слоты
| Slot | Type | Описание |
|---|---|---|
leaf | { node: GrJsonNode; } | Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. |
События
| Event | Сигнатура | Описание |
|---|---|---|
copy | (event: "copy", payload: { path: string; value: unknown; }): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
filter | (value: string) => void | Отфильтровать дерево по ключу или значению — тем же запросом, что в поле поиска. Пустая строка снимает фильтр. |
expandAll | () => void | Раскрыть все ветки. |
collapseAll | () => void | Свернуть все ветки, включая корень. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrJsonViewerConfigurableProps, GrJsonNodeKind, GrJsonNode } from '@feugene/granularity/components/GrJsonViewer'Файлdist/types/components/GrJsonViewer/config.d.ts
const grJsonViewerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrJsonViewer">;Файлdist/types/components/GrJsonViewer/defaults.d.ts
Пропы GrJsonViewer, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrJsonViewer: { … } }">.
Пороги обрезки здесь наравне с оформлением не случайно: «сколько показывать»
— решение приложения, а не экрана. У админки с ответами модели и у панели с
конфигами оно разное, и повторять его на каждом просмотрщике значит однажды
забыть.
interface GrJsonViewerConfigurableProps {
size: GrComponentSize;
defaultExpandDepth: number;
maxStringLength: number;
maxArrayItems: number;
searchable: boolean;
copyable: boolean;
}Файлdist/types/components/GrJsonViewer/jsonToNodes.d.ts
Что за значение в узле. Решает и цвет, и то, раскрывается ли узел.
type GrJsonNodeKind = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'type GrJsonNode = {
/** Читаемый адрес узла (`$.items[3].name`). Он же ключ для `GrTree`. */
path: string;
/** Имя ключа или индекс. Показывается слева от двоеточия. */
label: string;
kind: GrJsonNodeKind;
/** Что видно в строке. У ветки — счётчик, у листа — значение, возможно обрезанное. */
preview: string;
/** Полное значение. Копирование берёт его, а не `preview`. */
value: unknown;
/** Показ обрезан: копирование всё равно отдаёт целое. */
truncated: boolean;
isLeaf: boolean;
children?: GrJsonNode[];
};Файлdist/types/components/GrJsonViewer/GrJsonViewer.vue.d.ts
JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.
Дерево, клавиатуру и виртуализацию несёт GrTree; здесь — разбор unknown в
узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —
GrCodeBlock.
interface GrJsonViewerProps {
/** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */
value: unknown;
/** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */
rootLabel?: string;
/** Сколько уровней раскрыто изначально. */
defaultExpandDepth?: number;
/**
* Длина строкового значения, после которой показ обрезается.
*
* Не косметика: запрос к модели с картинкой в base64 — это один лист на
* сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не
* берут. Копирование при этом отдаёт значение целиком.
*/
maxStringLength?: number;
/** Сколько элементов массива разбирать до заглушки «ещё N». */
maxArrayItems?: number;
/**
* Панель над деревом: поле поиска и «Развернуть всё / Свернуть всё». По
* умолчанию **включена** (умолчание меняется через `GrConfigProvider`).
* `false` убирает панель целиком: поиск остаётся на `filter()`, раскрытие —
* на `expandAll()`/`collapseAll()` из `defineExpose`.
*/
searchable?: boolean;
/** Кнопка копирования узла в строке. */
copyable?: boolean;
/** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */
virtual?: boolean;
/**
* Высота области просмотра со своим скроллом — и без `virtual`. Число —
* пиксели, строка — как есть.
*/
maxHeight?: string | number;
/** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */
ariaLabel?: string;
/** Кегль дерева и высота поиска и кнопок панели. Не задан — из `GrConfigProvider`, иначе `md`. */
size?: GrComponentSize;
}interface GrJsonViewerEmits {
/** Узел скопирован: путь для тикета и полное значение. */
(e: 'copy', payload: {
path: string;
value: unknown;
}): void;
}Файлdist/types/components/GrJsonViewer/safelist.d.ts
Пуст: составных классов у просмотрщика нет. Цвет значения берётся из мапы
jsonValueClass целым литералом, как и остальные классы
grJsonViewerStyles.ts, и granum извлекает их из чанков компонента сам,
включая общие.
const grJsonViewerSafelist: string[];Файлdist/types/components/GrJsonViewer/index.d.ts
type GrJsonViewerInstance = ComponentExposed<typeof GrJsonViewerComponent>;Доступность
Паттерн APG
tree (через GrTree)
| Клавиша | Действие |
|---|---|
| — | своих клавиш нет: дерево внутри — GrTree, и весь его контракт действует без изменений. Поле поиска и кнопки свёртки над деревом — обычные контролы, каждый со своей остановкой Tab |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Показ обрезается, копирование — нет
Длинное строковое значение обрезается в строке (maxStringLength), длинный
массив обрывается заглушкой «ещё N» (maxArrayItems). Это не косметика:
запрос к модели с картинкой в base64 — один лист на сотни тысяч символов, и
ни свёртка по узлам, ни виртуализация по строкам его не берут, потому что узел
там один и строка одна.
В буфер при этом уходит полное значение узла вместе с путём: обрезка принадлежит показу, и вставить её обратно нельзя.
Повторная ссылка — не цикл
Маркер [Circular] достаётся только настоящему предку по цепочке, а объект,
честно положенный в данные дважды, рисуется дважды.
Это тот случай, где обход дерева умеет строго больше сериализации: replacer
у JSON.stringify стека предков не получает и вынужден метить любую повторную
ссылку. Обход стек имеет, поэтому у GrCodeBlock и GrJsonViewer на одних и
тех же данных разный — и каждый по-своему правильный — результат.
Адрес узла читается
Ключом узла служит путь ($.items[3].name), а не порядковый номер: он уходит в
событие copy, по нему же задаётся раскрытие. Ключ с точкой или пробелом
экранируется ($["a.b"]), иначе адрес перестал бы быть адресом.
Раскрытие задаётся глубиной, а не списком
defaultExpandDepth раскрывает первые N уровней; expandAll() и
collapseAll() из defineExpose переключают всё дерево. Обе кнопки
сбрасывают ручное раскрытие пользователя — от «раскрыть всё» этого и ждут.
Поиск идёт по ключу и по значению
В чужом ответе ищут то одно, то другое, поэтому предикат смотрит и на имя ключа, и на показанное значение. Совпавшие узлы дерево подсвечивает целиком и раскрывает к ним путь.
Панель над деревом — поле поиска и «Развернуть всё / Свернуть всё» — включена
по умолчанию. Её можно убрать (searchable: false) и звать filter(query),
expandAll() и collapseAll() снаружи — когда строка поиска на странице уже
есть и вторая была бы лишней.
maxHeight ограничивает область просмотра своим скроллом и без virtual.
Границы
Не редактирует и не диффит: просмотрщик показывает то, что пришло. Подсветки
совпавшей подстроки внутри строки нет — дерево отмечает узел целиком.
Виртуализация включается только вместе с maxHeight
(GrTree): без высоты окно считать не от чего.
Пропы этого примера