Recursive rendering of JSON tree with HTML elements (Input, Radio, etc.) - json

I am quite new to React and working with JSON structures. I am trying to construct a recursive render of a JSON tree structure that dynamically renders individual HTML elements (e.g. radio buttons, dropdown menus, etc.) from the tree. I have seen other implementations, but they do not have nested HTML elements that differ from li, ul, etc. They also do not typically have different naming conventions further down the tree (e.g. attributes, options).
The tree looks like this:
{
"id": "1",
"name": "Animals",
"color": "#e37939",
"shape": "bounding_box",
"attributes": [
{
"id": "1.1",
"name": "Type",
"type": "radio",
"required": false,
"options": [
{
"id": "1.1.1",
"optionName": "Cats",
"optionValue": "cats",
"options": [.... and so on
};
What I ultimately what to achieve is to get to a format where one clicks the 'Animals button', which then renders the nested radio button, and if one selects the 'cats' option value it'd render the next dropdown menu. I have set up an initial set of methods, but I can't quite figure out how to dynamically render the next set of nested options when an option is clicked. I have created a React fiddle here: https://codesandbox.io/s/vigilant-grothendieck-jknym
The biggest challenge is to get the nested recursive options embedded in an options group. I haven't been able to figure out how to do that yet.

I have created a datastructure for what you want to achieve , altho i have tweaked it a bit as there are parts of it redundant but you can still keep both data structure and convert between them.it goes recursively as deep as you want it to go.
const prodList = [
{
id: "1",
name: "Animals",
options: [
{
id: "1.1",
name: "Type",
inputType: "radio",
options: [
{
id: "1.1.1",
name: "Cats",
value: "Cats",
inputType: "select",
options: [
{ id: "1.1.1.1", name: "Siamese Grey", value: "Siamese Grey" },
{ id: "1.1.1.2", name: "Siamese Black", value: "Siamese Black" },
{ id: "1.1.1.3", name: "Siamese Cute", value: "Siamese Cute" },
{ id: "1.1.1.4", name: "House Cat", value: "House Cat" },
{ id: "1.1.1.5", name: "House Cat", value: "House Cat" }
]
},
{ id: "1.1.2", name: "Dogs", value: "Dogs" },
{ id: "1.1.3", name: "Cows", value: "Cows" }
]
}
]
}
];
above is the data structure where you have "inputType" property that helps determining what component to show. we will have a basic component , a radio component and a select component of each type which can render each other inside them.
export default class ProductsPage extends Component {
render() {
let prodItems = prodList.map(p => {
return <MainContentManager data={p} key={p.id} />;
});
return <div>{prodItems}</div>;
}
}
class MainContentManager extends Component {
render() {
let renderObj = null;
renderObj = basicMethod(renderObj, this.props.data);
return (
<div>
<h6> {this.props.data.name}</h6>
{renderObj}
</div>
);
}
}
class RadioButtonManager extends Component {
constructor(props) {
super(props);
this.state = {
activeOptionIndex: 0
};
this.handleInputClick = this.handleInputClick.bind(this);
}
handleInputClick(index) {
this.setState({
activeOptionIndex: index
});
}
render() {
let renderObj = null;
let renderDat = null;
renderDat = this.props.data.options.map((op, index) => {
return (
<label key={op.id}>
<input
type="radio"
onChange={e => {
this.handleInputClick(index);
}}
checked={index == this.state.activeOptionIndex ? true : false}
/>
{op.name}
</label>
);
});
renderObj = basicMethod(renderObj, {
options: [this.props.data.options[this.state.activeOptionIndex]]
});
return (
<div>
<h6> {this.props.data.name}</h6>
{renderDat}
{renderObj}
</div>
);
}
}
class SelectManager extends Component {
constructor(props) {
super(props);
this.state = { value: "", activeOptionIndex: 0 };
this.handleInputClick = this.handleInputClick.bind(this);
}
handleInputClick(value) {
let activeOptionIndex = this.state.activeOptionIndex;
if (this.props.data.options) {
for (let i = 0; i < this.props.data.options.length; i++) {
if (this.props.data.options[i].value == value) {
activeOptionIndex = i;
}
}
}
this.setState({
value: value,
activeOptionIndex: activeOptionIndex
});
}
render() {
let renderObj = null;
let selectOptions = this.props.data.options.map((op, index) => {
return (
<option key={op.value} value={op.value}>
{op.name}
</option>
);
});
renderObj = basicMethod(renderObj, {
options: [this.props.data.options[this.state.activeOptionIndex]]
});
return (
<div>
<select
onChange={e => {
this.handleInputClick(e.target.value);
}}
>
{selectOptions}
</select>
{renderObj}
</div>
);
}
}
function basicMethod(renderObj, data) {
if (data && data.options) {
renderObj = data.options.map(op => {
!op && console.log(data);
let comp = null;
if (op.inputType == "radio") {
comp = <RadioButtonManager data={op} key={op.id} />;
} else if (op.inputType == "select") {
comp = <SelectManager data={op} key={op.id} />;
} else {
comp = <MainContentManager data={op} key={op.id} />;
}
return comp;
});
}
return renderObj;
}
ask anything if it is unclear or you want it a bit different.

Related

How to keep empty cells as empty in JSON output instead of ignore the cell (Vue-XLSX)

I'm reading a excel file and converting it to JSON object using vue-xlsx in a Nuxt project. But there can be empty cells. Vue-xlsx ignores them.
Returned JSON object is like this:
[
{ "Header1": "a", "Header3": "c" },
{ "Header1": "d", "Header2": "e", "Header3": "f" },
{ "Header1": "g", "Header2": "h", "Header3": "i" }
]
I don't need to print this using 'xlsx-table' component because I need JSON object.
Tried to pass defval property as options on this way. But not success.
<section>
<input type="file" #change="onChange" />
<xlsx-read :options="options" :file="file">
<xlsx-json :sheet="selectedSheet" >
<template #default="{collection}">
<!-- <xlsx-table :sheet="selectedSheet" /> -->
<div>
{{ json = collection }}
</div>
</template>
</xlsx-json>
</xlsx-read>
</section>
<script>
export default {
data() {
return {
file: null,
options: {
defval: "",
},
json: null,
selectedSheet: 0,
};
},
}
</script>
I need some help to generate this kind of JSON object:
How Can I do this with Vue-XLSX.
This can be done with VanillaJS, no need to rely on vue-xlxs on this one.
If we do start from
[
{ "Header1": "a", "Header3": "c" },
{ "Header1": "d", "Header2": "e", "Header3": "f" },
{ "Header1": "g", "Header2": "h", "Header3": "i" }
]
We can have the following
<script>
export default {
data() {
return {
array: [
{ Header1: 'a', Header3: 'c' },
{ Header1: 'd', Header2: 'e', Header3: 'f' },
{ Header1: 'g', Header2: 'h', Header3: 'i' },
],
}
},
mounted() {
const neededHeaders = ['Header1', 'Header2', 'Header3']
const arrayWithFallback = this.array.map((row) => {
for (const headerKey of neededHeaders) {
if (!(headerKey in row)) {
// if you're missing one of the header, let's create a default value for it
row[headerKey] = 'your-fallback-here' // can also be an empty string of course
}
}
return row
})
console.log('arrayWithFallback', arrayWithFallback)
},
}
</script>
And it'll create this at the end
[
{ "Header1": "a", "Header2": "your-fallback-here", "Header3": "c" },
{ "Header1": "d", "Header2": "e", "Header3": "f" },
{ "Header1": "g", "Header2": "h", "Header3": "i" }
]
I used "parsed" event that was mentioned in the documentation for "xlsx-json" component. Here is the full Nuxt component for me:
If you have the "header info":
<template>
<div>
<h3>Import XLSX</h3>
<input type="file" #change="onChange" />
<xlsx-read :file="file">
<xlsx-json #parsed = "parsFunc">
<template #default="{collection}">
<div class="divShow">
{{ collection }}
</div>
</template>
</xlsx-json>
</xlsx-read>
</div>
</template>
<script>
/* put the address that matches for your app */
import { XlsxRead, XlsxJson } from "~/node_modules/vue-xlsx/dist/vue-xlsx.es.js";
export default {
components: {
XlsxRead,
XlsxJson
},
data() {
return {
file: null,
/* this array contains the exact words that you write on the first line of xlsx file */
headerData: ["Header1", "Header2", "Header3"],
};
},
computed: {
lengData: function() {
return this.headerData.length;
}
},
methods: {
onChange(event) {
this.file = event.target.files ? event.target.files[0] : null;
},
sortOutput(elemEach) {
/* this function sorts the new object */
let arrObj = Object.entries(elemEach);
arrObj.sort((a,b) => a[0].localeCompare(b[0]));
let arraySorted = Object.fromEntries(arrObj);
return arraySorted;
},
parsFunc(event) {
/* this function loops throught elements in "xlsx" file and replaces empty cells with "" */
let counter = 0;
event.forEach(element => {
for (let it = 0; it < this.lengData; it++) {
if( element[this.headerData[it]] == undefined ) {
element[this.headerData[it]] = "";
}
}
let sortedElem = this.sortOutput(element);
event[counter] = sortedElem;
counter++;
}); // end of forEach
}
}
};
</script>
<style scoped>
/* this is the styles that I used */
.divShow {
background-color: #865611;
padding: 50px;
color: white;
}
</style>
If you want to dynamically generate the "header info":
<template>
<div>
<h3>Import XLSX</h3>
<input type="file" #change="onChange" />
<xlsx-read :file="file">
<xlsx-json #parsed = "parsFunc">
<template #default="{collection}">
<div class="divShow">
{{ collection }}
</div>
</template>
</xlsx-json>
</xlsx-read>
</div>
</template>
<script>
/* put the address that matches for your app */
import { XlsxRead, XlsxJson } from "~/node_modules/vue-xlsx/dist/vue-xlsx.es.js";
export default {
components: {
XlsxRead,
XlsxJson
},
data() {
return {
file: null,
headerData: [],
};
},
computed: {
lengData: function() {
return this.headerData.length;
}
},
methods: {
onChange(event) {
this.file = event.target.files ? event.target.files[0] : null;
},
findHeader(elem, newArr) {
/* this function finds the headers of the "xlsx" file */
this.headerData = [];
let convertArr = Object.entries(elem);
convertArr.forEach(element2 => {
newArr.push( Object.entries(element2[1]).length );
});
let indexOfMaxValue = newArr.reduce((iMax, x, i, arr) => x > arr[iMax] ? i : iMax, 0);
let arrayExtract = Object.entries( convertArr[indexOfMaxValue][1] );
arrayExtract.forEach(element3 => {
this.headerData.push(element3[0]);
});
this.replaceEmpty(elem);
},
sortOutput(elemEach) {
/* this function sorts the new object */
let arrObj = Object.entries(elemEach);
arrObj.sort((a,b) => a[0].localeCompare(b[0]));
let arraySorted = Object.fromEntries(arrObj);
return arraySorted;
},
parsFunc(event) {
/* this function acts after submitting file and call another function to find "headers" */
let emptyArr = [];
this.findHeader(event, emptyArr);
},
replaceEmpty: function(eventPass) {
/* this function loops throught elements in "xlsx" file and replaces empty cells with "" */
let counter = 0;
eventPass.forEach(element => {
for (let it = 0; it < this.lengData; it++) {
if( element[this.headerData[it]] == undefined ) {
element[this.headerData[it]] = "";
}
}
let sortedElem = this.sortOutput(element);
eventPass[counter] = sortedElem;
counter++;
}); // end of forEach
} // end of replaceEmpty
}
};
</script>
<style scoped>
/* this is the styles that I used */
.divShow {
background-color: #865611;
padding: 50px;
color: white;
}
</style>
But for working on this version, you must have at least one full row in your xlsx file. For example this code does not work for this kind of xlsx file:

Error in vue.js (Multiple root nodes returned from render function)

i'm trying to generate html tags (child nodes) from JSON file with vue.js but i have this Error in console:
(Multiple root nodes returned from render function. Render function should return a single root node)
error screenshot
javaScript Code:
const createComponent = (dNode, h) => {
// Handle empty elements and return empty array in case the dNode passed in is empty
if (_.isEmpty(dNode)) {
return [];
}
// if the el is array call createComponent for all elements
if (_.isArray(dNode)) {
return dNode.map((child) => createComponent(child, h))
}
let children = [];
if (dNode.children && dNode.children.length > 0) {
dNode.children.forEach((c) => {
if (_.isString(c)) {
children.push(c)
} else {
children.push(createComponent(c, h))
}
});
}
// Need to clone
const properties = _.cloneDeep(dNode.properties)
return h(dNode.tagName, properties, children.length > 0? children : dNode.textNode)
}
/**
* A sample component uses the recursive createComponent to render a DOM / List of DOM nodes
*/
const MyComponent = Vue.component('my-component', {
render: function (h) {
return createComponent(this.nodes, h)
},
props: {
nodes: {
type: Array,
required: true
}
}
});
new Vue({
el: "#app",
data: {
nodes: []
},
methods: {
getChildrens() {
this.$http.get('nodes.json').then(response => {
this.nodes = response.body;
}, response => {});
}
},
created() {
this.getShortCodes();
this.getChildrens();
}
});
this is nodes.json File Content
[
{
"tagName": "div",
"children": [
{
"tagName": "h1",
"textNode": "Great News"
},
{
"tagName": "h3",
"textNode": "YOU CAN CREATE VUE COMPONENTS OUT OF JSON"
},
{
"tagName": "a",
"properties": {
"attrs": {"href": "#"}
},
"textNode": "Vue.js"
},
{
"tagName": "h2",
"textNode": "Hello from the other side"
}
]
},
{
"tagName": "div",
"children": [
{
"tagName": "h1",
"textNode": "another title"
},
{
"tagName": "h3",
"textNode": "third item"
},
{
"tagName": "a",
"properties": {
"attrs": {"href": "#"}
},
"textNode": "Vue.js"
},
{
"tagName": "h2",
"textNode": "Hello from the other side"
}
]
}
]
This is the vue.js component which i passed nodes as a props
<div id="app">
<div>
<my-component :nodes="nodes"></my-component>
</div>
</div>
Your createComponent returns an array of VNodes on line 9.
return dNode.map((child) => createComponent(child, h))
It seems that you are always passing an array of node definitions on your component and so you are generating an array of VNodes and Vue doesn't like you to have more than one root element in a component.
You have a couple of ways out of this:
Wrap your array in another element. Something like this:
render: function (h) {
return h('div', {}, createComponent(this.nodes, h))
},
Generate one MyComponent for each top element in your JSON.
You could also change the definition of createComponent to always return a single component, but this could break the semantics of createComponent and you may not have access to that code.
This might be possible with this plugin: https://www.npmjs.com/package/vue-fragments
The plugin let's you do cool stuff like this:
import Fragment from 'vue-fragment'
Vue.use(Fragment.Plugin)
// or
import { Plugin } from 'vue-fragment'
Vue.use(Plugin)
// …
export const MyComponent {
template: '
<fragment>
<input type="text" v-model="message">
<span>{{ message }}</span>
</fragment>
',
data() { return { message: 'hello world }}
}
So the fragment itself won't be in the dom. Hope this helps you out, even though i'm quite late with this answer I see.

Map JSON for Chartjs with Angular 7

Im trying to map JSON Data to show it in a Bar-Chart. The final Array I need has to look like this:[883, 5925, 17119, 27114, 2758].
Actually, the Array I want to use to set the barChartData (dringlichkeitenValues[])seems to be empty. Sorry for my bad coding skills. Can anyone show me how to solve this Problem?
JSON:
[{
"id": 1,
"value": 883
},
{
"id": 2,
"value": 5925
},
{
"id": 3,
"value": 17119
},
{
"id": 4,
"value": 27144
},
{
"id": 5,
"value": 2758
}]
api.service.ts
getDringlichkeiten(): Observable<IDringlichkeit[]> {
return this.http.get<IDringlichkeit[]>(this.ROOT_URL + '/aufenthalte/dringlichkeit');}
dringlichkeit.ts
export interface IDringlichkeit {
id: number;
value: number;
}
bar-chart.component.ts
export class BarChartComponent implements OnInit {
public dringlichkeitValues:number[] = [];
public dringlichkeiten: IDringlichkeit[];
public barChartLabels:String[] = ["1", "2", "3", "4", "5"];
public barChartData:number[] = this.dringlichkeitValues;
public barChartType:string = 'bar';
constructor(private aufenthaltService: AufenthaltService) {
}
ngOnInit() {
this.loadData();
this.getDringlichkeitValues();
}
loadData(){
this.aufenthaltService.getDringlichkeiten()
.subscribe( data => this.dringlichkeiten = data);
}
getDringlichkeitValues(){
let dringlichkeitValues:number[]=[];
this.dringlichkeiten.forEach(dringlichkeit=>{
dringlichkeitValues.push(dringlichkeit.value)
this.dringlichkeitValues = dringlichkeitValues;
});
return this.dringlichkeitValues;
}
}
UPDATE:
I updated my component but now my Array is still empty after subscribing to the Observable.
bar-chart.component.ts
chart: Chart;
dringlichkeiten: IDringlichkeit[] = [];
constructor(private aufenthaltService: AufenthaltService) {
}
ngOnInit() {
this.aufenthaltService.getDringlichkeiten()
.subscribe( data => {
this.dringlichkeiten = data;
//dringlichkeiten-Array full
console.log(this.dringlichkeiten);
});
//dringlichkeiten-Array empty
console.log(this.dringlichkeiten);
this.chart = new Chart('canvas', {
type: 'bar',
data: {
labels: this.dringlichkeiten.map(x => x.id),
datasets: [
{
label: 'Dringlichkeiten',
data: this.dringlichkeiten.map(x => x.value),
backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#E7E9ED', '#36A2EB']
}
]
},
});
}
To get the "values" from your JSON array, you can use:
dringlichkeiten.map(x => x.value)
This will get you an array you require, i.e.:
[883, 5925, 17119, 27114, 2758]
You can then pass this array to chartJS for it to render you a chart like so:
this.chart = new Chart('canvas', {
type: 'bar',
data: {
labels: dringlichkeiten.map(x => x.id),
datasets: [
{
label: 'My Bar Chart',
data: dringlichkeiten.map(x => x.value),
backgroundColor: ['red', 'green', 'yellow', 'blue', 'orange']
}
]
},
});
Take a look at this simplified working SlackBlitz example.
Hope this helps!

Set nested JSON Response as rowdata to ag-grid in Angular4

I am new to angular and doing a sample project in which I want to show some JSON data in a grid.
I'm using ag-grid for the same.
I have the following Json response that I'm getting from a rest API :-
[
{
"id": 64,
"name": "Utopia",
"language": "English",
"genres": [
"Drama",
"Science-Fiction",
"Thriller"
],
"status": "Ended",
"image": {
"medium": "http://static.tvmaze.com/uploads/images/medium_portrait/0/474.jpg",
"original": "http://static.tvmaze.com/uploads/images/original_untouched/0/474.jpg"
}
},
{
"id": 65,
"name": "Bones",
"language": "English",
"genres": [
"Drama",
"Crime",
"Medical"
],
"status": "Ended",
"image": {
"medium": "http://static.tvmaze.com/uploads/images/medium_portrait/80/201202.jpg",
"original": "http://static.tvmaze.com/uploads/images/original_untouched/80/201202.jpg"
}
}
]
I was able to successfully bind the data for the simple keys like id, name, language etc. but when it comes to binding the nested object I'm not able to do it.
If you look at the above json response, The 'image' field is an object. How can I get the value of 'medium' or 'original' key from it and just show the image in my row ?
Some help is appreciated, as this is the point I'm getting stuck at.
Below is my component code :-
shows.component.ts
#Component({
selector: 'app-shows',
templateUrl: './shows.component.html',
styleUrls: ['./shows.component.css']
})
export class ShowsComponent implements OnInit {
public gridOptions: GridOptions;
public tvShowsColumnDefs = new ShowColumn;
public showMetaData: any;
constructor(private _contentService: ContentService, private _router: Router,
private _route: ActivatedRoute) {
// GridOptions Initialized
this.gridOptions = <GridOptions>{};
this.gridOptions.columnDefs = this.tvShowsColumnDefs.columnDefs;
}
ngOnInit() {
// Prepare Grid Row Data
this.prepareRowData();
}
prepareRowData() {
// API Call for getting TV-Shows
this._contentService.getAllShows()
.subscribe(response => {
const shows = response;
console.log('TVShows-API Response ', shows);
// Setting Grid RowData using api response
this.gridOptions.api.setRowData(shows);
});
}
show.columnDef.ts
export class ShowColumn {
public columnDefs = [
{ field: 'id', headerName: '', width: 50 },
{ field: 'image', headerName: '', width: 50, cellRendererFramework: null},
{ field: 'name', headerName: '', width: 250},
{ field: 'language', headerName: 'Language', width: 100},
{ field: 'genres', headerName: 'Genres', width: 250},
{ field: 'status', headerName: 'Status', width: 145 }
];
constructor() { }
}
The nested properties are accessible by the dot notation (.), e.g.:
{ field: 'image.medium', headerName: '', width: 50}
For the nested arrays, a value-getter will most likely do the job:
function genreValueGetter(params) {
const arr = params.data.genres as Array<string>;
return arr.join(', ');
}
{ headerName: 'Genres', valueGetter: genreValueGetter, width: 250},
First let me build classes:
export class myShow {
image: myImage;
id: number;
...
constructor(obj: any) {
this.document = new myImage(obj.image);
this.id = obj.id;
...
}
}
export class myImage {
medium: string;
original: string;
constructor(obj?: any) {
if(obj){
this.medium = obj.medium;
this.original = obj.original;
}
}
}
Then you can use .map operator
allShows: myShow[] = [];
prepareRowData(){
this._contentService.getAllShows().map((shows: myShow[])=> {
return shows.map((show: myShow)=>{
return new myShow(show);
})
}).subscribe((allShows)=> {
this.allShows = allShows;
});
}

Getting json object data with react

I am attempting to pull data out of json like this, which is imported as "values"
{
"content": {
"person": [
{
"name": "Test"
"age" : "24:
}
]
}
}
I am using .map like below but getting the error .default.map is not a function I believe it is because i have objects not arrays, i've tried a bunch of stuff including object.keys but i'm getting errors all over the place, any direction would be appreciated.
import values from './sample.json'
const vals = values.map((myval, index) => {
const items = person.items.map((item, i) => {
return (
<div>{item.name}</div>
)
})
return (
<div>{items}</div>
)
})
I think your data and code have some errors. But after fixing those and also changing the name from 'person' to 'people' if that's what you are after, here's the code that does what you are trying to do:
var data = {
content: {
people: [
{
name: "Test",
age: 24,
},
{
name: "Foo",
age: 25,
},
],
},
};
var App = React.createClass({
render: function () {
var people = data.content.people.map(function (person) {
return <div>{person.name}</div>;
});
return <div>{people}</div>;
},
});
ReactDOM.render(<App />, document.getElementById("app"));
And here's the JSBin for that: https://jsbin.com/coyalec/2/edit?html,js,output
Update: I'm updating the answer with more detailed example. It now deals with data more generically, like it doesn't assume what are the entries of 'contents' and such, but it knows that each type like 'people' or 'pets' are an array.
var data = {
content: {
people: [
{
name: "Test",
age: 24,
},
{
name: "Foo",
age: 25,
},
],
pets: [
{
name: "Sweety",
age: 3,
},
{
name: "Kitty",
age: 5,
},
],
},
};
var App = React.createClass({
render: function () {
// Get the keys in data.content. This will return ['people', 'pets']
var contentKeys = Object.keys(data.content);
// Now start iterating through these keys and use those keys to
// retrieve the underlying arrays and then extract the name field
var allNames = contentKeys.map((t) =>
data.content[t].map((e) => <div>{e.name}</div>)
);
return <div>{allNames}</div>;
},
});
ReactDOM.render(<App />, document.getElementById("app"));
And here's the latest JSBin: https://jsbin.com/coyalec/4/edit?html,js,output