Понадобилось тут отобразить данные в виде дерева, с возможностью редактировать разные поля, удалять/добавлять строки и т.д. В процессе поиска подходящих компонентов (хотелось найти под material-ui и react) стал пробовать devextreme-reactive. Ньюанс, однако, оказался в том, что devextreme-reactive хочет данные для дерева в виде плоского массива объектов, в каждом из которых указан parent_id «родителя». А GraphQL сервер у меня отдаёт дерево в виде вложенных друг в друга объектов с массивами объектов. Пришлось делать из одного другое — возможно, кому-то пригодится. А может кто-то скажет, что я заморочился не по делу и всё это делается куда проще.
Итак, в ответ на GraphQL запрос (есть тесты, в каждом есть вопросы, для каждого опроса есть несколько вариантов ответов и мы хотим получить всё сразу):
Получаем от сервера ответ вида:
Для нормализации используем normalizr:
В описании схемы, через processStrategy, добавляем в детей свойства pid со ссылками на родителей. К слову, в свежем normalizr изменился способ описания схем, из-за чего примеры с assignEntity, ArrayOf, define (каких много) — практически неактуальны.
Получаем такое:
Нас отсюда интересует только .entities
К слову, в процессе въезжания в normalizr, зачитавшись issues, обнаружил, что далеко не я один пытаюсь использовать его не вполне по назначению (наверное просто потому, что это чуть ли не единственный подобный инструмент). Много народу жаждет всяких фич, чтобы получить результат в как можно более настраиваемом формате. Но авторы — кремень.
В силу вышесказанного, результат работы normalizr придётся рихтовать при помощи flat (рекурсивно разворачиваем до нужного уровня вложенности):
Получаем следующее:
Избавляемся от индексов:
Получаем:
Можно было бы почистить оставшиеся тут вложенные массивы questions, answers, но это уже мелочи — на отображение не влияют. А __typename нужны, чтобы потом при редактировании понимать, с чем имеем дело.
В компоненте результат обрабатывается так, как это показано у них в примере:
Вроде бы альтернативой всему вышеописанному может являться чтение непосредственно содержимого GraphQL store (в клиенте Apollo) — там всё тоже должно быть уже плоским. Но, честно говоря, я не нашёл, как это можно стандартным способом сделать, да и не очень уверен, что формат, в котором там хранятся данные не изменится в новых версиях.
Итак, в ответ на GraphQL запрос (есть тесты, в каждом есть вопросы, для каждого опроса есть несколько вариантов ответов и мы хотим получить всё сразу):
query TestQuery {
tests {
id
title
questions {
id
title
answers {
id
title
}
}
}
}
Получаем от сервера ответ вида:
Заголовок спойлера
{
"data": {
"tests": [
{
"id": "test_1",
"title": "Test 1",
"questions": [
{
"id": "question_1",
"title": "Question 1 (for t1)",
"answers": [
{
"id": "answer_1",
"title": "Answer 1 (for q1)"
},
{
"id": "answer_2",
"title": "Answer 2 (for q1)"
}
]
},
{
"id": "question_2",
"title": "Question 2 (for t1)",
"answers": [
{
"id": "answer_1_2",
"title": "Answer 1 (for q2)"
}
]
}
]
},
{
"id": "test_2",
"title": "Test 2",
"questions": [
{
"id": "question_1_2",
"title": "Question 1 (for t2)",
"answers": []
}
]
},
{
"id": "test_3",
"title": "Test 3",
"questions": []
}
]
}
}
Для нормализации используем normalizr:
В описании схемы, через processStrategy, добавляем в детей свойства pid со ссылками на родителей. К слову, в свежем normalizr изменился способ описания схем, из-за чего примеры с assignEntity, ArrayOf, define (каких много) — практически неактуальны.
const answerSchema = new schema.Entity('answers',{}, {
processStrategy: (entity, parent, key) => { return { ...entity, pid: parent.id} }
}
)
const questionSchema = new schema.Entity('questions',{
answers:[answerSchema]}, {
processStrategy: (entity, parent, key) => { return { ...entity, pid: parent.id} }
},
)
const testSchema = new schema.Entity('tests',{questions:[questionSchema]}, {
processStrategy: (entity, parent, key) => { return { ...entity, pid: 0 } }
}
)
const nRes = normalize(result.data, {tests: [testSchema]})
Получаем такое:
Заголовок спойлера
{
"entities": {
"answers": {
"answer_1": {
"id": "answer_1",
"title": "Answer 1 (for q1)",
"__typename": "Answer",
"pid": "question_1"
},
"answer_2": {
"id": "answer_2",
"title": "Answer 2 (for q1)",
"__typename": "Answer",
"pid": "question_1"
},
"answer_1_2": {
"id": "answer_1_2",
"title": "Answer 1 (for q2)",
"__typename": "Answer",
"pid": "question_2"
}
},
"questions": {
"question_1": {
"id": "question_1",
"title": "Question 1 (for t1)",
"answers": [
"answer_1",
"answer_2"
],
"__typename": "Question",
"pid": "test_1"
},
"question_2": {
"id": "question_2",
"title": "Question 2 (for t1)",
"answers": [
"answer_1_2"
],
"__typename": "Question",
"pid": "test_1"
},
"question_1_2": {
"id": "question_1_2",
"title": "Question 1 (for t2)",
"answers": [
],
"__typename": "Question",
"pid": "test_2"
}
},
"tests": {
"test_1": {
"id": "test_1",
"title": "Test 1",
"questions": [
"question_1",
"question_2"
],
"__typename": "Test",
"pid": 0
},
"test_2": {
"id": "test_2",
"title": "Test 2",
"questions": [
"question_1_2"
],
"__typename": "Test",
"pid": 0
},
"test_3": {
"id": "test_3",
"title": "Test 3",
"questions": [
],
"__typename": "Test",
"pid": 0
}
}
},
"result": {
"tests": [
"test_1",
"test_2",
"test_3"
]
}
}
Нас отсюда интересует только .entities
const normalized = { entities: nRes.entities }
К слову, в процессе въезжания в normalizr, зачитавшись issues, обнаружил, что далеко не я один пытаюсь использовать его не вполне по назначению (наверное просто потому, что это чуть ли не единственный подобный инструмент). Много народу жаждет всяких фич, чтобы получить результат в как можно более настраиваемом формате. Но авторы — кремень.
В силу вышесказанного, результат работы normalizr придётся рихтовать при помощи flat (рекурсивно разворачиваем до нужного уровня вложенности):
const flattened = flatten({ entities: nRes.entities }, { maxDepth: 3 })
Получаем следующее:
Заголовок спойлера
{
"entities.answers.answer_1": {
"id": "answer_1",
"title": "Answer 1 (for q1)",
"__typename": "Answer",
"pid": "question_1"
},
"entities.answers.answer_2": {
"id": "answer_2",
"title": "Answer 2 (for q1)",
"__typename": "Answer",
"pid": "question_1"
},
"entities.answers.answer_1_2": {
"id": "answer_1_2",
"title": "Answer 1 (for q2)",
"__typename": "Answer",
"pid": "question_2"
},
"entities.questions.question_1": {
"id": "question_1",
"title": "Question 1 (for t1)",
"answers": [
"answer_1",
"answer_2"
],
"__typename": "Question",
"pid": "test_1"
},
"entities.questions.question_2": {
"id": "question_2",
"title": "Question 2 (for t1)",
"answers": [
"answer_1_2"
],
"__typename": "Question",
"pid": "test_1"
},
"entities.questions.question_1_2": {
"id": "question_1_2",
"title": "Question 1 (for t2)",
"answers": [
],
"__typename": "Question",
"pid": "test_2"
},
"entities.tests.test_1": {
"id": "test_1",
"title": "Test 1",
"questions": [
"question_1",
"question_2"
],
"__typename": "Test",
"pid": 0
},
"entities.tests.test_2": {
"id": "test_2",
"title": "Test 2",
"questions": [
"question_1_2"
],
"__typename": "Test",
"pid": 0
},
"entities.tests.test_3": {
"id": "test_3",
"title": "Test 3",
"questions": [
],
"__typename": "Test",
"pid": 0
}
}
Избавляемся от индексов:
Object.keys(flattened).forEach( (key)=> rows.push(flattened[key]) )
Получаем:
Заголовок спойлера
[
{
"id": "answer_1",
"title": "Answer 1 (for q1)",
"__typename": "Answer",
"pid": "question_1"
},
{
"id": "answer_2",
"title": "Answer 2 (for q1)",
"__typename": "Answer",
"pid": "question_1"
},
{
"id": "answer_1_2",
"title": "Answer 1 (for q2)",
"__typename": "Answer",
"pid": "question_2"
},
{
"id": "question_1",
"title": "Question 1 (for t1)",
"answers": [
"answer_1",
"answer_2"
],
"__typename": "Question",
"pid": "test_1"
},
{
"id": "question_2",
"title": "Question 2 (for t1)",
"answers": [
"answer_1_2"
],
"__typename": "Question",
"pid": "test_1"
},
{
"id": "question_1_2",
"title": "Question 1 (for t2)",
"answers": [
],
"__typename": "Question",
"pid": "test_2"
},
{
"id": "test_1",
"title": "Test 1",
"questions": [
"question_1",
"question_2"
],
"__typename": "Test",
"pid": 0
},
{
"id": "test_2",
"title": "Test 2",
"questions": [
"question_1_2"
],
"__typename": "Test",
"pid": 0
},
{
"id": "test_3",
"title": "Test 3",
"questions": [
],
"__typename": "Test",
"pid": 0
}
]
Можно было бы почистить оставшиеся тут вложенные массивы questions, answers, но это уже мелочи — на отображение не влияют. А __typename нужны, чтобы потом при редактировании понимать, с чем имеем дело.
В компоненте результат обрабатывается так, как это показано у них в примере:
...
<CustomTreeData getChildRows={getChildRows} />
...
const getChildRows = (currentRow, rootRows) => {
const childRows = rootRows.filter(r => r.pid === (currentRow ? currentRow.id : 0));
const res = childRows.length ? childRows : null
return res
}
...
Вроде бы альтернативой всему вышеописанному может являться чтение непосредственно содержимого GraphQL store (в клиенте Apollo) — там всё тоже должно быть уже плоским. Но, честно говоря, я не нашёл, как это можно стандартным способом сделать, да и не очень уверен, что формат, в котором там хранятся данные не изменится в новых версиях.
Комментарии (6)
Kvet
04.07.2018 14:40Первый пример на сайте показывает как работать с иерархичными данными: devexpress.github.io/devextreme-reactive/react/grid/docs/guides/tree-data
R33GTRVspec
Спасибо за инфу.
А почему у них в демке чекбоксы не отмечаются у детей при отметке родителей?
Это же косяк, в моем понимании, причем серьезный.
frog Автор
Это там и в других примерах так…
Могу ошибаться (не использовал отметки) но, по-моему, у них дерево — это нахлобучка сверху на обычный grid. Соответственно, для выборки детей логику нужно самому дописывать, что для примеров посчитали лишним.
Kvet
Спасибо за фидбек по поводу рекурсивного селекшена. Планировали добавить.