I have a complex json file that I have to handle with TypeScript / Javascript to make it hierarchical, in order to later build a questionnaire. Every entry of the json has a Id (unique), ParentId (0 If root), Text, Description.
My Typescript Interface
export interface Question {
Id: number;
Text: string;
Desc: string;
ParentId: number;
ChildAnswers?: Answer[];
}
export interface Answer {
Id: number;
Text: string;
Desc: string;
ParentId: number;
ChildQuestion?: Question;
}
I can guarantee that when the object is an answer it will only have one child which we can assume to be a question.
Flat Data Example :
[
{
Id: 1,
Text: 'What kind of apple is it?',
Desc: '',
ParentId: 0
},
{
Id: 2,
Text: 'Green Apple',
Desc: '',
ParentId: 1
},
{
Id: 3,
Text: 'Red Apple',
Desc: '',
ParentId: 1
},
{
Id: 4,
Text: 'Purple GMO Apple',
Desc: '',
ParentId: 1
},
{
Id: 5,
Text: 'What is the issue with the apple?',
Desc: '',
ParentId: 2
},
{
Id: 6,
Text: 'Spoiled.',
Desc: '',
ParentId: 5
},
{
Id: 7,
Text: 'Taste Bad.',
Desc: '',
ParentId: 5
},
{
Id: 8,
Text: 'Too Ripe.',
Desc: '',
ParentId: 5
},
{
Id: 9,
Text: 'Is not an apple.',
Desc: '',
ParentId: 5
},
{
Id: 10,
Text: 'The apple was not green.',
Desc: '',
ParentId: 5
},
... So on ...
]
My Goal
{
Id: 1,
Text: 'What kind of apple is it?',
Desc: '',
ParentId: 0,
ChildAnswers: [
{
Id: 2,
Text: 'Green Apple',
Desc: '',
ParentId: 1,
ChildQuestion: {
Id: 5,
Text: 'What is the issue with the apple?',
Desc: '',
ParentId: 2,
ChildAnswers: [
{
Id: 6,
Text: 'Spoiled.',
Desc: '',
ParentId: 5,
... So on ...
},
{
Id: 7,
Text: 'Taste Bad.',
Desc: '',
ParentId: 5,
... So on ...
},
{
Id: 8,
Text: 'Too Ripe.',
Desc: '',
ParentId: 5,
... So on ...
},
{
Id: 9,
Text: 'Is not an apple.',
Desc: '',
ParentId: 5,
... So on ...
},
{
Id: 10,
Text: 'The apple was not green.',
Desc: '',
ParentId: 5,
... So on ...
},
... So on ...
]
}
},
{
Id: 3,
Text: 'Red Apple',
Desc: '',
ParentId: 1,
... So on ...
},
{
Id: 4,
Text: 'Red Apple',
Desc: '',
ParentId: 1,
... So on ...
}
... So on ...
]
}
I'm currently using this list_to_tree function I found here on stackoverflow, I just don't know if how to tell a question and answer apart. Should I just check to see if the length is one for a question or at odd intervals mark it?:
function list_to_tree(list) {
var map = {}, node, roots = [], i;
for (i = 0; i < list.length; i += 1) {
map[list[i].Id] = i; // initialize the map
list[i].Children = []; // initialize the children
}
for (i = 0; i < list.length; i += 1) {
node = list[i];
if (node.ParentId !== 0) {
// if you have dangling branches check that map[node.ParentId] exists
list[map[node.ParentId]].Children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
Here is a brute force solution to the problem:
var flat = [
{
Id: 1,
Text: 'What kind of apple is it?',
Desc: '',
ParentId: 0
},
{
Id: 2,
Text: 'Green Apple',
Desc: '',
ParentId: 1
},
{
Id: 3,
Text: 'Red Apple',
Desc: '',
ParentId: 1
},
{
Id: 4,
Text: 'Purple GMO Apple',
Desc: '',
ParentId: 1
},
{
Id: 5,
Text: 'What is the issue with the apple?',
Desc: '',
ParentId: 2
},
{
Id: 6,
Text: 'Spoiled.',
Desc: '',
ParentId: 5
},
{
Id: 7,
Text: 'Taste Bad.',
Desc: '',
ParentId: 5
},
{
Id: 8,
Text: 'Too Ripe.',
Desc: '',
ParentId: 5
},
{
Id: 9,
Text: 'Is not an apple.',
Desc: '',
ParentId: 5
},
{
Id: 10,
Text: 'The apple was not green.',
Desc: '',
ParentId: 5
},
]
// first get the roots
const tree = flat.filter((question) => question.ParentId === 0);
// Next we are going to call alternating methods recursively.
function populateQuestionChildren(node) {
const { Id } = node;
flat.forEach((answer) => {
if (answer.ParentId === Id) {
if (!node.ChildAnswers) {
node.ChildAnswers = [];
}
node.ChildAnswers.push(answer);
populateAnswerChildren(answer);
}
});
}
function populateAnswerChildren(node) {
const { Id } = node;
flat.forEach((question) => {
if (question.ParentId === Id) {
if (!node.ChildQuestions) {
node.ChildQuestions = [];
}
node.ChildQuestions.push(question);
populateQuestionChildren(question);
}
});
}
// Kick off the build for each question tree.
tree.forEach((question) => {
populateQuestionChildren(question);
});
It is likely that there are more elegant solutions - but given that this will be only few dozen or a few hundred question/answers - this should get you what you need.
I used your interfaces and discovered a problem with my code. There is only one "ChildQuestion" on an Answer object. So here is my change to TypeScript to make it work properly. I hope it helps:
interface Question {
Id: number;
Text: string;
Desc: string;
ParentId: number;
ChildAnswers ? : Answer[];
}
interface Answer {
Id: number;
Text: string;
Desc: string;
ParentId: number;
ChildQuestion ? : Question;
}
// first get the roots
const tree = flat.filter((question) => question.ParentId === 0);
function populateQuestionChildren(node: Question) {
const { Id } = node;
flat.forEach((answer) => {
if (answer.ParentId === Id) {
if (!node.ChildAnswers) {
node.ChildAnswers = [];
}
node.ChildAnswers.push(answer);
populateAnswerChild(answer);
}
});
}
function populateAnswerChild(answer: Answer) {
const { Id } = answer;
// switch to every so we can break early once a question is found.
flat.every((node) => {
if (node.ParentId === Id) {
answer.ChildQuestion = node;
populateQuestionChildren(node);
return false;
}
return true;
});
}
tree.forEach((question) => {
populateQuestionChildren(question);
});
I would like to store result from mysql query in an array ( docAutocomplete) and after all the query completed I wanted to see the final array. I am using async series for this purpose. The issue is ...
I would like to store result from mysql query in an array ( docAutocomplete) and after all the query completed I wanted to see the final array. I am using async series for this purpose. The issue is ...
I am using a off canvas menu similar to the one on W3 Schools by having two functions called to toggle the menu on/off using onclick. I am wanting to add in a function to be able to close the menu by ...
I am using a off canvas menu similar to the one on W3 Schools by having two functions called to toggle the menu on/off using onclick. I am wanting to add in a function to be able to close the menu by ...
I have AWS Cognito installed for my Angular application, and I am trying to hook in the s3.putObject some how into the ng-file-upload Upload service. I want to use ng-file-upload's Upload.upload() ...
I have AWS Cognito installed for my Angular application, and I am trying to hook in the s3.putObject some how into the ng-file-upload Upload service. I want to use ng-file-upload's Upload.upload() ...
I want to know if it is more correct to use innerHTML: function createModal(){ modal = document.createElement('div'); modal.id = "modal"; modal.className = "modal"; modal.innerHTML = "<...
I want to know if it is more correct to use innerHTML: function createModal(){ modal = document.createElement('div'); modal.id = "modal"; modal.className = "modal"; modal.innerHTML = "<...